Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/lazy-hornets-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": patch
---

Added a new action provider to support Base SQL API queries
9 changes: 9 additions & 0 deletions typescript/agentkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,15 @@ const agent = createReactAgent({
</table>
</details>
<details>
<summary><strong>Base SQL API</strong></summary>
<table width="100%">
<tr>
<td width="200"><code>execute_base_sql_query</code></td>
<td width="768">Queries the Base blockchain using SQL based on the defined tables and fields.</td>
</tr>
</table>
</details>
<details>
<summary><strong>Clanker</strong></summary>
<table width="100%">
<tr>
Expand Down
8 changes: 7 additions & 1 deletion typescript/agentkit/src/action-providers/cdp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseSqlApiDescription -> cdpSqlApiDescription

├── schemas.ts # Action schemas for CDP operations
├── index.ts # Main exports
├── constants.ts # Constant variables
└── README.md # This file
```

Expand All @@ -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

Expand Down Expand Up @@ -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/).
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { cdpSqlApiActionProvider } from "./cdpSqlApiActionProvider";
import { CdpSqlApiSchema } from "./schemas";
import { CDP_SQL_API_URL } from "./constants";

describe("CDP SQL API Action Provider", () => {
let originalFetch: typeof fetch | undefined;
let mockFetch: jest.MockedFunction<typeof fetch>;

const mockApiKey = "test-token";

beforeEach(() => {
process.env.CDP_API_CLIENT_KEY = mockApiKey;

originalFetch = globalThis.fetch;
mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
globalThis.fetch = mockFetch;
});

afterEach(() => {
mockFetch.mockReset();
if (originalFetch) {
globalThis.fetch = originalFetch;
}
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 = CdpSqlApiSchema.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 = CdpSqlApiSchema.safeParse(invalidInput);
expect(parsed.success).toBe(false);
});
});

describe("executeCdpSqlQuery", () => {
it("POSTs to the CDP SQL API with headers/body and returns the text result", async () => {
const args = { sqlQuery: "SELECT 1" };
const resultPayload = { columns: ["one"], rows: [[1]] };

mockFetch.mockResolvedValue(
new Response(JSON.stringify({ result: resultPayload }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
);

const result = await provider.executeCdpSqlQuery(args);

expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
CDP_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(JSON.stringify(resultPayload));
});

it("returns a readable error string when response.ok is false", async () => {
const args = { sqlQuery: "SELECT * FROM nope" };
const errorBody = { errorMessage: "Unauthorized" };

mockFetch.mockResolvedValue(
new Response(JSON.stringify(errorBody), {
status: 401,
headers: { "Content-Type": "application/json" },
}),
);

const result = await provider.executeCdpSqlQuery(args);

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.executeCdpSqlQuery(args);
expect(result).toBe("Error executing CDP SQL query: Error: boom");
});
});

describe("supportsNetwork", () => {
it("returns true for base network", () => {
expect(
provider.supportsNetwork({
protocolFamily: "evm",
networkId: "base-mainnet",
}),
).toBe(true);

expect(
provider.supportsNetwork({
protocolFamily: "evm",
networkId: "ethereum-mainnet",
}),
).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<EvmWalletProvider> {
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<typeof CdpSqlApiSchema>): Promise<string> {
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);
Loading
Loading