diff --git a/cdp-agentkit-core/typescript/package.json b/cdp-agentkit-core/typescript/package.json index 37d9eb8e..39f33c4f 100644 --- a/cdp-agentkit-core/typescript/package.json +++ b/cdp-agentkit-core/typescript/package.json @@ -7,7 +7,9 @@ "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "tsc", "lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts", @@ -38,6 +40,7 @@ ], "dependencies": { "@coinbase/coinbase-sdk": "^0.15.0", + "@alloralabs/allora-sdk": "^0.1.0", "twitter-api-v2": "^1.18.2", "viem": "^2.21.51", "zod": "^3.23.8" diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md b/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md new file mode 100644 index 00000000..7a0cdbb2 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/README.md @@ -0,0 +1,11 @@ +## Allora + +[Allora Network](https://allora.network/) is an AI-powered inference platform that delivers real-time, self-improving forecasts and insights for various use cases. By aggregating and analyzing data from diverse sources—such as blockchain networks and off-chain APIs—Allora seamlessly provides low-latency, high-performance analytics without requiring complex infrastructure. The platform’s intuitive approach allows developers to focus on building intelligence-driven solutions, while Allora takes care of the heavy lifting behind the scenes. + +### Actions + +- get_all_topics: Lists all available topics from Allora Network. +- get_price_inference: Returns the future price inference for a given crypto asset from Allora Network. It takes the crypto asset and timeframe as inputs. e.g.: "Get the inference for BTC in 5 min". + + + diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts new file mode 100644 index 00000000..2251e26f --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/allora_action.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { AlloraAPIClient } from "@alloralabs/allora-sdk"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AlloraActionSchemaAny = z.ZodObject; + +/** + * Represents the base structure for Allora Actions. + */ +export interface AlloraAction { + /** + * The name of the action. + */ + name: string; + + /** + * A description of what the action does + */ + description: string; + + /** + * Schema for validating action arguments + */ + argsSchema: TActionSchema; + + /** + * The function to execute for this action + */ + func: + | ((client: AlloraAPIClient, args: z.infer) => Promise) + | ((args: z.infer) => Promise); +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts new file mode 100644 index 00000000..a9d3bfe9 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_all_topics.ts @@ -0,0 +1,64 @@ +import { AlloraAction } from "./allora_action"; +import { AlloraAPIClient } from "@alloralabs/allora-sdk"; +import { z } from "zod"; + +const GET_ALL_TOPICS_PROMPT = ` +This tool will get all available topics from Allora Network. + +A successful response will return a message with a list of available topics from Allora Network in JSON format: + [ + { + "topic_id": 1, + "topic_name": ""Bitcoin 8h", + "description": "Bitcoin price prediction for the next 8 hours", + "epoch_length": 100, + "ground_truth_lag": 10, + "loss_method": "method1", + "worker_submission_window": 50, + "worker_count": 5, + "reputer_count": 3, + "total_staked_allo": 1000, + "total_emissions_allo": 500, + "is_active": true, + "updated_at": "2023-01-01T00:00:00Z" + } + ] +`; + +/** + * Input schema for get all topics action. + */ +export const GetAllTopicsInput = z + .object({}) + .strip() + .describe("Instructions for getting all topics"); + +/** + * Gets all available topics from Allora Network. + * + * @param client - The Allora API client. + * @param args - The input arguments for the action. + * @returns A message containing the topics. + */ +export async function getAllTopics( + client: AlloraAPIClient, + _: z.infer, +): Promise { + try { + const topics = await client.getAllTopics(); + const topicsJson = JSON.stringify(topics); + return `The available topics at Allora Network are:\n ${topicsJson}`; + } catch (error) { + return `Error getting all topics: ${error}`; + } +} + +/** + * Get all topics action. + */ +export class GetAllTopicsAction implements AlloraAction { + public name = "get_all_topics"; + public description = GET_ALL_TOPICS_PROMPT; + public argsSchema = GetAllTopicsInput; + public func = getAllTopics; +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts new file mode 100644 index 00000000..7c60a3da --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/get_price_inference.ts @@ -0,0 +1,63 @@ +import { AlloraAction } from "./allora_action"; +import { + AlloraAPIClient, + PriceInferenceTimeframe, + PriceInferenceToken, +} from "@alloralabs/allora-sdk"; +import { z } from "zod"; + +const GET_PRICE_INFERENCE_PROMPT = ` +This tool will get the future price inference for a given crypto asset from Allora Network. +It takes the crypto asset and timeframe as inputs. +`; + +/** + * Input schema for get price inference action. + */ +export const GetPriceInferenceInput = z + .object({ + asset: z.string().describe("The crypto asset to get the price inference for, e.g. 'BTC'"), + timeframe: z + .string() + .describe("The timeframe to get the price inference for, e.g. '5m' or '8h'"), + }) + .strip() + .describe("Instructions for getting the price inference"); + +/** + * Gets the future price inference for a given crypto asset from Allora Network. + * + * @param client - The Allora API client. + * @param args - A zod object containing the asset and timeframe. + * @returns A message containing the price inference. + */ +export async function getPriceInference( + client: AlloraAPIClient, + args: z.infer, +): Promise { + const getPriceInferenceArgs = { + asset: args.asset, + timeframe: args.timeframe, + }; + + try { + const asset = getPriceInferenceArgs.asset as PriceInferenceToken; + const timeframe = getPriceInferenceArgs.timeframe as PriceInferenceTimeframe; + + const priceInference = await client.getPriceInference(asset, timeframe); + + return `The future price inference for ${asset} in ${timeframe} is ${priceInference.inference_data.network_inference_normalized}`; + } catch (error) { + return `Error getting price inference: ${error}`; + } +} + +/** + * Get price inference action. + */ +export class GetPriceInferenceAction implements AlloraAction { + public name = "get_price_inference"; + public description = GET_PRICE_INFERENCE_PROMPT; + public argsSchema = GetPriceInferenceInput; + public func = getPriceInference; +} diff --git a/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts b/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts new file mode 100644 index 00000000..c343115d --- /dev/null +++ b/cdp-agentkit-core/typescript/src/actions/cdp/allora/index.ts @@ -0,0 +1,25 @@ +/** + * This module exports various Allora action instances and their associated types. + */ + +import { AlloraAction, AlloraActionSchemaAny } from "./allora_action"; +import { GetPriceInferenceAction } from "./get_price_inference"; +import { GetAllTopicsAction } from "./get_all_topics"; +/** + * Retrieve an array of Allora action instances. + * + * @returns {AlloraAction[]} An array of Allora action instances. + */ +export function getAllAlloraActions(): AlloraAction[] { + return [new GetPriceInferenceAction(), new GetAllTopicsAction()]; +} + +/** + * All available Allora actions. + */ +export const ALLORA_ACTIONS = getAllAlloraActions(); + +/** + * All Allora action types. + */ +export { AlloraAction, AlloraActionSchemaAny, GetPriceInferenceAction, GetAllTopicsAction }; diff --git a/cdp-agentkit-core/typescript/src/allora_agentkit.ts b/cdp-agentkit-core/typescript/src/allora_agentkit.ts new file mode 100644 index 00000000..799fbfff --- /dev/null +++ b/cdp-agentkit-core/typescript/src/allora_agentkit.ts @@ -0,0 +1,44 @@ +import { AlloraAPIClient, ChainSlug } from "@alloralabs/allora-sdk"; +import { AlloraAction, AlloraActionSchemaAny } from "./actions/cdp/allora"; + +interface AlloraAgentkitOptions { + apiKey?: string; + baseAPIUrl?: string; + chainSlug?: string; +} + +export class AlloraAgentkit { + private client: AlloraAPIClient; + + constructor(config: AlloraAgentkitOptions = {}) { + const apiKey = config.apiKey || process.env.ALLORA_API_KEY || "UP-4151d0cc489a44a7aa5cd7ef"; + const baseAPIUrl = config.baseAPIUrl || process.env.ALLORA_BASE_API_URL; + const chainSlug = config.chainSlug || process.env.ALLORA_CHAIN_SLUG || ChainSlug.TESTNET; + + if (!Object.values(ChainSlug).includes(chainSlug as ChainSlug)) { + throw new Error( + `Invalid chainSlug: ${chainSlug}. Valid options are: ${Object.values(ChainSlug).join(", ")}`, + ); + } + + this.client = new AlloraAPIClient({ + apiKey, + baseAPIUrl, + chainSlug: chainSlug as ChainSlug, + }); + } + + /** + * Executes a Allora action. + * + * @param action - The Allora action to execute. + * @param args - The arguments for the action. + * @returns The result of the execution. + */ + async run( + action: AlloraAction, + args: TActionSchema, + ): Promise { + return await action.func(this.client!, args); + } +} diff --git a/cdp-agentkit-core/typescript/src/index.ts b/cdp-agentkit-core/typescript/src/index.ts index fd696537..316108e1 100644 --- a/cdp-agentkit-core/typescript/src/index.ts +++ b/cdp-agentkit-core/typescript/src/index.ts @@ -21,3 +21,6 @@ export { FarcasterAgentkit } from "./farcaster_agentkit"; // Export Twitter AgentKit export { TwitterAgentkit } from "./twitter_agentkit"; + +// Export Allora AgentKit +export { AlloraAgentkit } from "./allora_agentkit"; diff --git a/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts new file mode 100644 index 00000000..f700771a --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_agentkit_test.ts @@ -0,0 +1,64 @@ +import { AlloraAPIClient, ChainSlug } from "@alloralabs/allora-sdk"; +import { AlloraAgentkit } from "../allora_agentkit"; + +jest.mock("@alloralabs/allora-sdk"); + +describe("AlloraAgentkit", () => { + describe("initialization", () => { + beforeEach(() => { + process.env.ALLORA_API_KEY = "test-api-key"; + process.env.ALLORA_BASE_API_URL = "https://test.api.url"; + process.env.ALLORA_CHAIN_SLUG = ChainSlug.TESTNET; + }); + + afterEach(() => { + jest.resetAllMocks(); + process.env.ALLORA_API_KEY = ""; + process.env.ALLORA_BASE_API_URL = ""; + process.env.ALLORA_CHAIN_SLUG = ""; + }); + + it("should successfully init with env variables", () => { + const agentkit = new AlloraAgentkit(); + expect(agentkit).toBeDefined(); + expect(AlloraAPIClient).toHaveBeenCalledWith({ + apiKey: "test-api-key", + baseAPIUrl: "https://test.api.url", + chainSlug: ChainSlug.TESTNET, + }); + }); + + it("should successfully init with options overriding env", () => { + const options = { + apiKey: "custom-api-key", + baseAPIUrl: "https://custom.api.url", + chainSlug: ChainSlug.MAINNET, + }; + + const agentkit = new AlloraAgentkit(options); + expect(agentkit).toBeDefined(); + expect(AlloraAPIClient).toHaveBeenCalledWith(options); + }); + + it("should use default values when no options or env provided", () => { + process.env.ALLORA_API_KEY = ""; + process.env.ALLORA_BASE_API_URL = ""; + process.env.ALLORA_CHAIN_SLUG = ""; + + const agentkit = new AlloraAgentkit(); + expect(agentkit).toBeDefined(); + expect(AlloraAPIClient).toHaveBeenCalledWith({ + apiKey: "UP-4151d0cc489a44a7aa5cd7ef", + baseAPIUrl: "", + chainSlug: ChainSlug.TESTNET, + }); + }); + + it("should throw error for invalid chain slug", () => { + const invalidChainSlug = "INVALID_CHAIN" as unknown as ChainSlug; + expect(() => { + new AlloraAgentkit({ chainSlug: invalidChainSlug }); + }).toThrow(/Invalid chainSlug/); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts new file mode 100644 index 00000000..b6b102af --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_get_all_topics_test.ts @@ -0,0 +1,58 @@ +import { AlloraAPIClient } from "@alloralabs/allora-sdk"; +import { GetAllTopicsAction, getAllTopics } from "../actions/cdp/allora/get_all_topics"; + +describe("GetAllTopicsAction", () => { + const mockTopics = [ + { + topic_id: 1, + topic_name: "Bitcoin 8h", + description: "Bitcoin price prediction for the next 8 hours", + epoch_length: 100, + ground_truth_lag: 10, + loss_method: "method1", + worker_submission_window: 50, + worker_count: 5, + reputer_count: 3, + total_staked_allo: 1000, + total_emissions_allo: 500, + is_active: true, + updated_at: "2023-01-01T00:00:00Z", + }, + ]; + + it("should have correct action properties", () => { + const action = new GetAllTopicsAction(); + expect(action.name).toBe("get_all_topics"); + expect(action.description).toContain( + "This tool will get all available topics from Allora Network", + ); + expect(action.argsSchema).toBeDefined(); + }); + + describe("getAllTopics", () => { + it("should return topics successfully", async () => { + const mockClient = { + getAllTopics: jest.fn().mockResolvedValue(mockTopics), + } as unknown as jest.Mocked; + + const result = await getAllTopics(mockClient, {}); + + expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1); + expect(result).toContain("The available topics at Allora Network are:"); + expect(result).toContain(JSON.stringify(mockTopics)); + }); + + it("should handle errors gracefully", async () => { + const mockError = new Error("API Error"); + const mockClient = { + getAllTopics: jest.fn().mockRejectedValue(mockError), + } as unknown as jest.Mocked; + + const result = await getAllTopics(mockClient, {}); + + expect(mockClient.getAllTopics).toHaveBeenCalledTimes(1); + expect(result).toContain("Error getting all topics:"); + expect(result).toContain(mockError.toString()); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts b/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts new file mode 100644 index 00000000..6eefdd07 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/tests/allora_get_price_inference_test.ts @@ -0,0 +1,79 @@ +import { + AlloraAPIClient, + PriceInferenceTimeframe, + PriceInferenceToken, + AlloraInference, +} from "@alloralabs/allora-sdk"; +import { + getPriceInference, + GetPriceInferenceInput, +} from "../actions/cdp/allora/get_price_inference"; + +describe("Get Price Inference Input", () => { + it("should successfully parse valid input", () => { + const validInput = { + asset: "BTC", + timeframe: "5m", + }; + + const result = GetPriceInferenceInput.safeParse(validInput); + + expect(result.success).toBe(true); + expect(result.data).toEqual(validInput); + }); + + it("should fail parsing empty input", () => { + const emptyInput = {}; + const result = GetPriceInferenceInput.safeParse(emptyInput); + + expect(result.success).toBe(false); + }); +}); + +describe("Get Price Inference Action", () => { + let mockAlloraClient: jest.Mocked; + + beforeEach(() => { + mockAlloraClient = { + getPriceInference: jest.fn(), + } as unknown as jest.Mocked; + }); + + it("should successfully get price inference", async () => { + const args = { + asset: "BTC", + timeframe: "5m", + }; + const mockInference = { + signature: "mockSignature", + inference_data: { network_inference_normalized: "45000.00" }, + }; + + mockAlloraClient.getPriceInference.mockResolvedValue(mockInference as AlloraInference); + + const response = await getPriceInference(mockAlloraClient, args); + + expect(mockAlloraClient.getPriceInference).toHaveBeenCalledWith( + args.asset as PriceInferenceToken, + args.timeframe as PriceInferenceTimeframe, + ); + expect(response).toBe( + `The future price inference for BTC in 5m is ${mockInference.inference_data.network_inference_normalized}`, + ); + }); + + it("should handle errors gracefully", async () => { + const args = { + asset: "BTC", + timeframe: "5m", + }; + + const error = new Error("Failed to fetch price inference"); + mockAlloraClient.getPriceInference.mockRejectedValue(error); + + const response = await getPriceInference(mockAlloraClient, args); + + expect(mockAlloraClient.getPriceInference).toHaveBeenCalled(); + expect(response).toBe(`Error getting price inference: ${error}`); + }); +}); diff --git a/cdp-langchain/typescript/src/toolkits/cdp_toolkit.ts b/cdp-langchain/typescript/src/toolkits/cdp_toolkit.ts index 10a7015b..201ece60 100644 --- a/cdp-langchain/typescript/src/toolkits/cdp_toolkit.ts +++ b/cdp-langchain/typescript/src/toolkits/cdp_toolkit.ts @@ -43,6 +43,8 @@ import { CdpTool } from "../tools/cdp_tool"; * // - wow_buy_token * // - wow_sell_token * // - wrap_eth + * // - get_price_inference + * // - get_all_topics * ``` */ export class CdpToolkit extends Toolkit { diff --git a/package-lock.json b/package-lock.json index 9913a968..9d10760b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "version": "0.0.14", "license": "Apache-2.0", "dependencies": { + "@alloralabs/allora-sdk": "^0.1.0", "@coinbase/coinbase-sdk": "^0.15.0", "twitter-api-v2": "^1.18.2", "viem": "^2.21.51", @@ -197,6 +198,31 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, + "node_modules/@alloralabs/allora-sdk": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@alloralabs/allora-sdk/-/allora-sdk-0.1.0.tgz", + "integrity": "sha512-jVCIx+PXOrklDf4TU27DCuf0Nri2+s+hhDGMP/s8CHUY6eSaL8G3S0E1L1vP+sF6gIjzCdV7P68QtRB0ym5vNQ==", + "dependencies": { + "@types/node": "^22.10.5", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alloralabs/allora-sdk/node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@alloralabs/allora-sdk/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -7862,7 +7888,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8378,28 +8403,6 @@ "peerDependencies": { "@coinbase/coinbase-sdk": "^0.15.0" } - }, - "twitter-langchain/typescript/node_modules/@coinbase/cdp-agentkit-core/node_modules/@coinbase/coinbase-sdk": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@coinbase/coinbase-sdk/-/coinbase-sdk-0.14.1.tgz", - "integrity": "sha512-3goViecQEPvUBndle4nTF8Sv4lIEOe6lad05yG3ZRIhj0tgfn2hI9i6V+eZ3np+sAQQO1J+6uL2j9PH/AMWtiw==", - "extraneous": true, - "license": "ISC", - "dependencies": { - "@scure/bip32": "^1.4.0", - "abitype": "^1.0.6", - "axios": "^1.6.8", - "axios-mock-adapter": "^1.22.0", - "axios-retry": "^4.4.1", - "bip32": "^4.0.0", - "bip39": "^3.1.0", - "decimal.js": "^10.4.3", - "dotenv": "^16.4.5", - "ethers": "^6.12.1", - "node-jose": "^2.2.0", - "secp256k1": "^5.0.0", - "viem": "^2.21.26" - } } } }