From fb9c47769a3474d950caeb948d0fa7d413236246 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 11:02:57 +0700 Subject: [PATCH 01/15] feat: add birdeye provider as base api for further integration --- packages/plugin-birdeye/.npmignore | 6 + packages/plugin-birdeye/eslint.config.mjs | 3 + packages/plugin-birdeye/package.json | 33 +++ packages/plugin-birdeye/src/environment.ts | 32 +++ packages/plugin-birdeye/src/index.ts | 13 ++ .../plugin-birdeye/src/providers/birdeye.ts | 196 ++++++++++++++++++ packages/plugin-birdeye/tsconfig.json | 10 + packages/plugin-birdeye/tsup.config.ts | 29 +++ 8 files changed, 322 insertions(+) create mode 100644 packages/plugin-birdeye/.npmignore create mode 100644 packages/plugin-birdeye/eslint.config.mjs create mode 100644 packages/plugin-birdeye/package.json create mode 100644 packages/plugin-birdeye/src/environment.ts create mode 100644 packages/plugin-birdeye/src/index.ts create mode 100644 packages/plugin-birdeye/src/providers/birdeye.ts create mode 100644 packages/plugin-birdeye/tsconfig.json create mode 100644 packages/plugin-birdeye/tsup.config.ts diff --git a/packages/plugin-birdeye/.npmignore b/packages/plugin-birdeye/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-birdeye/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-birdeye/eslint.config.mjs b/packages/plugin-birdeye/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-birdeye/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json new file mode 100644 index 0000000000..f89995ae7c --- /dev/null +++ b/packages/plugin-birdeye/package.json @@ -0,0 +1,33 @@ +{ + "name": "@ai16z/plugin-birdeye", + "version": "0.1.6-alpha.4", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@ai16z/plugin-trustdb": "workspace:*", + "@ai16z/plugin-tee": "workspace:*", + "@coral-xyz/anchor": "0.30.1", + "@solana/spl-token": "0.4.9", + "@solana/web3.js": "1.95.8", + "bignumber": "1.1.0", + "bignumber.js": "9.1.2", + "bs58": "6.0.0", + "fomo-sdk-solana": "1.3.2", + "node-cache": "5.1.2", + "pumpdotfun-sdk": "1.3.2", + "tsup": "8.3.5", + "vitest": "2.1.4" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-birdeye/src/environment.ts b/packages/plugin-birdeye/src/environment.ts new file mode 100644 index 0000000000..ebe6cbaf36 --- /dev/null +++ b/packages/plugin-birdeye/src/environment.ts @@ -0,0 +1,32 @@ +import { IAgentRuntime } from "@ai16z/eliza"; +import { z } from "zod"; + +export const birdeyeEnvSchema = z.object({ + BIRDEYE_API_KEY: z.string().min(1, "Birdeye API key is required"), +}); + +export type BirdeyeConfig = z.infer; + +export async function validateBirdeyeConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + BIRDEYE_API_KEY: + runtime.getSetting("BIRDEYE_API_KEY") || + process.env.BIRDEYE_API_KEY, + }; + + return birdeyeEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Birdeye configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts new file mode 100644 index 0000000000..32522dfb08 --- /dev/null +++ b/packages/plugin-birdeye/src/index.ts @@ -0,0 +1,13 @@ +import { Plugin } from "@ai16z/eliza"; + +import {birdeyeProvider, BirdeyeProvider} from './providers/birdeye' + +export { BirdeyeProvider }; + +export const birdeyePlugin: Plugin = { + name: "birdeye", + description: "Birdeye Plugin for Eliza", + providers: [birdeyeProvider], +}; + +export default birdeyePlugin; diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts new file mode 100644 index 0000000000..d181875567 --- /dev/null +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -0,0 +1,196 @@ + +import NodeCache from "node-cache"; +import * as path from "path"; +import { ICacheManager, settings } from "@ai16z/eliza"; +import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza"; + +const DEFAULT_MAX_RETRIES = 3 + +const DEFAULT_SUPPORTED_SYMBOLS = { + SOL: "So11111111111111111111111111111111111111112", + BTC: "qfnqNqs3nCAHjnyCgLRDbBtq4p2MtHZxw8YjSyYhPoL", + ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + Example: "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh", +} + +const API_BASE_URL = 'https://public-api.birdeye.so' +const ENDPOINT_MAP = { + price: '/defi/price?address=', + security: '/defi/token_security?address=', + volume: '/defi/v3/token/trade-data/single?address=', + portfolio: '/v1/wallet/token_list?wallet=' +} +const RETRY_DELAY_MS = 2_000 + +const waitFor = (ms:number) => new Promise((resolve)=> setTimeout(resolve, ms)) + + +class BaseCachedProvider { + private cache: NodeCache; + + constructor( + private cacheManager: ICacheManager, + private cacheKey, + ttl?: number + ) { + this.cache = new NodeCache({ stdTTL: ttl || 300 }); + } + + private readFsCache(key: string): Promise { + return this.cacheManager.get( + path.join(this.cacheKey, key) + ); + } + + private writeFsCache(key: string, data: T): Promise { + return this.cacheManager.set(path.join(this.cacheKey, key), data, { + expires: Date.now() + 5 * 60 * 1000, + }); + } + + public async readFromCache(key: string): Promise { + // get memory cache first + const val = this.cache.get(key); + if (val) { + return val + } + + const fsVal = await this.readFsCache(key) + if(fsVal) { + // set to memory cache + this.cache.set(key, fsVal) + } + + return fsVal; + } + + public async writeToCache(key: string, val: T): Promise { + // Set in-memory cache + this.cache.set(key, val); + + // Write to file-based cache + await this.writeFsCache(key, val); + } +} + +export class BirdeyeProvider extends BaseCachedProvider { + private symbolMap: Record + private maxRetries: number + + constructor( + cacheManager: ICacheManager, + symbolMap?: Record, + maxRetries?: number + ) { + super(cacheManager, "birdeye/data") + this.symbolMap = symbolMap || DEFAULT_SUPPORTED_SYMBOLS + this.maxRetries = maxRetries || DEFAULT_MAX_RETRIES + } + + private getTokenAddress(symbol: string) { + const addr = this.symbolMap[symbol] + + if(!addr) { + throw new Error(`Unsupported symbol ${symbol} in Birdeye provider`) + } + + return addr + } + + private getUrlByType(type: string, address: string) { + const path = ENDPOINT_MAP[type] + + if(!path) { + throw new Error(`Unsupported symbol ${type} in Birdeye provider`) + } + + return `${API_BASE_URL}${path}${address}` + } + + private async fetchWithRetry( + url: string, + options: RequestInit = {} + ): Promise { + let attempts = 0 + + while(attempts < this.maxRetries) { + attempts ++ + try { + const resp = await fetch(url, { + ...options, + headers: { + Accept: "application/json", + "x-chain": settings.BIRDEYE_CHAIN || "solana", + "X-API-KEY": settings.BIRDEYE_API_KEY || "", + ...options.headers + } + }) + + if(!resp.ok) { + const errorText = await resp.text(); + throw new Error( + `HTTP error! status: ${resp.status}, message: ${errorText}` + ); + } + + const data = await resp.json(); + return data; + + } catch(error) { + if(attempts === this.maxRetries) { + // failed after all + throw error + } + await waitFor(RETRY_DELAY_MS) + } + + } + } + + + public async fetchPriceBySymbol(symbol: string) { + return this.fetchPriceByAddress(this.getTokenAddress(symbol)) + } + public async fetchPriceByAddress(address: string) { + const url = this.getUrlByType('price', address) + return this.fetchWithRetry(url) + } + + public async fetchTokenSecurityBySymbol(symbol: string) { + return this.fetchTokenSecurityByAddress(this.getTokenAddress(symbol)) + } + public async fetchTokenSecurityByAddress(address: string) { + const url = this.getUrlByType('security', address) + return this.fetchWithRetry(url) + } + + public async fetchTokenTradeDataBySymbol(symbol:string) { + return this.fetchTokenTradeDataByAddress(this.getTokenAddress(symbol)) + } + public async fetchTokenTradeDataByAddress(address:string) { + const url = this.getUrlByType('volume', address) + return this.fetchWithRetry(url) + } + + public async fetchWalletPortfolio(address: string) { + const url = this.getUrlByType('portfolio', address) + return this.fetchWithRetry(url) + } +} + +export const birdeyeProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise => { + try { + const provider = new BirdeyeProvider(runtime.cacheManager); + + return ''; + } catch (error) { + console.error("Error fetching token data:", error); + return "Unable to fetch token information. Please try again later."; + } + }, +} diff --git a/packages/plugin-birdeye/tsconfig.json b/packages/plugin-birdeye/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/plugin-birdeye/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-birdeye/tsup.config.ts b/packages/plugin-birdeye/tsup.config.ts new file mode 100644 index 0000000000..dd25475bb6 --- /dev/null +++ b/packages/plugin-birdeye/tsup.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", + "stream", + "buffer", + "querystring", + "amqplib", + // Add other modules you want to externalize + ], +}); From 1b03327c57838b1e1b7357fee48eb105abaa2680 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 11:07:37 +0700 Subject: [PATCH 02/15] feat: init wallet porfolio if needed --- packages/plugin-birdeye/src/providers/birdeye.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index d181875567..4079dcdaa4 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -187,7 +187,17 @@ export const birdeyeProvider: Provider = { try { const provider = new BirdeyeProvider(runtime.cacheManager); - return ''; + const walletAddr = runtime.getSetting('BIRDEYE_WALLET_ADDR') + + if(!walletAddr) { + console.warn('No Birdeye wallet was specified') + + return `Birdeye provider initiated with no wallet found` + } + + const portfolio = await provider.fetchWalletPortfolio(walletAddr) + + return `Birdeye wallet addr: ${walletAddr}, portfolio: ${portfolio}`; } catch (error) { console.error("Error fetching token data:", error); return "Unable to fetch token information. Please try again later."; From d756b09bc94eac2cad54c4e3fa8bf5cf7100e688 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 11:24:06 +0700 Subject: [PATCH 03/15] feat: update README, provide some basic useage and integration guide --- packages/plugin-birdeye/README | 25 +++++++++++++++++++ .../plugin-birdeye/src/providers/birdeye.ts | 1 - 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-birdeye/README diff --git a/packages/plugin-birdeye/README b/packages/plugin-birdeye/README new file mode 100644 index 0000000000..3a2efd6f2d --- /dev/null +++ b/packages/plugin-birdeye/README @@ -0,0 +1,25 @@ +# Birdeye plugin for Eliza + +Basic API wrapper for inside our AI agent, almost readonly API only. +For more information please refer to their [docs](https://docs.birdeye.so/reference/get_defi-tokenlist) + +## What you need? + +``` +BIRDEYE_API_KEY=zzz-some-secret +BIRDEYE_WALLET_ADDR=your porfolio profile address +``` + +## Integrate with other plugin + +```js +// init your provider with custom support map +const provider = new BirdeyeProvider(runtime.cacheManager, { + 'WETH': '0xs0000000001231231', + ... +}) + +const price = await provider.fetchTokenPriceBySymbol('WETH') + +// further action based on this +``` \ No newline at end of file diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index 4079dcdaa4..a51fb13b21 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -147,7 +147,6 @@ export class BirdeyeProvider extends BaseCachedProvider { } } - public async fetchPriceBySymbol(symbol: string) { return this.fetchPriceByAddress(this.getTokenAddress(symbol)) } From 6b55ba4e01152531663ff2547b2370908c58fc62 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 13:21:39 +0700 Subject: [PATCH 04/15] feat: add tests and format code --- packages/plugin-birdeye/README | 2 +- packages/plugin-birdeye/package.json | 62 ++-- packages/plugin-birdeye/src/environment.ts | 3 + packages/plugin-birdeye/src/index.ts | 2 +- .../plugin-birdeye/src/providers/birdeye.ts | 123 ++++---- .../plugin-birdeye/src/tests/birdeye.test.ts | 291 ++++++++++++++++++ packages/plugin-birdeye/tsconfig.json | 6 +- 7 files changed, 388 insertions(+), 101 deletions(-) create mode 100644 packages/plugin-birdeye/src/tests/birdeye.test.ts diff --git a/packages/plugin-birdeye/README b/packages/plugin-birdeye/README index 3a2efd6f2d..6d9c54b726 100644 --- a/packages/plugin-birdeye/README +++ b/packages/plugin-birdeye/README @@ -22,4 +22,4 @@ const provider = new BirdeyeProvider(runtime.cacheManager, { const price = await provider.fetchTokenPriceBySymbol('WETH') // further action based on this -``` \ No newline at end of file +``` diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json index f89995ae7c..d6181a01b1 100644 --- a/packages/plugin-birdeye/package.json +++ b/packages/plugin-birdeye/package.json @@ -1,33 +1,33 @@ { - "name": "@ai16z/plugin-birdeye", - "version": "0.1.6-alpha.4", - "main": "dist/index.js", - "type": "module", - "types": "dist/index.d.ts", - "dependencies": { - "@ai16z/eliza": "workspace:*", - "@ai16z/plugin-trustdb": "workspace:*", - "@ai16z/plugin-tee": "workspace:*", - "@coral-xyz/anchor": "0.30.1", - "@solana/spl-token": "0.4.9", - "@solana/web3.js": "1.95.8", - "bignumber": "1.1.0", - "bignumber.js": "9.1.2", - "bs58": "6.0.0", - "fomo-sdk-solana": "1.3.2", - "node-cache": "5.1.2", - "pumpdotfun-sdk": "1.3.2", - "tsup": "8.3.5", - "vitest": "2.1.4" - }, - "scripts": { - "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache .", - "test": "vitest run" - }, - "peerDependencies": { - "form-data": "4.0.1", - "whatwg-url": "7.1.0" - } + "name": "@ai16z/plugin-birdeye", + "version": "0.1.6-alpha.4", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@ai16z/plugin-trustdb": "workspace:*", + "@ai16z/plugin-tee": "workspace:*", + "@coral-xyz/anchor": "0.30.1", + "@solana/spl-token": "0.4.9", + "@solana/web3.js": "1.95.8", + "bignumber": "1.1.0", + "bignumber.js": "9.1.2", + "bs58": "6.0.0", + "fomo-sdk-solana": "1.3.2", + "node-cache": "5.1.2", + "pumpdotfun-sdk": "1.3.2", + "tsup": "8.3.5", + "vitest": "2.1.4" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } } diff --git a/packages/plugin-birdeye/src/environment.ts b/packages/plugin-birdeye/src/environment.ts index ebe6cbaf36..149a338da1 100644 --- a/packages/plugin-birdeye/src/environment.ts +++ b/packages/plugin-birdeye/src/environment.ts @@ -15,6 +15,9 @@ export async function validateBirdeyeConfig( BIRDEYE_API_KEY: runtime.getSetting("BIRDEYE_API_KEY") || process.env.BIRDEYE_API_KEY, + BIRDEYE_WALLET_ADDR: + runtime.getSetting("BIRDEYE_WALLET_ADDR") || + process.env.BIRDEYE_WALLET_ADDR, }; return birdeyeEnvSchema.parse(config); diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts index 32522dfb08..f40ddd8773 100644 --- a/packages/plugin-birdeye/src/index.ts +++ b/packages/plugin-birdeye/src/index.ts @@ -1,6 +1,6 @@ import { Plugin } from "@ai16z/eliza"; -import {birdeyeProvider, BirdeyeProvider} from './providers/birdeye' +import { birdeyeProvider, BirdeyeProvider } from "./providers/birdeye"; export { BirdeyeProvider }; diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index a51fb13b21..9f0ab09052 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -1,29 +1,28 @@ - import NodeCache from "node-cache"; import * as path from "path"; import { ICacheManager, settings } from "@ai16z/eliza"; import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza"; -const DEFAULT_MAX_RETRIES = 3 +const DEFAULT_MAX_RETRIES = 3; const DEFAULT_SUPPORTED_SYMBOLS = { SOL: "So11111111111111111111111111111111111111112", BTC: "qfnqNqs3nCAHjnyCgLRDbBtq4p2MtHZxw8YjSyYhPoL", ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", Example: "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh", -} +}; -const API_BASE_URL = 'https://public-api.birdeye.so' +const API_BASE_URL = "https://public-api.birdeye.so"; const ENDPOINT_MAP = { - price: '/defi/price?address=', - security: '/defi/token_security?address=', - volume: '/defi/v3/token/trade-data/single?address=', - portfolio: '/v1/wallet/token_list?wallet=' -} -const RETRY_DELAY_MS = 2_000 - -const waitFor = (ms:number) => new Promise((resolve)=> setTimeout(resolve, ms)) + price: "/defi/price?address=", + security: "/defi/token_security?address=", + volume: "/defi/v3/token/trade-data/single?address=", + portfolio: "/v1/wallet/token_list?wallet=", +}; +const RETRY_DELAY_MS = 2_000; +const waitFor = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); class BaseCachedProvider { private cache: NodeCache; @@ -37,9 +36,7 @@ class BaseCachedProvider { } private readFsCache(key: string): Promise { - return this.cacheManager.get( - path.join(this.cacheKey, key) - ); + return this.cacheManager.get(path.join(this.cacheKey, key)); } private writeFsCache(key: string, data: T): Promise { @@ -52,13 +49,13 @@ class BaseCachedProvider { // get memory cache first const val = this.cache.get(key); if (val) { - return val + return val; } - const fsVal = await this.readFsCache(key) - if(fsVal) { + const fsVal = await this.readFsCache(key); + if (fsVal) { // set to memory cache - this.cache.set(key, fsVal) + this.cache.set(key, fsVal); } return fsVal; @@ -74,47 +71,47 @@ class BaseCachedProvider { } export class BirdeyeProvider extends BaseCachedProvider { - private symbolMap: Record - private maxRetries: number + private symbolMap: Record; + private maxRetries: number; constructor( cacheManager: ICacheManager, symbolMap?: Record, maxRetries?: number ) { - super(cacheManager, "birdeye/data") - this.symbolMap = symbolMap || DEFAULT_SUPPORTED_SYMBOLS - this.maxRetries = maxRetries || DEFAULT_MAX_RETRIES + super(cacheManager, "birdeye/data"); + this.symbolMap = symbolMap || DEFAULT_SUPPORTED_SYMBOLS; + this.maxRetries = maxRetries || DEFAULT_MAX_RETRIES; } private getTokenAddress(symbol: string) { - const addr = this.symbolMap[symbol] + const addr = this.symbolMap[symbol]; - if(!addr) { - throw new Error(`Unsupported symbol ${symbol} in Birdeye provider`) + if (!addr) { + throw new Error(`Unsupported symbol ${symbol} in Birdeye provider`); } - return addr + return addr; } private getUrlByType(type: string, address: string) { - const path = ENDPOINT_MAP[type] + const path = ENDPOINT_MAP[type]; - if(!path) { - throw new Error(`Unsupported symbol ${type} in Birdeye provider`) + if (!path) { + throw new Error(`Unsupported symbol ${type} in Birdeye provider`); } - return `${API_BASE_URL}${path}${address}` + return `${API_BASE_URL}${path}${address}`; } private async fetchWithRetry( url: string, options: RequestInit = {} ): Promise { - let attempts = 0 + let attempts = 0; - while(attempts < this.maxRetries) { - attempts ++ + while (attempts < this.maxRetries) { + attempts++; try { const resp = await fetch(url, { ...options, @@ -122,11 +119,11 @@ export class BirdeyeProvider extends BaseCachedProvider { Accept: "application/json", "x-chain": settings.BIRDEYE_CHAIN || "solana", "X-API-KEY": settings.BIRDEYE_API_KEY || "", - ...options.headers - } - }) + ...options.headers, + }, + }); - if(!resp.ok) { + if (!resp.ok) { const errorText = await resp.text(); throw new Error( `HTTP error! status: ${resp.status}, message: ${errorText}` @@ -135,45 +132,43 @@ export class BirdeyeProvider extends BaseCachedProvider { const data = await resp.json(); return data; - - } catch(error) { - if(attempts === this.maxRetries) { + } catch (error) { + if (attempts === this.maxRetries) { // failed after all - throw error + throw error; } - await waitFor(RETRY_DELAY_MS) + await waitFor(RETRY_DELAY_MS); } - } } public async fetchPriceBySymbol(symbol: string) { - return this.fetchPriceByAddress(this.getTokenAddress(symbol)) + return this.fetchPriceByAddress(this.getTokenAddress(symbol)); } public async fetchPriceByAddress(address: string) { - const url = this.getUrlByType('price', address) - return this.fetchWithRetry(url) + const url = this.getUrlByType("price", address); + return this.fetchWithRetry(url); } public async fetchTokenSecurityBySymbol(symbol: string) { - return this.fetchTokenSecurityByAddress(this.getTokenAddress(symbol)) + return this.fetchTokenSecurityByAddress(this.getTokenAddress(symbol)); } public async fetchTokenSecurityByAddress(address: string) { - const url = this.getUrlByType('security', address) - return this.fetchWithRetry(url) + const url = this.getUrlByType("security", address); + return this.fetchWithRetry(url); } - public async fetchTokenTradeDataBySymbol(symbol:string) { - return this.fetchTokenTradeDataByAddress(this.getTokenAddress(symbol)) + public async fetchTokenTradeDataBySymbol(symbol: string) { + return this.fetchTokenTradeDataByAddress(this.getTokenAddress(symbol)); } - public async fetchTokenTradeDataByAddress(address:string) { - const url = this.getUrlByType('volume', address) - return this.fetchWithRetry(url) + public async fetchTokenTradeDataByAddress(address: string) { + const url = this.getUrlByType("volume", address); + return this.fetchWithRetry(url); } public async fetchWalletPortfolio(address: string) { - const url = this.getUrlByType('portfolio', address) - return this.fetchWithRetry(url) + const url = this.getUrlByType("portfolio", address); + return this.fetchWithRetry(url); } } @@ -186,15 +181,15 @@ export const birdeyeProvider: Provider = { try { const provider = new BirdeyeProvider(runtime.cacheManager); - const walletAddr = runtime.getSetting('BIRDEYE_WALLET_ADDR') + const walletAddr = runtime.getSetting("BIRDEYE_WALLET_ADDR"); - if(!walletAddr) { - console.warn('No Birdeye wallet was specified') + if (!walletAddr) { + console.warn("No Birdeye wallet was specified"); - return `Birdeye provider initiated with no wallet found` + return `Birdeye provider initiated with no wallet found`; } - const portfolio = await provider.fetchWalletPortfolio(walletAddr) + const portfolio = await provider.fetchWalletPortfolio(walletAddr); return `Birdeye wallet addr: ${walletAddr}, portfolio: ${portfolio}`; } catch (error) { @@ -202,4 +197,4 @@ export const birdeyeProvider: Provider = { return "Unable to fetch token information. Please try again later."; } }, -} +}; diff --git a/packages/plugin-birdeye/src/tests/birdeye.test.ts b/packages/plugin-birdeye/src/tests/birdeye.test.ts new file mode 100644 index 0000000000..d95e0f59ea --- /dev/null +++ b/packages/plugin-birdeye/src/tests/birdeye.test.ts @@ -0,0 +1,291 @@ +import NodeCache from "node-cache"; +import { ICacheManager } from "@ai16z/eliza"; +import { BirdeyeProvider } from "../providers/birdeye"; + +import { describe, it, expect, beforeEach, afterEach, vi, Mock } from "vitest"; + +// Mock fetch implementation +global.fetch = vi.fn(); + +describe("BirdeyeProvider", () => { + describe("basic fetching", () => { + let cacheManager: ICacheManager; + let provider: BirdeyeProvider; + + beforeEach(() => { + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + provider = new BirdeyeProvider(cacheManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should fetch price by symbol", async () => { + const mockResponse = { price: 100 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchPriceBySymbol("SOL"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/price?address=So11111111111111111111111111111111111111112", + expect.any(Object) + ); + }); + + it("should fetch token security by symbol", async () => { + const mockResponse = { security: "secure" }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchTokenSecurityBySymbol("SOL"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/token_security?address=So11111111111111111111111111111111111111112", + expect.any(Object) + ); + }); + + it("should fetch token trade data by symbol", async () => { + const mockResponse = { volume: 1000 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchTokenTradeDataBySymbol("SOL"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/v3/token/trade-data/single?address=So11111111111111111111111111111111111111112", + expect.any(Object) + ); + }); + + it("should fetch wallet portfolio", async () => { + const mockResponse = { portfolio: [] }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchWalletPortfolio( + "some-wallet-address" + ); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/v1/wallet/token_list?wallet=some-wallet-address", + expect.any(Object) + ); + }); + }); + + describe("retries options", () => { + let cacheManager: ICacheManager; + let provider: BirdeyeProvider; + + beforeEach(() => { + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + provider = new BirdeyeProvider(cacheManager); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should retry fetch on failure and succeed", async () => { + const mockResponse = { price: 100 }; + (fetch as Mock) + .mockRejectedValueOnce(new Error("Network error")) // First attempt fails + .mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); // Second attempt succeeds + + const result = await provider.fetchPriceBySymbol("SOL"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(2); // Ensure it retried + }); + + it("should fail after max retries", async () => { + const error = new Error("Network error"); + (fetch as Mock) + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) // Second attempt fails + .mockRejectedValueOnce(error); // Third attempt also fails + + await expect(provider.fetchPriceBySymbol("SOL")).rejects.toThrow( + "Network error" + ); + + expect(fetch).toHaveBeenCalledTimes(3); // Ensure it retried + }); + }); + + describe("with custom symbols", () => { + let cacheManager: ICacheManager; + let provider: BirdeyeProvider; + + beforeEach(() => { + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + provider = new BirdeyeProvider(cacheManager, { + WETH: "0x32323232323232", + }); + }); + + it("should fetch price for a custom symbol WETH", async () => { + const mockResponse = { price: 2000 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchPriceBySymbol("WETH"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/price?address=0x32323232323232", + expect.any(Object) + ); + }); + + it("should fetch token security for a custom symbol WETH", async () => { + const mockResponse = { security: "secure" }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchTokenSecurityBySymbol("WETH"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/token_security?address=0x32323232323232", + expect.any(Object) + ); + }); + + it("should fetch token trade data for a custom symbol WETH", async () => { + const mockResponse = { volume: 500 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchTokenTradeDataBySymbol("WETH"); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledWith( + "https://public-api.birdeye.so/defi/v3/token/trade-data/single?address=0x32323232323232", + expect.any(Object) + ); + }); + }); + + describe.only("with cache", () => { + let cacheManager: ICacheManager; + let provider: BirdeyeProvider; + let nodeCache: NodeCache; + + beforeEach(() => { + nodeCache = new NodeCache(); + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + + provider = new BirdeyeProvider(cacheManager); + provider["cache"] = nodeCache; // Directly set the node cache + }); + + afterEach(() => { + vi.clearAllMocks(); + nodeCache.flushAll(); + }); + + it("should use memory cache when fetching price by symbol", async () => { + const mockResponse = { price: 100 }; + nodeCache.set( + "So11111111111111111111111111111111111111112", + mockResponse + ); // Pre-fill cache + + const result = await provider.fetchPriceBySymbol("SOL"); + + expect(result).toEqual(mockResponse); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("should fetch and cache price by symbol", async () => { + const mockResponse = { price: 100 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + // First call - should fetch and cache the result + const result1 = await provider.fetchPriceBySymbol("SOL"); + expect(result1).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(1); + + // Second call - should use cache + const result2 = await provider.fetchPriceBySymbol("SOL"); + expect(result2).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(1); // No additional fetch call + }); + + it("should use file system cache when fetching price by symbol", async () => { + const mockResponse = { price: 100 }; + (cacheManager.get as Mock).mockResolvedValue(mockResponse); + + // Memory cache miss, should use file system cache + const result = await provider.fetchPriceBySymbol("SOL"); + expect(result).toEqual(mockResponse); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("should fetch and cache price by symbol using file system cache", async () => { + const mockResponse = { price: 100 }; + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + (cacheManager.get as Mock).mockResolvedValue(null); // File system cache miss + + // First call - should fetch and cache the result + const result1 = await provider.fetchPriceBySymbol("SOL"); + expect(result1).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(1); + expect(cacheManager.set).toHaveBeenCalledWith( + expect.stringContaining( + "birdeye/data/So11111111111111111111111111111111111111112" + ), + mockResponse, + expect.any(Object) + ); + + // Second call - should use cache + const result2 = await provider.fetchPriceBySymbol("SOL"); + expect(result2).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(1); // No additional fetch call + }); + }); +}); diff --git a/packages/plugin-birdeye/tsconfig.json b/packages/plugin-birdeye/tsconfig.json index 73993deaaf..005fbac9d3 100644 --- a/packages/plugin-birdeye/tsconfig.json +++ b/packages/plugin-birdeye/tsconfig.json @@ -4,7 +4,5 @@ "outDir": "dist", "rootDir": "src" }, - "include": [ - "src/**/*.ts" - ] -} \ No newline at end of file + "include": ["src/**/*.ts"] +} From bbea87e50f3599b6e239abfbb1562817c93745e7 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 13:54:37 +0700 Subject: [PATCH 05/15] feat: utilize cache in fetch functions and tests --- .../plugin-birdeye/src/providers/birdeye.ts | 30 ++++++++++++++----- .../plugin-birdeye/src/tests/birdeye.test.ts | 12 ++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index 9f0ab09052..e1f934a900 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -142,33 +142,47 @@ export class BirdeyeProvider extends BaseCachedProvider { } } + private async fetchWithCacheAndRetry( + type: string, + address: string + ): Promise { + const key = `${type}/${address}` + const val = await this.readFromCache(key) + + if (val) { + return val + } + + const url = this.getUrlByType(type, address); + const data = await this.fetchWithRetry(url); + + await this.writeToCache(key, data) + return data + } + public async fetchPriceBySymbol(symbol: string) { return this.fetchPriceByAddress(this.getTokenAddress(symbol)); } public async fetchPriceByAddress(address: string) { - const url = this.getUrlByType("price", address); - return this.fetchWithRetry(url); + return this.fetchWithCacheAndRetry("price", address); } public async fetchTokenSecurityBySymbol(symbol: string) { return this.fetchTokenSecurityByAddress(this.getTokenAddress(symbol)); } public async fetchTokenSecurityByAddress(address: string) { - const url = this.getUrlByType("security", address); - return this.fetchWithRetry(url); + return this.fetchWithCacheAndRetry("security", address); } public async fetchTokenTradeDataBySymbol(symbol: string) { return this.fetchTokenTradeDataByAddress(this.getTokenAddress(symbol)); } public async fetchTokenTradeDataByAddress(address: string) { - const url = this.getUrlByType("volume", address); - return this.fetchWithRetry(url); + return this.fetchWithCacheAndRetry("volume", address); } public async fetchWalletPortfolio(address: string) { - const url = this.getUrlByType("portfolio", address); - return this.fetchWithRetry(url); + return this.fetchWithCacheAndRetry("portfolio", address); } } diff --git a/packages/plugin-birdeye/src/tests/birdeye.test.ts b/packages/plugin-birdeye/src/tests/birdeye.test.ts index d95e0f59ea..155fc2cfd5 100644 --- a/packages/plugin-birdeye/src/tests/birdeye.test.ts +++ b/packages/plugin-birdeye/src/tests/birdeye.test.ts @@ -4,8 +4,6 @@ import { BirdeyeProvider } from "../providers/birdeye"; import { describe, it, expect, beforeEach, afterEach, vi, Mock } from "vitest"; -// Mock fetch implementation -global.fetch = vi.fn(); describe("BirdeyeProvider", () => { describe("basic fetching", () => { @@ -18,6 +16,7 @@ describe("BirdeyeProvider", () => { set: vi.fn(), } as unknown as ICacheManager; provider = new BirdeyeProvider(cacheManager); + global.fetch = vi.fn(); }); afterEach(() => { @@ -101,6 +100,7 @@ describe("BirdeyeProvider", () => { set: vi.fn(), } as unknown as ICacheManager; provider = new BirdeyeProvider(cacheManager); + global.fetch = vi.fn(); }); afterEach(() => { @@ -149,6 +149,7 @@ describe("BirdeyeProvider", () => { provider = new BirdeyeProvider(cacheManager, { WETH: "0x32323232323232", }); + global.fetch = vi.fn(); }); it("should fetch price for a custom symbol WETH", async () => { @@ -200,7 +201,7 @@ describe("BirdeyeProvider", () => { }); }); - describe.only("with cache", () => { + describe("with cache", () => { let cacheManager: ICacheManager; let provider: BirdeyeProvider; let nodeCache: NodeCache; @@ -214,6 +215,7 @@ describe("BirdeyeProvider", () => { provider = new BirdeyeProvider(cacheManager); provider["cache"] = nodeCache; // Directly set the node cache + global.fetch = vi.fn(); }); afterEach(() => { @@ -224,7 +226,7 @@ describe("BirdeyeProvider", () => { it("should use memory cache when fetching price by symbol", async () => { const mockResponse = { price: 100 }; nodeCache.set( - "So11111111111111111111111111111111111111112", + "price/So11111111111111111111111111111111111111112", mockResponse ); // Pre-fill cache @@ -276,7 +278,7 @@ describe("BirdeyeProvider", () => { expect(fetch).toHaveBeenCalledTimes(1); expect(cacheManager.set).toHaveBeenCalledWith( expect.stringContaining( - "birdeye/data/So11111111111111111111111111111111111111112" + "birdeye/data/price/So11111111111111111111111111111111111111112" ), mockResponse, expect.any(Object) From f28d149d167004faf4c83f308b6471bcc728b834 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 14:09:29 +0700 Subject: [PATCH 06/15] fix: remove unused deps --- packages/plugin-birdeye/package.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json index d6181a01b1..3950e3cf05 100644 --- a/packages/plugin-birdeye/package.json +++ b/packages/plugin-birdeye/package.json @@ -6,17 +6,7 @@ "types": "dist/index.d.ts", "dependencies": { "@ai16z/eliza": "workspace:*", - "@ai16z/plugin-trustdb": "workspace:*", - "@ai16z/plugin-tee": "workspace:*", - "@coral-xyz/anchor": "0.30.1", - "@solana/spl-token": "0.4.9", - "@solana/web3.js": "1.95.8", - "bignumber": "1.1.0", - "bignumber.js": "9.1.2", - "bs58": "6.0.0", - "fomo-sdk-solana": "1.3.2", "node-cache": "5.1.2", - "pumpdotfun-sdk": "1.3.2", "tsup": "8.3.5", "vitest": "2.1.4" }, From d5680517fc85d969d253dfe0d4db2057d47949b6 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 14:15:11 +0700 Subject: [PATCH 07/15] chore: update lock file --- pnpm-lock.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f0e904aa4..47f4dcd47a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -971,6 +971,27 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-birdeye: + dependencies: + '@ai16z/eliza': + specifier: workspace:* + version: link:../core + form-data: + specifier: 4.0.1 + version: 4.0.1 + node-cache: + specifier: 5.1.2 + version: 5.1.2 + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.1)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + vitest: + specifier: 2.1.4 + version: 2.1.4(@types/node@22.10.2)(jsdom@25.0.1(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@5.0.10))(terser@5.37.0) + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-bootstrap: dependencies: '@elizaos/core': From ea3c313c02b57f8013eb3cef665e07f2e3f9ded9 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Fri, 20 Dec 2024 15:47:19 +0700 Subject: [PATCH 08/15] feat: add new package to agent --- agent/package.json | 1 + packages/plugin-birdeye/package.json | 6 +++--- pnpm-lock.yaml | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/agent/package.json b/agent/package.json index 91a900d600..4bf784b5b9 100644 --- a/agent/package.json +++ b/agent/package.json @@ -51,6 +51,7 @@ "@elizaos/plugin-multiversx": "workspace:*", "@elizaos/plugin-near": "workspace:*", "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-birdeye": "workspace:*", "readline": "1.3.0", "ws": "8.18.0", "yargs": "17.7.2" diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json index 3950e3cf05..583dea19f5 100644 --- a/packages/plugin-birdeye/package.json +++ b/packages/plugin-birdeye/package.json @@ -1,11 +1,11 @@ { - "name": "@ai16z/plugin-birdeye", - "version": "0.1.6-alpha.4", + "name": "@elizaos/plugin-birdeye", + "version": "0.1.7-alpha.1", "main": "dist/index.js", "type": "module", "types": "dist/index.d.ts", "dependencies": { - "@ai16z/eliza": "workspace:*", + "@elizaos/eliza": "workspace:*", "node-cache": "5.1.2", "tsup": "8.3.5", "vitest": "2.1.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47f4dcd47a..30e04a191b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,7 +141,10 @@ importers: '@elizaos/plugin-aptos': specifier: workspace:* version: link:../packages/plugin-aptos - '@elizaos/plugin-bootstrap': + '@ai16zaos/plugin-birdeye': + specifier: workspace:* + version: link:../packages/plugin-birdeye + '@ai16z/plugin-bootstrap': specifier: workspace:* version: link:../packages/plugin-bootstrap '@elizaos/plugin-coinbase': From a4f5d8bfaad1257c387013c56291a7ca3ada231e Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Sun, 22 Dec 2024 10:14:05 +0700 Subject: [PATCH 09/15] feat: init birdeye provider from character config --- agent/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agent/src/index.ts b/agent/src/index.ts index 1e49bae84f..6b034d3e4e 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -55,6 +55,7 @@ import { suiPlugin } from "@elizaos/plugin-sui"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { tonPlugin } from "@elizaos/plugin-ton"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; +import { birdeyePlugin } from "@elizaos/plugin-birdeye"; import Database from "better-sqlite3"; import fs from "fs"; import path from "path"; @@ -571,6 +572,7 @@ export async function createAgent( getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, + getSecret(character, "BIRDEYE_API_KEY") ? birdeyePlugin : null, ].filter(Boolean), providers: [], actions: [], From 999ecc0f66984c1705b96d3e3897b3da6037a2e3 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Sun, 22 Dec 2024 11:10:06 +0700 Subject: [PATCH 10/15] chore: update lock file --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30e04a191b..1180dd2aff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -987,7 +987,7 @@ importers: version: 5.1.2 tsup: specifier: 8.3.5 - version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.1)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) + version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(typescript@5.6.3)(yaml@2.6.1) vitest: specifier: 2.1.4 version: 2.1.4(@types/node@22.10.2)(jsdom@25.0.1(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@5.0.10))(terser@5.37.0) From 003d4103c9d2812e1cd99347f5f6b316d4f1848c Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Sun, 22 Dec 2024 13:34:49 +0700 Subject: [PATCH 11/15] feat: update root package prefix --- packages/plugin-birdeye/package.json | 2 +- packages/plugin-birdeye/src/environment.ts | 2 +- packages/plugin-birdeye/src/index.ts | 2 +- packages/plugin-birdeye/src/providers/birdeye.ts | 4 ++-- packages/plugin-birdeye/src/tests/birdeye.test.ts | 2 +- pnpm-lock.yaml | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json index 583dea19f5..f167b47a9a 100644 --- a/packages/plugin-birdeye/package.json +++ b/packages/plugin-birdeye/package.json @@ -5,7 +5,7 @@ "type": "module", "types": "dist/index.d.ts", "dependencies": { - "@elizaos/eliza": "workspace:*", + "@elizaos/core": "workspace:*", "node-cache": "5.1.2", "tsup": "8.3.5", "vitest": "2.1.4" diff --git a/packages/plugin-birdeye/src/environment.ts b/packages/plugin-birdeye/src/environment.ts index 149a338da1..316ba6a021 100644 --- a/packages/plugin-birdeye/src/environment.ts +++ b/packages/plugin-birdeye/src/environment.ts @@ -1,4 +1,4 @@ -import { IAgentRuntime } from "@ai16z/eliza"; +import { IAgentRuntime } from "@elizaos/eliza"; import { z } from "zod"; export const birdeyeEnvSchema = z.object({ diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts index f40ddd8773..cdf34a18d7 100644 --- a/packages/plugin-birdeye/src/index.ts +++ b/packages/plugin-birdeye/src/index.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@ai16z/eliza"; +import { Plugin } from "@elizaos/eliza"; import { birdeyeProvider, BirdeyeProvider } from "./providers/birdeye"; diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index e1f934a900..2072c247e3 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -1,7 +1,7 @@ import NodeCache from "node-cache"; import * as path from "path"; -import { ICacheManager, settings } from "@ai16z/eliza"; -import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza"; +import { ICacheManager, settings } from "@elizaos/eliza"; +import { IAgentRuntime, Memory, Provider, State } from "@elizaos/eliza"; const DEFAULT_MAX_RETRIES = 3; diff --git a/packages/plugin-birdeye/src/tests/birdeye.test.ts b/packages/plugin-birdeye/src/tests/birdeye.test.ts index 155fc2cfd5..8a1af6944c 100644 --- a/packages/plugin-birdeye/src/tests/birdeye.test.ts +++ b/packages/plugin-birdeye/src/tests/birdeye.test.ts @@ -1,5 +1,5 @@ import NodeCache from "node-cache"; -import { ICacheManager } from "@ai16z/eliza"; +import { ICacheManager } from "@elizaos/eliza"; import { BirdeyeProvider } from "../providers/birdeye"; import { describe, it, expect, beforeEach, afterEach, vi, Mock } from "vitest"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1180dd2aff..857953e524 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,10 +141,10 @@ importers: '@elizaos/plugin-aptos': specifier: workspace:* version: link:../packages/plugin-aptos - '@ai16zaos/plugin-birdeye': + '@elizaos/plugin-birdeye': specifier: workspace:* version: link:../packages/plugin-birdeye - '@ai16z/plugin-bootstrap': + '@elizaos/plugin-bootstrap': specifier: workspace:* version: link:../packages/plugin-bootstrap '@elizaos/plugin-coinbase': @@ -976,7 +976,7 @@ importers: packages/plugin-birdeye: dependencies: - '@ai16z/eliza': + '@elizaos/core': specifier: workspace:* version: link:../core form-data: From dd370914de200aae68202a974270bc0e2b677c7a Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Sun, 22 Dec 2024 13:54:57 +0700 Subject: [PATCH 12/15] fix: update core package after rebasing --- packages/plugin-birdeye/package.json | 4 ---- packages/plugin-birdeye/src/environment.ts | 2 +- packages/plugin-birdeye/src/index.ts | 2 +- packages/plugin-birdeye/src/providers/birdeye.ts | 4 ++-- packages/plugin-birdeye/src/tests/birdeye.test.ts | 2 +- packages/plugin-birdeye/tsup.config.ts | 15 --------------- pnpm-lock.yaml | 6 ------ 7 files changed, 5 insertions(+), 30 deletions(-) diff --git a/packages/plugin-birdeye/package.json b/packages/plugin-birdeye/package.json index f167b47a9a..3cbaa22469 100644 --- a/packages/plugin-birdeye/package.json +++ b/packages/plugin-birdeye/package.json @@ -15,9 +15,5 @@ "dev": "tsup --format esm --dts --watch", "lint": "eslint --fix --cache .", "test": "vitest run" - }, - "peerDependencies": { - "form-data": "4.0.1", - "whatwg-url": "7.1.0" } } diff --git a/packages/plugin-birdeye/src/environment.ts b/packages/plugin-birdeye/src/environment.ts index 316ba6a021..47378f9b1b 100644 --- a/packages/plugin-birdeye/src/environment.ts +++ b/packages/plugin-birdeye/src/environment.ts @@ -1,4 +1,4 @@ -import { IAgentRuntime } from "@elizaos/eliza"; +import { IAgentRuntime } from "@elizaos/core"; import { z } from "zod"; export const birdeyeEnvSchema = z.object({ diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts index cdf34a18d7..47445aed4c 100644 --- a/packages/plugin-birdeye/src/index.ts +++ b/packages/plugin-birdeye/src/index.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@elizaos/eliza"; +import { Plugin } from "@elizaos/core"; import { birdeyeProvider, BirdeyeProvider } from "./providers/birdeye"; diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index 2072c247e3..fa286204ed 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -1,7 +1,7 @@ import NodeCache from "node-cache"; import * as path from "path"; -import { ICacheManager, settings } from "@elizaos/eliza"; -import { IAgentRuntime, Memory, Provider, State } from "@elizaos/eliza"; +import { ICacheManager, settings } from "@elizaos/core"; +import { IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; const DEFAULT_MAX_RETRIES = 3; diff --git a/packages/plugin-birdeye/src/tests/birdeye.test.ts b/packages/plugin-birdeye/src/tests/birdeye.test.ts index 8a1af6944c..577fee36eb 100644 --- a/packages/plugin-birdeye/src/tests/birdeye.test.ts +++ b/packages/plugin-birdeye/src/tests/birdeye.test.ts @@ -1,5 +1,5 @@ import NodeCache from "node-cache"; -import { ICacheManager } from "@elizaos/eliza"; +import { ICacheManager } from "@elizaos/core"; import { BirdeyeProvider } from "../providers/birdeye"; import { describe, it, expect, beforeEach, afterEach, vi, Mock } from "vitest"; diff --git a/packages/plugin-birdeye/tsup.config.ts b/packages/plugin-birdeye/tsup.config.ts index dd25475bb6..047167c52d 100644 --- a/packages/plugin-birdeye/tsup.config.ts +++ b/packages/plugin-birdeye/tsup.config.ts @@ -10,20 +10,5 @@ export default defineConfig({ "dotenv", // Externalize dotenv to prevent bundling "fs", // Externalize fs to use Node.js built-in module "path", // Externalize other built-ins if necessary - "@reflink/reflink", - "@node-llama-cpp", - "https", - "http", - "agentkeepalive", - "safe-buffer", - "base-x", - "bs58", - "borsh", - "@solana/buffer-layout", - "stream", - "buffer", - "querystring", - "amqplib", - // Add other modules you want to externalize ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 857953e524..f9d6a69032 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -979,9 +979,6 @@ importers: '@elizaos/core': specifier: workspace:* version: link:../core - form-data: - specifier: 4.0.1 - version: 4.0.1 node-cache: specifier: 5.1.2 version: 5.1.2 @@ -991,9 +988,6 @@ importers: vitest: specifier: 2.1.4 version: 2.1.4(@types/node@22.10.2)(jsdom@25.0.1(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@5.0.10))(terser@5.37.0) - whatwg-url: - specifier: 7.1.0 - version: 7.1.0 packages/plugin-bootstrap: dependencies: From b39aeb3a8855ba9c7fc7d959457390320ffa81ed Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Mon, 23 Dec 2024 12:12:50 +0700 Subject: [PATCH 13/15] feat: add reviewToken action --- packages/plugin-birdeye/src/actions/report.ts | 182 ++++++++++++++++++ packages/plugin-birdeye/src/index.ts | 2 + 2 files changed, 184 insertions(+) create mode 100644 packages/plugin-birdeye/src/actions/report.ts diff --git a/packages/plugin-birdeye/src/actions/report.ts b/packages/plugin-birdeye/src/actions/report.ts new file mode 100644 index 0000000000..fd3ae8af9d --- /dev/null +++ b/packages/plugin-birdeye/src/actions/report.ts @@ -0,0 +1,182 @@ +import { + Action, + ActionExample, + composeContext, + elizaLogger, + generateText, + ModelClass, + type IAgentRuntime, + type Memory, + type State +} from "@elizaos/core"; +import { BirdeyeProvider } from "../providers/birdeye"; + +const extractTokenSymbolTemplate = `Given the recent message below: + +{{recentMessages}} + +Extract the following information about the requested token report: +- Input token symbol +- Extra about this symbol + +When to symbol is specified in all lower case, such as btc, eth, sol..., we should convert it into wellknown symbol. +E.g. btc instead of BTC, sol instead of SOL. + +But when we see them in mix form, such as SOl, DOl, we should keep the original and notice user instead. +When in doubt, specify in the message field. + +Respond exactly a JSON object containing only the extracted values, no extra description or message needed. +Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema: +{ + "symbol": string | null, + "message": string | null, +} + +Examples: + Message: "Tell me about BTC" + Response: { + "symbol": "BTC", + "message": null + } + + Message: "Do you know about SOl." + Response: + { + "symbol": "SOl", + "message": "Please note that the symbol is not in the standard format." + } +`; + + +const formatTokenReport = (data) => { + let output = `**Token Security and Trade Report**\n`; + output += `Token Address: ${data.tokenAddress}\n\n`; + + output += `**Ownership Distribution:**\n`; + output += `- Owner Balance: ${data.security.ownerBalance}\n`; + output += `- Creator Balance: ${data.security.creatorBalance}\n`; + output += `- Owner Percentage: ${data.security.ownerPercentage}%\n`; + output += `- Creator Percentage: ${data.security.creatorPercentage}%\n`; + output += `- Top 10 Holders Balance: ${data.security.top10HolderBalance}\n`; + output += `- Top 10 Holders Percentage: ${data.security.top10HolderPercent}%\n\n`; + + // Trade Data + output += `**Trade Data:**\n`; + output += `- Holders: ${data.volume.holder}\n`; + output += `- Unique Wallets (24h): ${data.volume.unique_wallet_24h}\n`; + output += `- Price Change (24h): ${data.volume.price_change_24h_percent}%\n`; + output += `- Price Change (12h): ${data.volume.price_change_12h_percent}%\n`; + output += `- Volume (24h USD): $${data.volume.volume_24h_usd}\n`; + output += `- Current Price: $${data.volume.price}\n\n`; + + return output +} + +const extractTokenSymbol = async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback?: any) => { + const context = composeContext({ + state, + template: extractTokenSymbolTemplate + }) + + const response = await generateText({ + runtime, + context, + modelClass: ModelClass.LARGE, + }) + + try { + const regex = new RegExp(/\{(.+)\}/gms); + const normalized = response && regex.exec(response)?.[0] + elizaLogger.debug('Normalized data', normalized) + return normalized && JSON.parse(normalized) + } catch { + callback?.({text: response}) + return true + } +} + +export const reportToken = { + name: "REPORT_TOKEN", + similes: ["CHECK_TOKEN", "REVIEW_TOKEN", "TOKEN_DETAILS"], + description: "Check background data for a given token", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback?: any + ) => { + try { + const params = await extractTokenSymbol( + runtime, + message, + state, + options, + callback + ) + + elizaLogger.debug('Params', params) + + if(!params?.symbol) { + callback?.({text: "I need a token symbol to begin"}) + return true + } + + if(params?.message) { + callback?.({text: `I need more clarification to continue, ${params.message}`}) + return true + } + + const symbol = params?.symbol + elizaLogger.log('Fetching birdeye data', symbol) + const provider = new BirdeyeProvider(runtime.cacheManager) + + const [security, volume] = await Promise.all([ + provider.fetchTokenSecurityBySymbol(symbol), + provider.fetchTokenTradeDataBySymbol(symbol), + ]); + + elizaLogger.log('Fetching birdeye done') + const msg = formatTokenReport({security: security.data, volume: volume.data}) + callback?.({text: msg}) + return true + } catch (error) { + console.error("Error in reportToken handler:", error.message); + callback?.({ text: `Error: ${error.message}` }); + return false; + } + }, + validate: async (runtime: IAgentRuntime, message: Memory) => { + return true; + }, + examples: [ + [ + { + user: "user", + content: { + text: "Tell me what you know about SOL", + action: "CHECK_TOKEN", + }, + }, + { + user: "user", + content: { + text: "Do you know about SOL", + action: "TOKEN_DETAILS", + }, + }, + { + user: "user", + content: { + text: "Tell me about WETH", + action: "REVIEW_TOKEN", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts index 47445aed4c..60d18a37f7 100644 --- a/packages/plugin-birdeye/src/index.ts +++ b/packages/plugin-birdeye/src/index.ts @@ -1,6 +1,7 @@ import { Plugin } from "@elizaos/core"; import { birdeyeProvider, BirdeyeProvider } from "./providers/birdeye"; +import { reportToken } from "./actions/report"; export { BirdeyeProvider }; @@ -8,6 +9,7 @@ export const birdeyePlugin: Plugin = { name: "birdeye", description: "Birdeye Plugin for Eliza", providers: [birdeyeProvider], + actions: [reportToken] }; export default birdeyePlugin; From 5f9d7ed09ad506b059af2662f9d0cafeb427de64 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Mon, 23 Dec 2024 12:22:04 +0700 Subject: [PATCH 14/15] feat: enhance token report --- packages/plugin-birdeye/src/actions/report.ts | 13 ++++++++++--- packages/plugin-birdeye/src/providers/birdeye.ts | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/plugin-birdeye/src/actions/report.ts b/packages/plugin-birdeye/src/actions/report.ts index fd3ae8af9d..9d5e50057a 100644 --- a/packages/plugin-birdeye/src/actions/report.ts +++ b/packages/plugin-birdeye/src/actions/report.ts @@ -15,7 +15,7 @@ const extractTokenSymbolTemplate = `Given the recent message below: {{recentMessages}} -Extract the following information about the requested token report: +Extract the 1 latest information about the requested token report: - Input token symbol - Extra about this symbol @@ -50,6 +50,7 @@ Examples: const formatTokenReport = (data) => { let output = `**Token Security and Trade Report**\n`; + output += `Token symbol: ${data.symbol}\n` output += `Token Address: ${data.tokenAddress}\n\n`; output += `**Ownership Distribution:**\n`; @@ -136,13 +137,19 @@ export const reportToken = { elizaLogger.log('Fetching birdeye data', symbol) const provider = new BirdeyeProvider(runtime.cacheManager) - const [security, volume] = await Promise.all([ + const [tokenAddress, security, volume] = await Promise.all([ + provider.getTokenAddress(symbol), provider.fetchTokenSecurityBySymbol(symbol), provider.fetchTokenTradeDataBySymbol(symbol), ]); elizaLogger.log('Fetching birdeye done') - const msg = formatTokenReport({security: security.data, volume: volume.data}) + const msg = formatTokenReport({ + symbol, + tokenAddress, + security: security.data, + volume: volume.data + }) callback?.({text: msg}) return true } catch (error) { diff --git a/packages/plugin-birdeye/src/providers/birdeye.ts b/packages/plugin-birdeye/src/providers/birdeye.ts index fa286204ed..2be703bbda 100644 --- a/packages/plugin-birdeye/src/providers/birdeye.ts +++ b/packages/plugin-birdeye/src/providers/birdeye.ts @@ -84,7 +84,7 @@ export class BirdeyeProvider extends BaseCachedProvider { this.maxRetries = maxRetries || DEFAULT_MAX_RETRIES; } - private getTokenAddress(symbol: string) { + public getTokenAddress(symbol: string) { const addr = this.symbolMap[symbol]; if (!addr) { From 0adbb9cd1fd0b107c082521fba206d8ab3893af1 Mon Sep 17 00:00:00 2001 From: simpletrontdip Date: Mon, 23 Dec 2024 13:44:45 +0700 Subject: [PATCH 15/15] feat: refine prompt template --- packages/plugin-birdeye/src/actions/report.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugin-birdeye/src/actions/report.ts b/packages/plugin-birdeye/src/actions/report.ts index 9d5e50057a..9d29367190 100644 --- a/packages/plugin-birdeye/src/actions/report.ts +++ b/packages/plugin-birdeye/src/actions/report.ts @@ -19,11 +19,11 @@ Extract the 1 latest information about the requested token report: - Input token symbol - Extra about this symbol -When to symbol is specified in all lower case, such as btc, eth, sol..., we should convert it into wellknown symbol. +When the symbol is specified in all lower case, such as btc, eth, sol..., we should convert it into wellknown symbol. E.g. btc instead of BTC, sol instead of SOL. -But when we see them in mix form, such as SOl, DOl, we should keep the original and notice user instead. -When in doubt, specify in the message field. +But when we see them in mixed form, such as SOl, DOl, eTH we should keep the original and notice user instead. +When in doubt, specify the concern in the message field. Respond exactly a JSON object containing only the extracted values, no extra description or message needed. Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema: