From 9738a2b50d7b73dc27006f2577fdf6487a0f7592 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 15 Sep 2025 21:02:52 -0400 Subject: [PATCH 1/4] initial impl of base sql api action provider --- .../src/action-providers/baseSqlApi/README.md | 87 +++++ .../baseSqlApiActionProvider.test.ts | 32 ++ .../baseSqlApi/baseSqlApiActionProvider.ts | 89 +++++ .../baseSqlApi/baseSqlApiDescription.ts | 316 ++++++++++++++++++ .../action-providers/baseSqlApi/constants.ts | 1 + .../baseSqlApi/exampleAction.test.ts | 75 +++++ .../src/action-providers/baseSqlApi/index.ts | 2 + .../action-providers/baseSqlApi/schemas.ts | 19 ++ .../agentkit/src/action-providers/index.ts | 1 + 9 files changed, 622 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/README.md create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/constants.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/index.ts create mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/README.md b/typescript/agentkit/src/action-providers/baseSqlApi/README.md new file mode 100644 index 000000000..8a0ab261f --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/README.md @@ -0,0 +1,87 @@ +# BaseSqlApi Action Provider + +This directory contains the **BaseSqlApiActionProvider** implementation, which provides actions for baseSqlApi operations. + +## Overview + +The BaseSqlApiActionProvider is designed to work with EvmWalletProvider for blockchain interactions. It provides a set of actions that enable the querying of real-time and historical onchain data on Base using custom SQL queries. + +Visit the [Base SQL API documentation](https://docs.cdp.coinbase.com/data/sql-api/welcome) for more information. + +## Directory Structure + +``` +baseSqlApi/ +├── baseSqlApiActionProvider.ts # Main provider implementation +├── baseSqlApiActionProvider.test.ts # Provider test suite +├── exampleAction.test.ts # Example action test suite +├── schemas.ts # Action schemas and types +├── index.ts # Package exports +├── constants.ts # Constant variables +├── baseSqlApiDescription.ts # Variables describing the action and valid SQL Schemas +└── README.md # Documentation (this file) +``` + +## Actions + +### Actions +- `execute_base_sql_query`: Execute a SQL query for Base data + - **Purpose**: Demonstrates the basic structure of an action + - **Input**: + - `fieldName` (string): A descriptive name for the field (1-100 chars) + - `amount` (string): The amount as a decimal string (e.g. "1.5") + - `optionalField` (string, optional): Optional parameter example + - **Output**: String describing the action result + - **Example**: + ```typescript + const result = await provider.exampleAction(walletProvider, { + fieldName: "test", + amount: "1.0" + }); + ``` + +## Implementation Details + +### Network Support +This provider supports all evm networks. + +### Wallet Provider Integration +This provider is specifically designed to work with EvmWalletProvider. Key integration points: +- Network compatibility checks +- Transaction signing and execution +- Balance and account management + +## Adding New Actions + +To add new actions: + +1. Define the schema in `schemas.ts`: + ```typescript + export const NewActionSchema = z.object({ + // Define your action's parameters + }); + ``` + +2. Implement the action in `baseSqlApiActionProvider.ts`: + ```typescript + @CreateAction({ + name: "new_action", + description: "Description of what your action does", + schema: NewActionSchema, + }) + async newAction( +walletProvider: EvmWalletProvider, args: z.infer + ): Promise { + // Implement your action logic + } + ``` + +## Testing + +When implementing new actions, ensure to: +1. Add unit tests for schema validations +2. Test network support + +## Notes + +- Requires an **CDP Client API Key** for authentication. Visit [CDP](https://portal.cdp.coinbase.com/projects/api-keys/client-key/) to get your key. \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts new file mode 100644 index 000000000..7a8e111aa --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts @@ -0,0 +1,32 @@ +/** + * BaseSqlApiActionProvider Tests + */ + +import { BaseSqlApiActionProvider } from "./baseSqlApiActionProvider"; +import { Network } from "../../network"; + +describe("BaseSqlApiActionProvider", () => { + // default setup: instantiate the provider + const provider = new BaseSqlApiActionProvider(); + + it("should support the protocol family", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + } as Network), + ).toBe(true); + }); + + it("should not support other protocol families", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "other-protocol-family", + } as Network), + ).toBe(false); + }); + + it("should handle invalid network objects", () => { + expect(provider.supportsNetwork({ protocolFamily: "invalid-protocol" } as Network)).toBe(false); + expect(provider.supportsNetwork({} as Network)).toBe(false); + }); +}); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts new file mode 100644 index 000000000..a42c0f9a1 --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { CreateAction } from "../actionDecorator"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { BaseSqlApiSchema } from "./schemas"; +import { description } from "./baseSqlApiDescription"; +import { BASE_SQL_API_URL } from "./constants"; + +/** + * BaseSqlApiActionProvider provides actions for baseSqlApi operations. + * + * @description + * This provider is designed to work with EvmWalletProvider for blockchain interactions. + * It supports querying on the Base network. + */ +export class BaseSqlApiActionProvider extends ActionProvider { + /** + * Constructor for the BaseSqlApiActionProvider. + */ + constructor() { + super("baseSqlApi", []); + } + + /** + * Base SQL API action provider + * + * @description + * This action queries the Coinbase SQL API endpoint to efficiently retrieve onchain data on Base. + * + * @param walletProvider - The wallet provider instance for blockchain interactions + * @param args - Arguments defined by BaseSqlApiSchema, i.e. the SQL query to execute + * @returns A promise that resolves to a string describing the query result + */ + @CreateAction({ + name: "execute_base_sql_query", + description, + schema: BaseSqlApiSchema, + }) + async exampleAction( + walletProvider: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const cdpApiKey = process.env.CDP_API_CLIENT_KEY; + + const response = await fetch(BASE_SQL_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${cdpApiKey}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ sql: args.sqlQuery }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + console.log("Resp: " + response) + + const result = await response.text(); + + console.log("Result: " + result) + + return `Query executed with result: ${result}.`; + } catch (error) { + return `Error executing Base SQL query: ${error}`; + } + } + + /** + * Checks if this provider supports the given network. + * + * @param network - The network to check support for + * @returns True if the network is supported + */ + supportsNetwork(network: Network): boolean { + return network.networkId === "base-mainnet"; + } +} + +/** + * Factory function to create a new BaseSqlApiActionProvider instance. + * + * @returns A new BaseSqlApiActionProvider instance + */ +export const baseSqlApiActionProvider = () => new BaseSqlApiActionProvider(); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts new file mode 100644 index 000000000..53d91865e --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts @@ -0,0 +1,316 @@ +const apiSchema = [ + { + tableName: "base.blocks", + fields: [ + { fieldName: "block_number", type: "uint64", description: "The number of the block" }, + { + fieldName: "block_hash", + type: "String", + description: "The unique hash identifying this block", + }, + { fieldName: "parent_hash", type: "String", description: "The hash of the parent block" }, + { + fieldName: "timestamp", + type: "DateTime", + description: "The timestamp when this block was created", + }, + { + fieldName: "miner", + type: "String", + description: "The address of the miner/validator who created this block", + }, + { fieldName: "nonce", type: "uint64", description: "The proof-of-work nonce value" }, + { + fieldName: "sha3_uncles", + type: "String", + description: "The hash of the uncles list for this block", + }, + { + fieldName: "transactions_root", + type: "String", + description: "The root hash of the transactions trie", + }, + { fieldName: "state_root", type: "String", description: "The root hash of the state trie" }, + { + fieldName: "receipts_root", + type: "String", + description: "The root hash of the receipts trie", + }, + { + fieldName: "logs_bloom", + type: "String", + description: "The bloom filter for the logs of the block", + }, + { + fieldName: "gas_limit", + type: "uint64", + description: "The maximum gas allowed in this block", + }, + { + fieldName: "gas_used", + type: "uint64", + description: "The total gas used by all transactions in this block", + }, + { + fieldName: "base_fee_per_gas", + type: "uint64", + description: "The base fee per gas in this block (EIP-1559)", + }, + { + fieldName: "total_difficulty", + type: "String", + description: "The total difficulty of the chain up to this block", + }, + { fieldName: "size", type: "uint64", description: "The size of this block in bytes" }, + { fieldName: "extra_data", type: "String", description: "Extra data field for this block" }, + { fieldName: "mix_hash", type: "String", description: "The mix hash for this block" }, + { + fieldName: "withdrawals_root", + type: "String", + description: "The root hash of withdrawals (post-merge)", + }, + { + fieldName: "parent_beacon_block_root", + type: "String", + description: "The parent beacon block root (post-merge)", + }, + { + fieldName: "blob_gas_used", + type: "uint64", + description: "The amount of blob gas used in this block", + }, + { + fieldName: "excess_blob_gas", + type: "uint64", + description: "The excess blob gas in this block", + }, + { + fieldName: "transaction_count", + type: "uint64", + description: "The number of transactions in this block", + }, + { + fieldName: "action", + type: "Int8", + description: "Indicates if block was added (1) or removed (-1) due to chain reorganization", + }, + ], + }, + { + tableName: "base.events", + fields: [ + { fieldName: "block_number", type: "uint64", description: "The block number" }, + { + fieldName: "block_hash", + type: "String", + description: "Keccak-256 hash of the block header; verifies block contents", + }, + { + fieldName: "timestamp", + type: "DateTime64", + description: "Time at which the block was created", + }, + { + fieldName: "transaction_hash", + type: "String", + description: "Keccak-256 hash of the signed transaction; unique tx identifier", + }, + { + fieldName: "transaction_to", + type: "String", + description: "Address the transaction is acting against (EOA or contract)", + }, + { fieldName: "transaction_from", type: "String", description: "Originating address (EOA)" }, + { + fieldName: "transaction_index", + type: "uint64", + description: "Order of the transaction within the block", + }, + { + fieldName: "log_index", + type: "uint64", + description: "Index of the log within the transaction (0-based)", + }, + { + fieldName: "address", + type: "String", + description: "Contract address that created the log", + }, + { + fieldName: "topics", + type: "Array(String)", + description: "Indexed params and the keccak256 of the event signature", + }, + { fieldName: "event_name", type: "String", description: "Human-readable event name" }, + { + fieldName: "event_signature", + type: "String", + description: "Full canonical declaration (name + parameter types)", + }, + { + fieldName: "parameters", + type: "Map(String, Variant(Bool, Int256, String, uint256))", + description: "Parameter name -> value", + }, + { + fieldName: "parameter_types", + type: "Map(String, String)", + description: "Parameter name -> ABI type", + }, + { + fieldName: "action", + type: "Int8", + description: "1 if created; −1 if reorged out; sum > 0 means still active", + }, + ], + }, + { + tableName: "base.transactions", + fields: [ + { + fieldName: "block_number", + type: "uint64", + description: "The number of the block that contains this transaction", + }, + { + fieldName: "block_hash", + type: "String", + description: "The hash of the block that contains this transaction", + }, + { + fieldName: "transaction_hash", + type: "String", + description: "The unique hash identifying this transaction", + }, + { + fieldName: "transaction_index", + type: "uint64", + description: "Index position within the block", + }, + { fieldName: "from_address", type: "String", description: "Originating address" }, + { fieldName: "to_address", type: "String", description: "Destination address" }, + { fieldName: "value", type: "String", description: "Transferred value" }, + { fieldName: "gas", type: "uint64", description: "Gas limit" }, + { fieldName: "gas_price", type: "uint64", description: "Gas price (wei)" }, + { fieldName: "input", type: "String", description: "Data payload" }, + { + fieldName: "nonce", + type: "uint64", + description: "Count of prior transactions from the sender", + }, + { fieldName: "type", type: "uint64", description: "Transaction type" }, + { + fieldName: "max_fee_per_gas", + type: "uint64", + description: "Max fee per gas the sender will pay", + }, + { + fieldName: "max_priority_fee_per_gas", + type: "uint64", + description: "Max priority fee per gas", + }, + { fieldName: "chain_id", type: "uint64", description: "Chain ID" }, + { fieldName: "v", type: "String", description: "Signature v" }, + { fieldName: "r", type: "String", description: "Signature r" }, + { fieldName: "s", type: "String", description: "Signature s" }, + { + fieldName: "is_system_tx", + type: "Bool", + description: "Whether this is a system transaction", + }, + { fieldName: "max_fee_per_blob_gas", type: "String", description: "Max fee per blob gas" }, + { + fieldName: "blob_versioned_hashes", + type: "Array(String)", + description: "Versioned hashes for associated blobs", + }, + { + fieldName: "timestamp", + type: "DateTime64", + description: "When the tx was included in a block", + }, + { fieldName: "action", type: "Int8", description: "1 if added, −1 if removed due to reorg" }, + ], + }, + { + tableName: "base.encoded_logs", + fields: [ + { fieldName: "block_number", type: "uint64", description: "Block number containing the log" }, + { + fieldName: "block_hash", + type: "String", + description: "Hash of the block containing the log", + }, + { fieldName: "block_timestamp", type: "DateTime64", description: "Timestamp of that block" }, + { + fieldName: "transaction_hash", + type: "String", + description: "Hash of the transaction containing the log", + }, + { + fieldName: "transaction_to", + type: "String", + description: "Transaction recipient (EOA or contract)", + }, + { fieldName: "transaction_from", type: "String", description: "Transaction sender (EOA)" }, + { + fieldName: "log_index", + type: "uint32", + description: "Log index within the transaction (0-based)", + }, + { + fieldName: "address", + type: "String", + description: "Contract address that created the log", + }, + { + fieldName: "topics", + type: "Array(String)", + description: "Indexed params / signature hash", + }, + { + fieldName: "action", + type: "Enum8('removed' = -1, 'added' = 1)", + description: "1 = created; −1 = reorged out; sum > 0 means active", + }, + ], + }, + { + tableName: "base.transfers", + fields: [ + { + fieldName: "block_number", + type: "uint64", + description: "Block number containing the transfer", + }, + { fieldName: "block_timestamp", type: "DateTime64", description: "Block timestamp" }, + { fieldName: "transaction_to", type: "String", description: "Transaction recipient address" }, + { fieldName: "transaction_from", type: "String", description: "Transaction sender address" }, + { fieldName: "log_index", type: "uint32", description: "Log index within the transaction" }, + { fieldName: "token_address", type: "String", description: "Token contract address" }, + { + fieldName: "from_address", + type: "String", + description: "Address tokens were transferred from", + }, + { + fieldName: "to_address", + type: "String", + description: "Address tokens were transferred to", + }, + { fieldName: "value", type: "uint256", description: "Amount of tokens transferred" }, + { fieldName: "action", type: "Enum8", description: "Action flag: 1 add, −1 reorg removal" }, + ], + }, +]; + +const schemaJson = JSON.stringify(apiSchema, null, 2); + +export const description = ` + This action can call Coinbase's Base SQL API to retrieve onchain data on Base. + The SQL API schema is a set of opinionated tables and columns used to organize onchain data for efficient retrieval. + + The supported table names, and fields in each table, for SQL queries are defined in the json string: + + ${schemaJson} +`; diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts b/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts new file mode 100644 index 000000000..8b768e6a8 --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts @@ -0,0 +1 @@ +export const BASE_SQL_API_URL = "https://api.cdp.coinbase.com/platform/v2/data/query/run"; diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts b/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts new file mode 100644 index 000000000..10f2217c0 --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts @@ -0,0 +1,75 @@ +import { BaseSqlApiActionProvider } from "./baseSqlApiActionProvider"; +import { BaseSqlApiSchema } from "./schemas"; +import { EvmWalletProvider } from "../../wallet-providers"; + +describe("Base SQL API Action Provider tests", () => { + const provider = new BaseSqlApiActionProvider(); + + let mockWalletProvider: jest.Mocked; + + beforeEach(() => { + mockWalletProvider = { + getAddress: jest.fn(), + getBalance: jest.fn(), + getName: jest.fn(), + getNetwork: jest.fn().mockReturnValue({ + protocolFamily: "evm", + networkId: "base-mainnet", + }), + nativeTransfer: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("schema validation", () => { + it("should validate example action schema", () => { + const validInput = { + sqlQuery: "test", + }; + const parseResult = BaseSqlApiSchema.safeParse(validInput); + expect(parseResult.success).toBe(true); + if (parseResult.success) { + expect(parseResult.data.sqlQuery).toBe("test"); + } + }); + + it("should reject invalid example action input", () => { + const invalidInput = { + fieldName: "", + amount: "invalid", + }; + const parseResult = BaseSqlApiSchema.safeParse(invalidInput); + expect(parseResult.success).toBe(false); + }); + }); + + describe("example action execution", () => { + it("should execute example action with wallet provider", async () => { + const args = { + sqlQuery: "test", + }; + const result = await provider.exampleAction(mockWalletProvider, args); + expect(result).toContain(args.sqlQuery); + expect(mockWalletProvider.getNetwork).toHaveBeenCalled(); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for base-mainnet with evm protocol", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-mainnet", + }), + ).toBe(true); + }); + + it("should return false for non-base networks", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "ethereum-mainnet", + }), + ).toBe(false); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/index.ts b/typescript/agentkit/src/action-providers/baseSqlApi/index.ts new file mode 100644 index 000000000..6ecd7702e --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/index.ts @@ -0,0 +1,2 @@ +export * from "./baseSqlApiActionProvider"; +export * from "./schemas"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts b/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts new file mode 100644 index 000000000..64edcbf33 --- /dev/null +++ b/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +/** + * Action schemas for the baseSqlApi action provider. + * + * This file contains the Zod schemas that define the shape and validation + * rules for action parameters in the baseSqlApi action provider. + */ + +/** + * Example action schema demonstrating various field types and validations. + * Replace or modify this with your actual action schemas. + */ +export const BaseSqlApiSchema = z.object({ + /** + * A descriptive name for the field + */ + sqlQuery: z.string().describe("The sql query to execute, using the defined tables and fields"), +}); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 36ebec21c..f3559413a 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -36,3 +36,4 @@ export * from "./zerodev"; export * from "./zeroX"; export * from "./zora"; export * from "./clanker"; +export * from "./baseSqlApi"; From df9a568b00d821781495cbc375b0923613998622 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 15 Sep 2025 21:45:58 -0400 Subject: [PATCH 2/4] tests and cleanup --- typescript/.changeset/lazy-hornets-kiss.md | 5 + .../src/action-providers/baseSqlApi/README.md | 26 ++-- .../baseSqlApiActionProvider.test.ts | 125 ++++++++++++++---- .../baseSqlApi/baseSqlApiActionProvider.ts | 20 +-- .../baseSqlApi/baseSqlApiDescription.ts | 1 + .../baseSqlApi/exampleAction.test.ts | 75 ----------- .../src/action-providers/baseSqlApi/index.ts | 2 +- .../action-providers/baseSqlApi/schemas.ts | 6 +- 8 files changed, 122 insertions(+), 138 deletions(-) create mode 100644 typescript/.changeset/lazy-hornets-kiss.md delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts diff --git a/typescript/.changeset/lazy-hornets-kiss.md b/typescript/.changeset/lazy-hornets-kiss.md new file mode 100644 index 000000000..870fc618f --- /dev/null +++ b/typescript/.changeset/lazy-hornets-kiss.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Added a new action provider to support Base SQL API queries diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/README.md b/typescript/agentkit/src/action-providers/baseSqlApi/README.md index 8a0ab261f..b1c4ccf53 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/README.md +++ b/typescript/agentkit/src/action-providers/baseSqlApi/README.md @@ -14,42 +14,32 @@ Visit the [Base SQL API documentation](https://docs.cdp.coinbase.com/data/sql-ap baseSqlApi/ ├── baseSqlApiActionProvider.ts # Main provider implementation ├── baseSqlApiActionProvider.test.ts # Provider test suite -├── exampleAction.test.ts # Example action test suite +├── baseSqlApiDescription.ts # Variables describing the action and valid SQL Schemas ├── schemas.ts # Action schemas and types ├── index.ts # Package exports ├── constants.ts # Constant variables -├── baseSqlApiDescription.ts # Variables describing the action and valid SQL Schemas └── README.md # Documentation (this file) ``` ## Actions -### Actions +### Execute Query Action - `execute_base_sql_query`: Execute a SQL query for Base data - - **Purpose**: Demonstrates the basic structure of an action + - **Purpose**: Query any onchain Base historical data - **Input**: - - `fieldName` (string): A descriptive name for the field (1-100 chars) - - `amount` (string): The amount as a decimal string (e.g. "1.5") - - `optionalField` (string, optional): Optional parameter example - - **Output**: String describing the action result + - `sqlQuery` (string): The sql query to run + - **Output**: String describing the query result - **Example**: ```typescript - const result = await provider.exampleAction(walletProvider, { - fieldName: "test", - amount: "1.0" + const result = await provider.executeBaseSqlQuery({ + sqlQuery: "SELECT size FROM base.blocks ORDER BY block_number DESC LIMIT 1", }); ``` ## Implementation Details ### Network Support -This provider supports all evm networks. - -### Wallet Provider Integration -This provider is specifically designed to work with EvmWalletProvider. Key integration points: -- Network compatibility checks -- Transaction signing and execution -- Balance and account management +This provider supports all evm networks, but can only be run against Base data. ## Adding New Actions diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts index 7a8e111aa..6f3efb7b3 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts @@ -1,32 +1,107 @@ -/** - * BaseSqlApiActionProvider Tests - */ - import { BaseSqlApiActionProvider } from "./baseSqlApiActionProvider"; -import { Network } from "../../network"; - -describe("BaseSqlApiActionProvider", () => { - // default setup: instantiate the provider - const provider = new BaseSqlApiActionProvider(); - - it("should support the protocol family", () => { - expect( - provider.supportsNetwork({ - protocolFamily: "evm", - } as Network), - ).toBe(true); +import { BaseSqlApiSchema } from "./schemas"; +import { BASE_SQL_API_URL } from "./constants"; + +describe("Base SQL API Action Provider", () => { + let provider: BaseSqlApiActionProvider; + let originalFetch: typeof fetch | undefined; + let mockFetch: jest.MockedFunction; + + beforeEach(() => { + provider = new BaseSqlApiActionProvider(); + + process.env.CDP_API_CLIENT_KEY = "test-token"; + + originalFetch = globalThis.fetch; + mockFetch = jest.fn() as jest.MockedFunction; + globalThis.fetch = mockFetch; + }); + + afterEach(() => { + mockFetch.mockReset(); + if (originalFetch) { + globalThis.fetch = originalFetch; + } + delete process.env.CDP_API_CLIENT_KEY; + }); + + describe("schema validation", () => { + it("validates a correct payload", () => { + const validInput = { sqlQuery: "SELECT 1" }; + const parsed = BaseSqlApiSchema.safeParse(validInput); + expect(parsed.success).toBe(true); + if (parsed.success) { + expect(parsed.data.sqlQuery).toBe("SELECT 1"); + } + }); + + it("rejects an incorrect payload", () => { + const invalidInput = { fieldName: "", amount: "invalid" }; + const parsed = BaseSqlApiSchema.safeParse(invalidInput); + expect(parsed.success).toBe(false); + }); }); - it("should not support other protocol families", () => { - expect( - provider.supportsNetwork({ - protocolFamily: "other-protocol-family", - } as Network), - ).toBe(false); + describe("executeBaseSqlQuery", () => { + it("POSTs to the Base SQL API with headers/body and returns the text result", async () => { + const args = { sqlQuery: "SELECT 1" }; + const mockText = JSON.stringify({ columns: ["one"], rows: [[1]] }); + + mockFetch.mockResolvedValue(new Response(mockText, { status: 200 })); + + const result = await provider.executeBaseSqlQuery(args); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + BASE_SQL_API_URL, + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer test-token", + "Content-Type": "application/json", + Accept: "application/json", + }), + body: JSON.stringify({ sql: args.sqlQuery }), + }), + ); + + expect(result).toBe(`Query executed with result: ${mockText}.`); + }); + + it("returns a readable error string when response.ok is false", async () => { + const args = { sqlQuery: "SELECT * FROM nope" }; + mockFetch.mockResolvedValue(new Response("Unauthorized", { status: 401 })); + + const result = await provider.executeBaseSqlQuery(args); + + expect(result).toContain("Error executing Base SQL query:"); + expect(result).toContain("HTTP error! status: 401"); + }); + + it("returns a readable error string when fetch throws", async () => { + const args = { sqlQuery: "SELECT * FROM throws" }; + mockFetch.mockRejectedValue(new Error("boom")); + + const result = await provider.executeBaseSqlQuery(args); + expect(result).toBe("Error executing Base SQL query: Error: boom"); + }); }); - it("should handle invalid network objects", () => { - expect(provider.supportsNetwork({ protocolFamily: "invalid-protocol" } as Network)).toBe(false); - expect(provider.supportsNetwork({} as Network)).toBe(false); + describe("supportsNetwork", () => { + it("returns true for any network", () => { + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "base-mainnet", + }), + ).toBe(true); + + expect( + provider.supportsNetwork({ + protocolFamily: "evm", + networkId: "ethereum-mainnet", + }), + ).toBe(true); + }); }); }); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts index a42c0f9a1..731d85dd2 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts @@ -11,8 +11,7 @@ import { BASE_SQL_API_URL } from "./constants"; * BaseSqlApiActionProvider provides actions for baseSqlApi operations. * * @description - * This provider is designed to work with EvmWalletProvider for blockchain interactions. - * It supports querying on the Base network. + * This provider supports SQL querying on the Base network. */ export class BaseSqlApiActionProvider extends ActionProvider { /** @@ -28,7 +27,6 @@ export class BaseSqlApiActionProvider extends ActionProvider * @description * This action queries the Coinbase SQL API endpoint to efficiently retrieve onchain data on Base. * - * @param walletProvider - The wallet provider instance for blockchain interactions * @param args - Arguments defined by BaseSqlApiSchema, i.e. the SQL query to execute * @returns A promise that resolves to a string describing the query result */ @@ -37,10 +35,7 @@ export class BaseSqlApiActionProvider extends ActionProvider description, schema: BaseSqlApiSchema, }) - async exampleAction( - walletProvider: EvmWalletProvider, - args: z.infer, - ): Promise { + async executeBaseSqlQuery(args: z.infer): Promise { try { const cdpApiKey = process.env.CDP_API_CLIENT_KEY; @@ -58,12 +53,8 @@ export class BaseSqlApiActionProvider extends ActionProvider throw new Error(`HTTP error! status: ${response.status}`); } - console.log("Resp: " + response) - const result = await response.text(); - console.log("Result: " + result) - return `Query executed with result: ${result}.`; } catch (error) { return `Error executing Base SQL query: ${error}`; @@ -73,11 +64,12 @@ export class BaseSqlApiActionProvider extends ActionProvider /** * Checks if this provider supports the given network. * - * @param network - The network to check support for + * @param _ - The network to check support for * @returns True if the network is supported */ - supportsNetwork(network: Network): boolean { - return network.networkId === "base-mainnet"; + supportsNetwork(_: Network): boolean { + // all networks + return true; } } diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts index 53d91865e..cf905551e 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts +++ b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts @@ -308,6 +308,7 @@ const schemaJson = JSON.stringify(apiSchema, null, 2); export const description = ` This action can call Coinbase's Base SQL API to retrieve onchain data on Base. + Call this action if the user requests historical data on Base. The SQL API schema is a set of opinionated tables and columns used to organize onchain data for efficient retrieval. The supported table names, and fields in each table, for SQL queries are defined in the json string: diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts b/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts deleted file mode 100644 index 10f2217c0..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/exampleAction.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { BaseSqlApiActionProvider } from "./baseSqlApiActionProvider"; -import { BaseSqlApiSchema } from "./schemas"; -import { EvmWalletProvider } from "../../wallet-providers"; - -describe("Base SQL API Action Provider tests", () => { - const provider = new BaseSqlApiActionProvider(); - - let mockWalletProvider: jest.Mocked; - - beforeEach(() => { - mockWalletProvider = { - getAddress: jest.fn(), - getBalance: jest.fn(), - getName: jest.fn(), - getNetwork: jest.fn().mockReturnValue({ - protocolFamily: "evm", - networkId: "base-mainnet", - }), - nativeTransfer: jest.fn(), - } as unknown as jest.Mocked; - }); - - describe("schema validation", () => { - it("should validate example action schema", () => { - const validInput = { - sqlQuery: "test", - }; - const parseResult = BaseSqlApiSchema.safeParse(validInput); - expect(parseResult.success).toBe(true); - if (parseResult.success) { - expect(parseResult.data.sqlQuery).toBe("test"); - } - }); - - it("should reject invalid example action input", () => { - const invalidInput = { - fieldName: "", - amount: "invalid", - }; - const parseResult = BaseSqlApiSchema.safeParse(invalidInput); - expect(parseResult.success).toBe(false); - }); - }); - - describe("example action execution", () => { - it("should execute example action with wallet provider", async () => { - const args = { - sqlQuery: "test", - }; - const result = await provider.exampleAction(mockWalletProvider, args); - expect(result).toContain(args.sqlQuery); - expect(mockWalletProvider.getNetwork).toHaveBeenCalled(); - }); - }); - - describe("supportsNetwork", () => { - it("should return true for base-mainnet with evm protocol", () => { - expect( - provider.supportsNetwork({ - protocolFamily: "evm", - networkId: "base-mainnet", - }), - ).toBe(true); - }); - - it("should return false for non-base networks", () => { - expect( - provider.supportsNetwork({ - protocolFamily: "evm", - networkId: "ethereum-mainnet", - }), - ).toBe(false); - }); - }); -}); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/index.ts b/typescript/agentkit/src/action-providers/baseSqlApi/index.ts index 6ecd7702e..e7d2151be 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/index.ts +++ b/typescript/agentkit/src/action-providers/baseSqlApi/index.ts @@ -1,2 +1,2 @@ export * from "./baseSqlApiActionProvider"; -export * from "./schemas"; \ No newline at end of file +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts b/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts index 64edcbf33..0383ce20c 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts +++ b/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts @@ -7,13 +7,9 @@ import { z } from "zod"; * rules for action parameters in the baseSqlApi action provider. */ -/** - * Example action schema demonstrating various field types and validations. - * Replace or modify this with your actual action schemas. - */ export const BaseSqlApiSchema = z.object({ /** - * A descriptive name for the field + * The SQL query to run */ sqlQuery: z.string().describe("The sql query to execute, using the defined tables and fields"), }); From bfbd0895547e04a16a69d009ca24b0cbba9e8771 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 15 Sep 2025 21:52:37 -0400 Subject: [PATCH 3/4] update agentkit readme --- typescript/agentkit/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index bd408cd20..934bd69ba 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -236,6 +236,15 @@ const agent = createReactAgent({
+Base SQL API + + + + + +
execute_base_sql_queryQueries the Base blockchain using SQL based on the defined tables and fields.
+
+
Clanker From 17662daf2794e059feceb65d36e0de39e3e3d25c Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 22 Sep 2025 20:01:19 -0400 Subject: [PATCH 4/4] move action provider to cdp, update description, other minor changes --- .../src/action-providers/baseSqlApi/README.md | 77 ---------- .../baseSqlApi/baseSqlApiActionProvider.ts | 81 ----------- .../action-providers/baseSqlApi/constants.ts | 1 - .../src/action-providers/baseSqlApi/index.ts | 2 - .../action-providers/baseSqlApi/schemas.ts | 15 -- .../src/action-providers/cdp/README.md | 8 +- .../cdpSqlApiActionProvider.test.ts} | 73 ++++++---- .../cdp/cdpSqlApiActionProvider.ts | 100 +++++++++++++ .../cdpSqlApiDescription.ts} | 131 +++++++++++++----- .../src/action-providers/cdp/constants.ts | 1 + .../src/action-providers/cdp/index.ts | 1 + .../src/action-providers/cdp/schemas.ts | 14 ++ .../agentkit/src/action-providers/index.ts | 1 - 13 files changed, 270 insertions(+), 235 deletions(-) delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/README.md delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/constants.ts delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/index.ts delete mode 100644 typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts rename typescript/agentkit/src/action-providers/{baseSqlApi/baseSqlApiActionProvider.test.ts => cdp/cdpSqlApiActionProvider.test.ts} (50%) create mode 100644 typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.ts rename typescript/agentkit/src/action-providers/{baseSqlApi/baseSqlApiDescription.ts => cdp/cdpSqlApiDescription.ts} (69%) create mode 100644 typescript/agentkit/src/action-providers/cdp/constants.ts diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/README.md b/typescript/agentkit/src/action-providers/baseSqlApi/README.md deleted file mode 100644 index b1c4ccf53..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# BaseSqlApi Action Provider - -This directory contains the **BaseSqlApiActionProvider** implementation, which provides actions for baseSqlApi operations. - -## Overview - -The BaseSqlApiActionProvider is designed to work with EvmWalletProvider for blockchain interactions. It provides a set of actions that enable the querying of real-time and historical onchain data on Base using custom SQL queries. - -Visit the [Base SQL API documentation](https://docs.cdp.coinbase.com/data/sql-api/welcome) for more information. - -## Directory Structure - -``` -baseSqlApi/ -├── baseSqlApiActionProvider.ts # Main provider implementation -├── baseSqlApiActionProvider.test.ts # Provider test suite -├── baseSqlApiDescription.ts # Variables describing the action and valid SQL Schemas -├── schemas.ts # Action schemas and types -├── index.ts # Package exports -├── constants.ts # Constant variables -└── README.md # Documentation (this file) -``` - -## Actions - -### Execute Query Action -- `execute_base_sql_query`: Execute a SQL query for Base data - - **Purpose**: Query any onchain Base historical data - - **Input**: - - `sqlQuery` (string): The sql query to run - - **Output**: String describing the query result - - **Example**: - ```typescript - const result = await provider.executeBaseSqlQuery({ - sqlQuery: "SELECT size FROM base.blocks ORDER BY block_number DESC LIMIT 1", - }); - ``` - -## Implementation Details - -### Network Support -This provider supports all evm networks, but can only be run against Base data. - -## Adding New Actions - -To add new actions: - -1. Define the schema in `schemas.ts`: - ```typescript - export const NewActionSchema = z.object({ - // Define your action's parameters - }); - ``` - -2. Implement the action in `baseSqlApiActionProvider.ts`: - ```typescript - @CreateAction({ - name: "new_action", - description: "Description of what your action does", - schema: NewActionSchema, - }) - async newAction( -walletProvider: EvmWalletProvider, args: z.infer - ): Promise { - // Implement your action logic - } - ``` - -## Testing - -When implementing new actions, ensure to: -1. Add unit tests for schema validations -2. Test network support - -## Notes - -- Requires an **CDP Client API Key** for authentication. Visit [CDP](https://portal.cdp.coinbase.com/projects/api-keys/client-key/) to get your key. \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts b/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts deleted file mode 100644 index 731d85dd2..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { z } from "zod"; -import { ActionProvider } from "../actionProvider"; -import { Network } from "../../network"; -import { CreateAction } from "../actionDecorator"; -import { EvmWalletProvider } from "../../wallet-providers"; -import { BaseSqlApiSchema } from "./schemas"; -import { description } from "./baseSqlApiDescription"; -import { BASE_SQL_API_URL } from "./constants"; - -/** - * BaseSqlApiActionProvider provides actions for baseSqlApi operations. - * - * @description - * This provider supports SQL querying on the Base network. - */ -export class BaseSqlApiActionProvider extends ActionProvider { - /** - * Constructor for the BaseSqlApiActionProvider. - */ - constructor() { - super("baseSqlApi", []); - } - - /** - * Base SQL API action provider - * - * @description - * This action queries the Coinbase SQL API endpoint to efficiently retrieve onchain data on Base. - * - * @param args - Arguments defined by BaseSqlApiSchema, i.e. the SQL query to execute - * @returns A promise that resolves to a string describing the query result - */ - @CreateAction({ - name: "execute_base_sql_query", - description, - schema: BaseSqlApiSchema, - }) - async executeBaseSqlQuery(args: z.infer): Promise { - try { - const cdpApiKey = process.env.CDP_API_CLIENT_KEY; - - const response = await fetch(BASE_SQL_API_URL, { - method: "POST", - headers: { - Authorization: `Bearer ${cdpApiKey}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ sql: args.sqlQuery }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.text(); - - return `Query executed with result: ${result}.`; - } catch (error) { - return `Error executing Base SQL query: ${error}`; - } - } - - /** - * Checks if this provider supports the given network. - * - * @param _ - The network to check support for - * @returns True if the network is supported - */ - supportsNetwork(_: Network): boolean { - // all networks - return true; - } -} - -/** - * Factory function to create a new BaseSqlApiActionProvider instance. - * - * @returns A new BaseSqlApiActionProvider instance - */ -export const baseSqlApiActionProvider = () => new BaseSqlApiActionProvider(); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts b/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts deleted file mode 100644 index 8b768e6a8..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const BASE_SQL_API_URL = "https://api.cdp.coinbase.com/platform/v2/data/query/run"; diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/index.ts b/typescript/agentkit/src/action-providers/baseSqlApi/index.ts deleted file mode 100644 index e7d2151be..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./baseSqlApiActionProvider"; -export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts b/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts deleted file mode 100644 index 0383ce20c..000000000 --- a/typescript/agentkit/src/action-providers/baseSqlApi/schemas.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from "zod"; - -/** - * Action schemas for the baseSqlApi action provider. - * - * This file contains the Zod schemas that define the shape and validation - * rules for action parameters in the baseSqlApi action provider. - */ - -export const BaseSqlApiSchema = z.object({ - /** - * The SQL query to run - */ - sqlQuery: z.string().describe("The sql query to execute, using the defined tables and fields"), -}); diff --git a/typescript/agentkit/src/action-providers/cdp/README.md b/typescript/agentkit/src/action-providers/cdp/README.md index 5d628e7c7..39928b061 100644 --- a/typescript/agentkit/src/action-providers/cdp/README.md +++ b/typescript/agentkit/src/action-providers/cdp/README.md @@ -12,8 +12,12 @@ cdp/ ├── cdpSmartWalletActionProvider.ts # Provider for CDP Smart Wallet operations ├── cdpEvmWalletActionProvider.test.ts # Tests for CDP EVM Wallet provider ├── cdpSmartWalletActionProvider.test.ts # Tests for CDP Smart Wallet provider +├── cdpSqlApiActionProvider.ts # Main provider implementation +├── cdpSqlApiActionProvider.test.ts # Provider test suite +├── baseSqlApiDescription.ts # Variables describing the action and valid SQL Schemas ├── schemas.ts # Action schemas for CDP operations ├── index.ts # Main exports +├── constants.ts # Constant variables └── README.md # This file ``` @@ -22,8 +26,8 @@ cdp/ ### CDP API Actions - `request_faucet_funds`: Request testnet funds from CDP faucet - - Available only on Base Sepolia, Ethereum Sepolia or Solana Devnet +- `execute_cdp_sql_query`: Execute a SQL query for Base or Sepolia Base data ### CDP EVM Wallet Actions @@ -62,4 +66,6 @@ The CDP providers support all networks available on the Coinbase Developer Platf - Requires CDP API credentials (API Key ID and Secret). Visit the [CDP Portal](https://portal.cdp.coinbase.com/) to get your credentials. +- Requires a **CDP Client API Key** for authentication. Visit [CDP](https://portal.cdp.coinbase.com/projects/api-keys/client-key/) to get your key. + For more information on the **Coinbase Developer Platform**, visit [CDP Documentation](https://docs.cdp.coinbase.com/). diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.test.ts similarity index 50% rename from typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts rename to typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.test.ts index 6f3efb7b3..6767effd3 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.test.ts @@ -1,16 +1,15 @@ -import { BaseSqlApiActionProvider } from "./baseSqlApiActionProvider"; -import { BaseSqlApiSchema } from "./schemas"; -import { BASE_SQL_API_URL } from "./constants"; +import { cdpSqlApiActionProvider } from "./cdpSqlApiActionProvider"; +import { CdpSqlApiSchema } from "./schemas"; +import { CDP_SQL_API_URL } from "./constants"; -describe("Base SQL API Action Provider", () => { - let provider: BaseSqlApiActionProvider; +describe("CDP SQL API Action Provider", () => { let originalFetch: typeof fetch | undefined; let mockFetch: jest.MockedFunction; - beforeEach(() => { - provider = new BaseSqlApiActionProvider(); + const mockApiKey = "test-token"; - process.env.CDP_API_CLIENT_KEY = "test-token"; + beforeEach(() => { + process.env.CDP_API_CLIENT_KEY = mockApiKey; originalFetch = globalThis.fetch; mockFetch = jest.fn() as jest.MockedFunction; @@ -25,10 +24,22 @@ describe("Base SQL API Action Provider", () => { delete process.env.CDP_API_CLIENT_KEY; }); + it("should throw if no API key is provided", () => { + delete process.env.CDP_API_CLIENT_KEY; + expect(() => cdpSqlApiActionProvider()).toThrow("CDP_API_CLIENT_KEY is not configured."); + }); + + it("should use provided API key from config", () => { + const provider = cdpSqlApiActionProvider({ cdpApiClientKey: "foo" }); + expect(provider).toBeDefined(); + }); + + const provider = cdpSqlApiActionProvider({ cdpApiClientKey: "test-token" }); + describe("schema validation", () => { it("validates a correct payload", () => { const validInput = { sqlQuery: "SELECT 1" }; - const parsed = BaseSqlApiSchema.safeParse(validInput); + const parsed = CdpSqlApiSchema.safeParse(validInput); expect(parsed.success).toBe(true); if (parsed.success) { expect(parsed.data.sqlQuery).toBe("SELECT 1"); @@ -37,23 +48,28 @@ describe("Base SQL API Action Provider", () => { it("rejects an incorrect payload", () => { const invalidInput = { fieldName: "", amount: "invalid" }; - const parsed = BaseSqlApiSchema.safeParse(invalidInput); + const parsed = CdpSqlApiSchema.safeParse(invalidInput); expect(parsed.success).toBe(false); }); }); - describe("executeBaseSqlQuery", () => { - it("POSTs to the Base SQL API with headers/body and returns the text result", async () => { + describe("executeCdpSqlQuery", () => { + it("POSTs to the CDP SQL API with headers/body and returns the text result", async () => { const args = { sqlQuery: "SELECT 1" }; - const mockText = JSON.stringify({ columns: ["one"], rows: [[1]] }); + const resultPayload = { columns: ["one"], rows: [[1]] }; - mockFetch.mockResolvedValue(new Response(mockText, { status: 200 })); + mockFetch.mockResolvedValue( + new Response(JSON.stringify({ result: resultPayload }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); - const result = await provider.executeBaseSqlQuery(args); + const result = await provider.executeCdpSqlQuery(args); expect(mockFetch).toHaveBeenCalledTimes(1); expect(mockFetch).toHaveBeenCalledWith( - BASE_SQL_API_URL, + CDP_SQL_API_URL, expect.objectContaining({ method: "POST", headers: expect.objectContaining({ @@ -65,30 +81,37 @@ describe("Base SQL API Action Provider", () => { }), ); - expect(result).toBe(`Query executed with result: ${mockText}.`); + expect(result).toBe(JSON.stringify(resultPayload)); }); it("returns a readable error string when response.ok is false", async () => { const args = { sqlQuery: "SELECT * FROM nope" }; - mockFetch.mockResolvedValue(new Response("Unauthorized", { status: 401 })); + const errorBody = { errorMessage: "Unauthorized" }; - const result = await provider.executeBaseSqlQuery(args); + mockFetch.mockResolvedValue( + new Response(JSON.stringify(errorBody), { + status: 401, + headers: { "Content-Type": "application/json" }, + }), + ); + + const result = await provider.executeCdpSqlQuery(args); - expect(result).toContain("Error executing Base SQL query:"); - expect(result).toContain("HTTP error! status: 401"); + expect(result).toContain("Error 401 executing CDP SQL query:"); + expect(result).toContain("Unauthorized"); }); it("returns a readable error string when fetch throws", async () => { const args = { sqlQuery: "SELECT * FROM throws" }; mockFetch.mockRejectedValue(new Error("boom")); - const result = await provider.executeBaseSqlQuery(args); - expect(result).toBe("Error executing Base SQL query: Error: boom"); + const result = await provider.executeCdpSqlQuery(args); + expect(result).toBe("Error executing CDP SQL query: Error: boom"); }); }); describe("supportsNetwork", () => { - it("returns true for any network", () => { + it("returns true for base network", () => { expect( provider.supportsNetwork({ protocolFamily: "evm", @@ -101,7 +124,7 @@ describe("Base SQL API Action Provider", () => { protocolFamily: "evm", networkId: "ethereum-mainnet", }), - ).toBe(true); + ).toBe(false); }); }); }); diff --git a/typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.ts b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.ts new file mode 100644 index 000000000..eaba00df9 --- /dev/null +++ b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiActionProvider.ts @@ -0,0 +1,100 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { Network } from "../../network"; +import { CreateAction } from "../actionDecorator"; +import { EvmWalletProvider } from "../../wallet-providers"; +import { CdpSqlApiSchema } from "./schemas"; +import { description } from "./cdpSqlApiDescription"; +import { CDP_SQL_API_URL } from "./constants"; + +/** + * Configuration options for the CdpSqlApiActionProvider. + */ +export interface CdpSqlApiActionProviderConfig { + /** + * CDP Client API Key. Request new at https://portal.cdp.coinbase.com/projects/api-keys/client-key/ + */ + cdpApiClientKey?: string; +} + +/** + * CdpSqlApiActionProvider provides actions for cdpSqlApi operations. + * + * @description + * This provider supports SQL querying on the Base Sepolia Base network. + */ +export class CdpSqlApiActionProvider extends ActionProvider { + private readonly cdpApiClientKey: string; + + /** + * Constructor for the CdpSqlApiActionProvider. + * + * @param config - The configuration options for the CdpSqlApiActionProvider. + */ + constructor(config: CdpSqlApiActionProviderConfig = {}) { + super("cdpSqlApi", []); + + const cdpApiClientKey = config.cdpApiClientKey || process.env.CDP_API_CLIENT_KEY; + if (!cdpApiClientKey) { + throw new Error("CDP_API_CLIENT_KEY is not configured."); + } + this.cdpApiClientKey = cdpApiClientKey; + } + + /** + * CDP SQL API action provider + * + * @description + * This action queries the Coinbase SQL API endpoint to efficiently retrieve onchain data on Base or Base Sepolia. + * + * @param args - Arguments defined by CdpSqlApiSchema, i.e. the SQL query to execute + * @returns A promise that resolves to a string describing the query result + */ + @CreateAction({ + name: "execute_cdp_sql_query", + description, + schema: CdpSqlApiSchema, + }) + async executeCdpSqlQuery(args: z.infer): Promise { + try { + const response = await fetch(CDP_SQL_API_URL, { + method: "POST", + headers: { + Authorization: `Bearer ${this.cdpApiClientKey}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ sql: args.sqlQuery }), + }); + + if (!response.ok) { + const errorData = await response.json(); + return `Error ${response.status} executing CDP SQL query: ${errorData.errorMessage || response.statusText}`; + } + + const data = await response.json(); + return JSON.stringify(data.result); + } catch (error) { + return `Error executing CDP SQL query: ${error}`; + } + } + + /** + * Checks if this provider supports the given network. + * + * @param network - The network to check support for + * @returns True if the network is supported + */ + supportsNetwork(network: Network): boolean { + return network.networkId === "base-mainnet" || network.networkId === "base-sepolia"; + } +} + +/** + * Factory function to create a new CdpSqlApiActionProvider instance. + * + * @param config - the config of the cdp sql api action provider, contains the cdp client api key + * @returns A new CdpSqlApiActionProvider instance + */ +export const cdpSqlApiActionProvider = (config?: CdpSqlApiActionProviderConfig) => + new CdpSqlApiActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiDescription.ts similarity index 69% rename from typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts rename to typescript/agentkit/src/action-providers/cdp/cdpSqlApiDescription.ts index cf905551e..c7fbffe7d 100644 --- a/typescript/agentkit/src/action-providers/baseSqlApi/baseSqlApiDescription.ts +++ b/typescript/agentkit/src/action-providers/cdp/cdpSqlApiDescription.ts @@ -275,43 +275,110 @@ const apiSchema = [ }, ], }, - { - tableName: "base.transfers", - fields: [ - { - fieldName: "block_number", - type: "uint64", - description: "Block number containing the transfer", - }, - { fieldName: "block_timestamp", type: "DateTime64", description: "Block timestamp" }, - { fieldName: "transaction_to", type: "String", description: "Transaction recipient address" }, - { fieldName: "transaction_from", type: "String", description: "Transaction sender address" }, - { fieldName: "log_index", type: "uint32", description: "Log index within the transaction" }, - { fieldName: "token_address", type: "String", description: "Token contract address" }, - { - fieldName: "from_address", - type: "String", - description: "Address tokens were transferred from", - }, - { - fieldName: "to_address", - type: "String", - description: "Address tokens were transferred to", - }, - { fieldName: "value", type: "uint256", description: "Amount of tokens transferred" }, - { fieldName: "action", type: "Enum8", description: "Action flag: 1 add, −1 reorg removal" }, - ], - }, + // not public yet + // { + // tableName: "base.transfers", + // fields: [ + // { + // fieldName: "block_number", + // type: "uint64", + // description: "Block number containing the transfer", + // }, + // { fieldName: "block_timestamp", type: "DateTime64", description: "Block timestamp" }, + // { fieldName: "transaction_to", type: "String", description: "Transaction recipient address" }, + // { fieldName: "transaction_from", type: "String", description: "Transaction sender address" }, + // { fieldName: "log_index", type: "uint32", description: "Log index within the transaction" }, + // { fieldName: "token_address", type: "String", description: "Token contract address" }, + // { + // fieldName: "from_address", + // type: "String", + // description: "Address tokens were transferred from", + // }, + // { + // fieldName: "to_address", + // type: "String", + // description: "Address tokens were transferred to", + // }, + // { fieldName: "value", type: "uint256", description: "Amount of tokens transferred" }, + // { fieldName: "action", type: "Enum8", description: "Action flag: 1 add, −1 reorg removal" }, + // ], + // }, ]; const schemaJson = JSON.stringify(apiSchema, null, 2); export const description = ` - This action can call Coinbase's Base SQL API to retrieve onchain data on Base. - Call this action if the user requests historical data on Base. - The SQL API schema is a set of opinionated tables and columns used to organize onchain data for efficient retrieval. - - The supported table names, and fields in each table, for SQL queries are defined in the json string: + This action executes read-only SQL queries against indexed blockchain data using the CDP SQL API. + + **Use Cases:** + - Query transaction history and patterns + - Analyze event logs and smart contract interactions + - Retrieve block information and metadata + - Examine token transfers and DeFi activity + + **IMPORTANT Query Requirements:** + - Must be SELECT statements only (ClickHouse SQL dialect) + - Casts use the :: syntax (not CAST(... AS ...)) + - Maximum query length: 10,000 characters + - Maximum result rows: 10,000 + - Query timeout: 30 seconds + - Maximum JOINs: 5 + - No cartesian products allowed + - No DDL/DML operations (INSERT, UPDATE, DELETE, etc.) + - Keep it simple and break down the task into several queries if appropriate. + **Available Tables:** + - base.events: Decoded event logs with parameters and signatures + - base.transactions: Complete transaction data including gas and signatures + - base.blocks: Block information and metadata + - base.encoded_logs: Raw log data that couldn't be decoded + **Table Schema Details:** ${schemaJson} + + **Example Queries:** + + 1. Get ERC-20 token transfers for USDC: + SELECT + parameters['from']::String AS sender, + parameters['to']::String AS to, + parameters['value']::UInt256 AS amount, + address AS token_address + FROM base.events + WHERE + event_signature = 'Transfer(address,address,uint256)' + AND address = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' + LIMIT 10; + + 2. Get swap events from Uniswap v2-style DEXes: + SELECT parameters ['to']::String AS to, + parameters ['amount0In']::UInt256 AS amount0In, + parameters ['amount0Out']::UInt256 AS amount0Out, + parameters ['amount1In']::UInt256 AS amount1In, + parameters ['amount1Out']::UInt256 AS amount1Out, + parameters ['sender']::String AS sender + FROM base.events + WHERE event_signature = 'Swap(address,uint256,uint256,uint256,uint256,address)' + LIMIT 10; + + 3. Show me 10 rows from the events table: + SELECT * FROM base.events LIMIT 10; + + 4. Aggregate ZORA content rewards by coin and currency for payout recipient 0x0bC5f409e4d9298B93E98920276128b89280d832: + SELECT + parameters ['coin']::String as coin, + parameters ['currency']::String as currency, + sum( + ( + replaceAll( + splitByChar(' ', parameters ['marketRewards']::String) [1], + '{', + '' + ) + )::UInt64 + ) as market_rewards + FROM base.events + WHERE + event_signature = 'CoinMarketRewardsV4(address,address,address,address,address,address,address,(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))' + AND parameters ['payoutRecipient']::String = lower('0x0bC5f409e4d9298B93E98920276128b89280d832') + GROUP BY coin, currency; `; diff --git a/typescript/agentkit/src/action-providers/cdp/constants.ts b/typescript/agentkit/src/action-providers/cdp/constants.ts new file mode 100644 index 000000000..5f1f7ebba --- /dev/null +++ b/typescript/agentkit/src/action-providers/cdp/constants.ts @@ -0,0 +1 @@ +export const CDP_SQL_API_URL = "https://api.cdp.coinbase.com/platform/v2/data/query/run"; diff --git a/typescript/agentkit/src/action-providers/cdp/index.ts b/typescript/agentkit/src/action-providers/cdp/index.ts index aee526eea..d5ab18b15 100644 --- a/typescript/agentkit/src/action-providers/cdp/index.ts +++ b/typescript/agentkit/src/action-providers/cdp/index.ts @@ -3,3 +3,4 @@ export * from "./cdpApiActionProvider"; export * from "./cdpSmartWalletActionProvider"; export * from "./cdpEvmWalletActionProvider"; export * from "./spendPermissionUtils"; +export * from "./cdpSqlApiActionProvider"; diff --git a/typescript/agentkit/src/action-providers/cdp/schemas.ts b/typescript/agentkit/src/action-providers/cdp/schemas.ts index 9df2e65cc..1f0430b80 100644 --- a/typescript/agentkit/src/action-providers/cdp/schemas.ts +++ b/typescript/agentkit/src/action-providers/cdp/schemas.ts @@ -70,3 +70,17 @@ export const UseSpendPermissionSchema = z }) .strip() .describe("Instructions for using a spend permission"); + +/** + * Input schema for querying the CDP SQL API + */ +export const CdpSqlApiSchema = z.object({ + sqlQuery: z + .string() + .min(1, "SQL query cannot be empty") + .max(10000, "Query exceeds maximum length of 10,000 characters") + .describe( + `The SQL query to execute using ClickHouse syntax. Must be a read-only SELECT statement. ` + + `Limitations: max 10,000 characters, max 5 JOINs, no cartesian products, 30s timeout. API supports a max of 10,000 result rows but limit it to 10 unless otherwise specified.`, + ), +}); diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index f3559413a..36ebec21c 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -36,4 +36,3 @@ export * from "./zerodev"; export * from "./zeroX"; export * from "./zora"; export * from "./clanker"; -export * from "./baseSqlApi";