diff --git a/src/agent/index.ts b/src/agent/index.ts index 8f0ef101..962c6b25 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -66,6 +66,9 @@ import { parseTransaction, getAssetsByOwner, sendTransactionWithPriorityFee, + create_HeliusWebhook, + getHeliusWebhook, + deleteHeliusWebhook, } from "../tools"; import { CollectionDeployment, @@ -79,6 +82,8 @@ import { FlashTradeParams, FlashCloseTradeParams, PriorityFeeTransaction, + HeliusWebhookIdResponse, + HeliusWebhookResponse, } from "../types"; import { createCollection, @@ -620,4 +625,16 @@ export class SolanaAgentKit { ): Promise { return sendTransactionWithPriorityFee(this, priorityLevel, amount, to); } + async CreateWebhook( + accountAddresses: string[], + webhookURL: string, + ): Promise { + return create_HeliusWebhook(this, accountAddresses, webhookURL); + } + async getWebhook(id: string): Promise { + return getHeliusWebhook(this, id); + } + async deleteWebhook(webhookID: string): Promise { + return deleteHeliusWebhook(this, webhookID); + } } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index bf2945d9..f253aa0a 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -2547,6 +2547,150 @@ export class SolanaSendTransactionWithPriorityFee extends Tool { } } +export class SolanaHeliusWebhookTool extends Tool { + name = "create_helius_webhook"; + description = `Creates a Helius Webhook that listens to specified account addresses. + + Inputs (input is a JSON string): + accountAddresses: string[] | string, + e.g. ["BVdNLvyG2DNiWAXBE9qAmc4MTQXymd5Bzfo9xrQSUzVP","Eo2ciguhMLmcTWXELuEQPdu7DWZt67LHXb2rdHZUbot7"] + or "BVdNLvyG2DNiWAXBE9qAmc4MTQXymd5Bzfo9xrQSUzVP,Eo2ciguhMLmcTWXELuEQPdu7DWZt67LHXb2rdHZUbot7" + webhookURL: string, e.g. "https://TestServer.test.repl.co/webhooks"`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + let accountAddresses: string[] = []; + + if (!parsedInput.accountAddresses) { + throw new Error('Missing "accountAddresses" property in input JSON.'); + } + if (Array.isArray(parsedInput.accountAddresses)) { + accountAddresses = parsedInput.accountAddresses.map((addr: string) => + addr.trim(), + ); + } else if (typeof parsedInput.accountAddresses === "string") { + accountAddresses = parsedInput.accountAddresses + .split(",") + .map((addr: string) => addr.trim()); + } else { + throw new Error( + 'Invalid type for "accountAddresses". Expected array or comma-separated string.', + ); + } + + const webhookURL = parsedInput.webhookURL; + if (!webhookURL) { + throw new Error( + 'Invalid input. Expected a "webhookURL" property in the JSON.', + ); + } + const result = await this.solanaKit.CreateWebhook( + accountAddresses, + webhookURL, + ); + + // Return success in JSON + return JSON.stringify({ + status: "success", + message: "Helius Webhook created successfully", + webhookURL: result.webhookURL, + webhookID: result.webhookID, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + +export class SolanaGetHeliusWebhookTool extends Tool { + name = "get_helius_webhook"; + description = `Retrieves a Helius Webhook by its ID. + +Inputs (input is a JSON string): + webhookID: string, e.g. "1ed4244d-a591-4854-ac31-cc28d40b8255"`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + const webhookID = parsedInput.webhookID; + if (!webhookID || typeof webhookID !== "string") { + throw new Error( + 'Invalid input. Expected a "webhookID" property in the JSON.', + ); + } + + const result = await this.solanaKit.getWebhook(webhookID); + return JSON.stringify({ + status: "success", + message: "Helius Webhook retrieved successfully", + wallet: result.wallet, + webhookURL: result.webhookURL, + transactionTypes: result.transactionTypes, + accountAddresses: result.accountAddresses, + webhookType: result.webhookType, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + +export class SolanaDeleteHeliusWebhookTool extends Tool { + name = "delete_helius_webhook"; + description = `Deletes a Helius Webhook by its ID. + +Inputs (input is a JSON string): + webhookID: string, e.g. "1ed4244d-a591-4854-ac31-cc28d40b8255"`; + + constructor(private solanaKit: SolanaAgentKit) { + super(); + } + + protected async _call(input: string): Promise { + try { + const parsedInput = JSON.parse(input); + + const webhookID = parsedInput.webhookID; + if (!webhookID || typeof webhookID !== "string") { + throw new Error( + 'Invalid input. Expected a "webhookID" property in the JSON.', + ); + } + const result = await this.solanaKit.deleteWebhook(webhookID); + + return JSON.stringify({ + status: "success", + message: "Helius Webhook deleted successfully", + data: result, + }); + } catch (error: any) { + return JSON.stringify({ + status: "error", + message: error.message, + code: error.code || "UNKNOWN_ERROR", + }); + } + } +} + export function createSolanaTools(solanaKit: SolanaAgentKit) { return [ new SolanaBalanceTool(solanaKit), @@ -2610,5 +2754,8 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) { new SolanaGetAllAssetsByOwner(solanaKit), new Solana3LandCreateSingle(solanaKit), new SolanaSendTransactionWithPriorityFee(solanaKit), + new SolanaHeliusWebhookTool(solanaKit), + new SolanaGetHeliusWebhookTool(solanaKit), + new SolanaDeleteHeliusWebhookTool(solanaKit), ]; } diff --git a/src/tools/helius_webhook.ts b/src/tools/helius_webhook.ts new file mode 100644 index 00000000..98b083fc --- /dev/null +++ b/src/tools/helius_webhook.ts @@ -0,0 +1,132 @@ +import { SolanaAgentKit } from "../index"; +import { HeliusWebhookResponse, HeliusWebhookIdResponse } from "../index"; + +export async function create_HeliusWebhook( + agent: SolanaAgentKit, + accountAddresses: string[], + webhookURL: string, +): Promise { + try { + const response = await fetch( + `https://api.helius.xyz/v0/webhooks?api-key=${agent.config.HELIUS_API_KEY}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + webhookURL, + transactionTypes: ["Any"], + accountAddresses, + webhookType: "enhanced", + txnStatus: "all", + }), + }, + ); + + const data = await response.json(); + return { + webhookURL: data.webhookURL, + webhookID: data.webhookID, + }; + } catch (error: any) { + throw new Error(`Failed to create Webhook: ${error.message}`); + } +} + +/** + * Retrieves a Helius Webhook by ID, returning only the specified fields. + * + * @param agent - An instance of SolanaAgentKit (with .config.HELIUS_API_KEY) + * @param webhookID - The unique ID of the webhook to retrieve + * + * @returns A HeliusWebhook object containing { wallet, webhookURL, transactionTypes, accountAddresses, webhookType } + */ +export async function getHeliusWebhook( + agent: SolanaAgentKit, + webhookID: string, +): Promise { + try { + const apiKey = agent.config.HELIUS_API_KEY; + if (!apiKey) { + throw new Error("HELIUS_API_KEY is missing in agent.config"); + } + + const response = await fetch( + `https://api.helius.xyz/v0/webhooks/${webhookID}?api-key=${apiKey}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch webhook with ID ${webhookID}. ` + + `Status Code: ${response.status}`, + ); + } + + const data = await response.json(); + + return { + wallet: data.wallet, + webhookURL: data.webhookURL, + transactionTypes: data.transactionTypes, + accountAddresses: data.accountAddresses, + webhookType: data.webhookType, + }; + } catch (error: any) { + throw new Error(`Failed to get webhook by ID: ${error.message}`); + } +} + +/** + * Deletes a Helius Webhook by its ID. + * + * @param agent - An instance of SolanaAgentKit (with .config.HELIUS_API_KEY) + * @param webhookID - The unique ID of the webhook to delete + * + * @returns The response body from the Helius API (which may contain status or other info) + */ +export async function deleteHeliusWebhook( + agent: SolanaAgentKit, + webhookID: string, +): Promise { + try { + const apiKey = agent.config.HELIUS_API_KEY; + if (!apiKey) { + throw new Error("Missing Helius API key in agent.config.HELIUS_API_KEY"); + } + + const url = `https://api.helius.xyz/v0/webhooks/${webhookID}?api-key=${apiKey}`; + const response = await fetch(url, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to delete webhook: ${response.status} ${response.statusText}`, + ); + } + if (response.status === 204) { + return { message: "Webhook deleted successfully (no content returned)" }; + } + const contentLength = response.headers.get("Content-Length"); + if (contentLength === "0" || !contentLength) { + return { message: "Webhook deleted successfully (empty body)" }; + } + + // Otherwise, parse as JSON + const data = await response.json(); + return data; + } catch (error: any) { + console.error("Error deleting Helius Webhook:", error.message); + throw new Error(`Failed to delete Helius Webhook: ${error.message}`); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index c8584e33..22893486 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -53,3 +53,4 @@ export * from "./get_assets_by_owner"; export * from "./create_3land_collectible"; export * from "./send_transaction_with_priority"; +export * from "./helius_webhook"; diff --git a/src/types/index.ts b/src/types/index.ts index 7ded4624..0fedb5e5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -243,3 +243,15 @@ export interface PriorityFeeTransaction { transactionId: string; fee: number; } + +export interface HeliusWebhookResponse { + webhookURL: string; + webhookID: string; +} +export interface HeliusWebhookIdResponse { + wallet: string; + webhookURL: string; + transactionTypes: string[]; + accountAddresses: string[]; + webhookType: string; +}