diff --git a/agent/package.json b/agent/package.json index 6f93fb7b8a1..1e6e261939e 100644 --- a/agent/package.json +++ b/agent/package.json @@ -35,8 +35,9 @@ "@elizaos/plugin-0g": "workspace:*", "@elizaos/plugin-abstract": "workspace:*", "@elizaos/plugin-aptos": "workspace:*", - "@elizaos/plugin-coinmarketcap": "workspace:*", + "@elizaos/plugin-birdeye": "workspace:*", "@elizaos/plugin-coingecko": "workspace:*", + "@elizaos/plugin-coinmarketcap": "workspace:*", "@elizaos/plugin-binance": "workspace:*", "@elizaos/plugin-avail": "workspace:*", "@elizaos/plugin-bootstrap": "workspace:*", @@ -107,4 +108,4 @@ "ts-node": "10.9.2", "tsup": "8.3.5" } -} +} \ No newline at end of file diff --git a/agent/src/index.ts b/agent/src/index.ts index 6dd9859b0bb..d60bdb3c266 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -41,13 +41,16 @@ import createGoatPlugin from "@elizaos/plugin-goat"; import { DirectClient } from "@elizaos/client-direct"; import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation"; import { abstractPlugin } from "@elizaos/plugin-abstract"; +import { akashPlugin } from "@elizaos/plugin-akash"; import { alloraPlugin } from "@elizaos/plugin-allora"; import { aptosPlugin } from "@elizaos/plugin-aptos"; import { artheraPlugin } from "@elizaos/plugin-arthera"; +import { autonomePlugin } from "@elizaos/plugin-autonome"; import { availPlugin } from "@elizaos/plugin-avail"; import { avalanchePlugin } from "@elizaos/plugin-avalanche"; import { b2Plugin } from "@elizaos/plugin-b2"; import { binancePlugin } from "@elizaos/plugin-binance"; +import { birdeyePlugin } from "@elizaos/plugin-birdeye"; import { advancedTradePlugin, coinbaseCommercePlugin, @@ -56,8 +59,8 @@ import { tradePlugin, webhookPlugin, } from "@elizaos/plugin-coinbase"; -import { coinmarketcapPlugin } from "@elizaos/plugin-coinmarketcap"; import { coingeckoPlugin } from "@elizaos/plugin-coingecko"; +import { coinmarketcapPlugin } from "@elizaos/plugin-coinmarketcap"; import { confluxPlugin } from "@elizaos/plugin-conflux"; import { createCosmosPlugin } from "@elizaos/plugin-cosmos"; import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; @@ -66,45 +69,41 @@ import { evmPlugin } from "@elizaos/plugin-evm"; import { flowPlugin } from "@elizaos/plugin-flow"; import { fuelPlugin } from "@elizaos/plugin-fuel"; import { genLayerPlugin } from "@elizaos/plugin-genlayer"; +import { giphyPlugin } from "@elizaos/plugin-giphy"; +import { hyperliquidPlugin } from "@elizaos/plugin-hyperliquid"; import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; import { lensPlugin } from "@elizaos/plugin-lensNetwork"; +import { letzAIPlugin } from "@elizaos/plugin-letzai"; import { multiversxPlugin } from "@elizaos/plugin-multiversx"; import { nearPlugin } from "@elizaos/plugin-near"; +import createNFTCollectionsPlugin from "@elizaos/plugin-nft-collections"; import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; import { createNodePlugin } from "@elizaos/plugin-node"; import { obsidianPlugin } from "@elizaos/plugin-obsidian"; +import { OpacityAdapter } from "@elizaos/plugin-opacity"; +import { openWeatherPlugin } from "@elizaos/plugin-open-weather"; +import { quaiPlugin } from "@elizaos/plugin-quai"; import { sgxPlugin } from "@elizaos/plugin-sgx"; import { solanaPlugin } from "@elizaos/plugin-solana"; import { solanaAgentkitPlguin } from "@elizaos/plugin-solana-agentkit"; -import { autonomePlugin } from "@elizaos/plugin-autonome"; +import { stargazePlugin } from "@elizaos/plugin-stargaze"; import { storyPlugin } from "@elizaos/plugin-story"; import { suiPlugin } from "@elizaos/plugin-sui"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { teeLogPlugin } from "@elizaos/plugin-tee-log"; import { teeMarlinPlugin } from "@elizaos/plugin-tee-marlin"; +import { verifiableLogPlugin } from "@elizaos/plugin-tee-verifiable-log"; +import { thirdwebPlugin } from "@elizaos/plugin-thirdweb"; import { tonPlugin } from "@elizaos/plugin-ton"; import { squidRouterPlugin } from "@elizaos/plugin-squid-router"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; - -import { giphyPlugin } from "@elizaos/plugin-giphy"; -import { letzAIPlugin } from "@elizaos/plugin-letzai"; -import { thirdwebPlugin } from "@elizaos/plugin-thirdweb"; -import { hyperliquidPlugin } from "@elizaos/plugin-hyperliquid"; import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; - -import { OpacityAdapter } from "@elizaos/plugin-opacity"; -import { openWeatherPlugin } from "@elizaos/plugin-open-weather"; -import { stargazePlugin } from "@elizaos/plugin-stargaze"; -import { akashPlugin } from "@elizaos/plugin-akash"; -import { quaiPlugin } from "@elizaos/plugin-quai"; import Database from "better-sqlite3"; import fs from "fs"; import net from "net"; import path from "path"; import { fileURLToPath } from "url"; import yargs from "yargs"; -import { verifiableLogPlugin } from "@elizaos/plugin-tee-verifiable-log"; -import createNFTCollectionsPlugin from "@elizaos/plugin-nft-collections"; const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file const __dirname = path.dirname(__filename); // get the name of the directory @@ -801,9 +800,11 @@ export async function createAgent( ] : []), ...(teeMode !== TEEMode.OFF && walletSecretSalt ? [teePlugin] : []), - (teeMode !== TEEMode.OFF && walletSecretSalt &&getSecret(character,"VLOG") + teeMode !== TEEMode.OFF && + walletSecretSalt && + getSecret(character, "VLOG") ? verifiableLogPlugin - : null), + : null, getSecret(character, "SGX") ? sgxPlugin : null, getSecret(character, "ENABLE_TEE_LOG") && ((teeMode !== TEEMode.OFF && walletSecretSalt) || @@ -859,6 +860,7 @@ export async function createAgent( getSecret(character, "AVALANCHE_PRIVATE_KEY") ? avalanchePlugin : null, + getSecret(character, "BIRDEYE_API_KEY") ? birdeyePlugin : null, getSecret(character, "ECHOCHAMBERS_API_URL") && getSecret(character, "ECHOCHAMBERS_API_KEY") ? echoChambersPlugin diff --git a/packages/plugin-birdeye/.npmignore b/packages/plugin-birdeye/.npmignore new file mode 100644 index 00000000000..078562eceab --- /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/.nvmrc b/packages/plugin-birdeye/.nvmrc new file mode 100644 index 00000000000..fa12cf298e4 --- /dev/null +++ b/packages/plugin-birdeye/.nvmrc @@ -0,0 +1 @@ +v23.3.0 \ No newline at end of file diff --git a/packages/plugin-birdeye/README.md b/packages/plugin-birdeye/README.md new file mode 100644 index 00000000000..0ac543844f4 --- /dev/null +++ b/packages/plugin-birdeye/README.md @@ -0,0 +1,38 @@ +# Eliza Birdeye Plugin + +A powerful plugin for Eliza that integrates with Birdeye's comprehensive DeFi and token analytics API. This plugin provides real-time access to blockchain data, token metrics, and DeFi analytics across multiple networks. + +## Features + +### Provider Featurs + +- **Agent Portfolio Provider** + + - If `BIRDEYE_WALLET_ADDR` is set, this provider will fetch the wallet's portfolio data from Birdeye and be able to respond to questions related to the wallet's holdings. + +### Action Features + +- **Token Search Address** + + - This action will search input message for token addresses and when present will query Birdeye for token information + +- **Token Search Symbol** + + - This action will search input message for token symbols in the format of `$SYMBOL` and when present will query Birdeye for token information. Note that this action currently only supports SOL, SUI, and ETH addresses. + - _Any addresses that look like EVM addresses will be treated as ETH addresses since there is no easy way to distinguish between the other EVM chains that are supported by Birdeye_. + +- **Wallet Search Address** + + - This action will search input message for wallet addresses and when present will query Birdeye for wallet information + +## API Reference + +The plugin provides access to a subset of Birdeye API endpoints through structured interfaces. For detailed API documentation, visit [Birdeye's API Documentation](https://public-api.birdeye.so). + +## License + +See parent project for license information. + +## Contributing + +Contributions are welcome! See parent project for contribution guidelines. diff --git a/packages/plugin-birdeye/eslint.config.mjs b/packages/plugin-birdeye/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /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 00000000000..682b88c751c --- /dev/null +++ b/packages/plugin-birdeye/package.json @@ -0,0 +1,37 @@ +{ + "name": "@elizaos/plugin-birdeye", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@coral-xyz/anchor": "0.30.1", + "@elizaos/core": "workspace:*", + "@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" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.2" + } +} \ No newline at end of file diff --git a/packages/plugin-birdeye/src/actions/test-all-endpoints.ts b/packages/plugin-birdeye/src/actions/test-all-endpoints.ts new file mode 100644 index 00000000000..a2ed8758758 --- /dev/null +++ b/packages/plugin-birdeye/src/actions/test-all-endpoints.ts @@ -0,0 +1,398 @@ +import { + Action, + ActionExample, + elizaLogger, + type IAgentRuntime, + type Memory, + type State, +} from "@elizaos/core"; +import { BirdeyeProvider } from "../birdeye"; +import { waitFor } from "../utils"; + +// This is a dummy action generated solely to test all Birdeye endpoints and should not be used in production +export const testAllEndpointsAction = { + name: "BIRDEYE_TEST_ALL_ENDPOINTS", + similes: [], + description: "Test all Birdeye endpoints with sample data", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: any, + callback?: any + ) => { + try { + elizaLogger.info("Testing all endpoints"); + + await waitFor(1000); + + const birdeyeProvider = new BirdeyeProvider(runtime.cacheManager); + + // Sample data for testing + const sampleParams = { + token: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + address: "MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa", + network: "solana", + list_address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + address_type: "token", + type: "1D", + tx_type: "all", + sort_type: "desc", + unixtime: 1234567890, + base_address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + quote_address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + time_to: 1672531199, // Unix timestamp + meme_platform_enabled: true, + time_frame: "1D", + sort_by: undefined, + list_addresses: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + wallet: "MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa", + token_address: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm", + pair: "samplePair", + before_time: 1672531199, + after_time: 1672331199, + }; + + // Test each fetch function + elizaLogger.info("fetchDefiSupportedNetworks"); + await birdeyeProvider.fetchDefiSupportedNetworks(); + elizaLogger.success("fetchDefiSupportedNetworks: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPrice"); + await birdeyeProvider.fetchDefiPrice({ ...sampleParams }); + elizaLogger.success("fetchDefiPrice: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPriceMultiple"); + await birdeyeProvider.fetchDefiPriceMultiple({ ...sampleParams }); + elizaLogger.success("fetchDefiPriceMultiple: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPriceMultiple_POST"); + await birdeyeProvider.fetchDefiPriceMultiple_POST({ + ...sampleParams, + }); + elizaLogger.success("fetchDefiPriceMultiple_POST: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPriceHistorical"); + await birdeyeProvider.fetchDefiPriceHistorical({ + ...sampleParams, + address_type: "token", + type: "1D", + }); + elizaLogger.success("fetchDefiPriceHistorical: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPriceHistoricalByUnixTime"); + await birdeyeProvider.fetchDefiPriceHistoricalByUnixTime({ + address: sampleParams.token, + }); + elizaLogger.success("fetchDefiPriceHistoricalByUnixTime: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiTradesToken"); + await birdeyeProvider.fetchDefiTradesToken({ + address: sampleParams.token, + }); + elizaLogger.success("fetchDefiTradesToken: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiTradesPair"); + await birdeyeProvider.fetchDefiTradesPair({ + address: sampleParams.token, + }); + elizaLogger.success("fetchDefiTradesPair: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiTradesTokenSeekByTime"); + await birdeyeProvider.fetchDefiTradesTokenSeekByTime({ + address: sampleParams.token, + before_time: sampleParams.before_time, + }); + elizaLogger.success("fetchDefiTradesTokenSeekByTime: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiTradesPairSeekByTime"); + await birdeyeProvider.fetchDefiTradesPairSeekByTime({ + address: sampleParams.token, + after_time: sampleParams.after_time, + }); + elizaLogger.success("fetchDefiTradesPairSeekByTime: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiOHLCV"); + await birdeyeProvider.fetchDefiOHLCV({ + ...sampleParams, + type: "1D", + }); + elizaLogger.success("fetchDefiOHLCV: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiOHLCVPair"); + await birdeyeProvider.fetchDefiOHLCVPair({ + ...sampleParams, + type: "1D", + }); + elizaLogger.success("fetchDefiOHLCVPair: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiOHLCVBaseQuote"); + await birdeyeProvider.fetchDefiOHLCVBaseQuote({ + ...sampleParams, + type: "1D", + }); + elizaLogger.success("fetchDefiOHLCVBaseQuote: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchDefiPriceVolume"); + await birdeyeProvider.fetchDefiPriceVolume({ + address: sampleParams.token, + }); + elizaLogger.success("fetchDefiPriceVolume: SUCCESS!"); + await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchDefiPriceVolumeMulti_POST"); + // await birdeyeProvider.fetchDefiPriceVolumeMulti_POST({ + // list_address: sampleParams.token, + // }); + // elizaLogger.success("fetchDefiPriceVolumeMulti_POST: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchTokenList"); + await birdeyeProvider.fetchTokenList({ + ...sampleParams, + sort_by: "mc", + sort_type: "desc", + }); + elizaLogger.success("fetchTokenList: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenSecurityByAddress"); + await birdeyeProvider.fetchTokenSecurityByAddress({ + ...sampleParams, + }); + elizaLogger.success("fetchTokenSecurityByAddress: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenOverview"); + await birdeyeProvider.fetchTokenOverview({ ...sampleParams }); + elizaLogger.success("fetchTokenOverview: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenCreationInfo"); + await birdeyeProvider.fetchTokenCreationInfo({ ...sampleParams }); + elizaLogger.success("fetchTokenCreationInfo: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenTrending"); + await birdeyeProvider.fetchTokenTrending({ + ...sampleParams, + sort_by: "volume24hUSD", + sort_type: "desc", + }); + elizaLogger.success("fetchTokenTrending: SUCCESS!"); + await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchTokenListV2_POST"); + // await birdeyeProvider.fetchTokenListV2_POST({}); + // elizaLogger.success("fetchTokenListV2_POST: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchTokenNewListing"); + await birdeyeProvider.fetchTokenNewListing({ + time_to: new Date().getTime(), + meme_platform_enabled: true, + }); + elizaLogger.success("fetchTokenNewListing: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenTopTraders"); + await birdeyeProvider.fetchTokenTopTraders({ + ...sampleParams, + time_frame: "24h", + sort_type: "asc", + sort_by: "volume", + }); + elizaLogger.success("fetchTokenTopTraders: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenAllMarketsList"); + await birdeyeProvider.fetchTokenAllMarketsList({ + ...sampleParams, + time_frame: "12H", + sort_type: "asc", + sort_by: "volume24h", + }); + elizaLogger.success("fetchTokenAllMarketsList: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenMetadataSingle"); + await birdeyeProvider.fetchTokenMetadataSingle({ ...sampleParams }); + elizaLogger.success("fetchTokenMetadataSingle: SUCCESS!"); + await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchTokenMetadataMulti"); + // await birdeyeProvider.fetchTokenMetadataMulti({ ...sampleParams }); + // elizaLogger.success("fetchTokenMetadataMulti: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchTokenMarketData"); + await birdeyeProvider.fetchTokenMarketData({ ...sampleParams }); + elizaLogger.success("fetchTokenMarketData: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenTradeDataSingle"); + await birdeyeProvider.fetchTokenTradeDataSingle({ + ...sampleParams, + }); + elizaLogger.success("fetchTokenTradeDataSingle: SUCCESS!"); + await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchTokenTradeDataMultiple"); + // await birdeyeProvider.fetchTokenTradeDataMultiple({ + // ...sampleParams, + // }); + // elizaLogger.success("fetchTokenTradeDataMultiple: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchTokenHolders"); + await birdeyeProvider.fetchTokenHolders({ ...sampleParams }); + elizaLogger.success("fetchTokenHolders: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTokenMintBurn"); + await birdeyeProvider.fetchTokenMintBurn({ + ...sampleParams, + sort_by: "block_time", + sort_type: "desc", + type: "all", + }); + elizaLogger.success("fetchTokenMintBurn: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchWalletSupportedNetworks"); + await birdeyeProvider.fetchWalletSupportedNetworks(); + elizaLogger.success("fetchWalletSupportedNetworks: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchWalletPortfolio"); + await birdeyeProvider.fetchWalletPortfolio({ ...sampleParams }); + elizaLogger.success("fetchWalletPortfolio: SUCCESS!"); + await waitFor(500); + + // elizaLogger.info("fetchWalletPortfolioMultichain"); + // await birdeyeProvider.fetchWalletPortfolioMultichain({ + // ...sampleParams, + // }); + // elizaLogger.success("fetchWalletPortfolioMultichain: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchWalletTokenBalance"); + await birdeyeProvider.fetchWalletTokenBalance({ ...sampleParams }); + elizaLogger.success("fetchWalletTokenBalance: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchWalletTransactionHistory"); + await birdeyeProvider.fetchWalletTransactionHistory({ + ...sampleParams, + }); + elizaLogger.success("fetchWalletTransactionHistory: SUCCESS!"); + await waitFor(500); + + // elizaLogger.info("fetchWalletTransactionHistoryMultichain"); + // await birdeyeProvider.fetchWalletTransactionHistoryMultichain({ + // ...sampleParams, + // }); + // elizaLogger.success( + // "fetchWalletTransactionHistoryMultichain: SUCCESS!" + // ); + // await waitFor(500); + + elizaLogger.info("fetchWalletTransactionSimulate_POST"); + await birdeyeProvider.fetchWalletTransactionSimulate_POST({ + from: sampleParams.token, + to: sampleParams.token, + data: JSON.stringify({ test: "ok" }), + value: "100000", + }); + elizaLogger.success( + "fetchWalletTransactionSimulate_POST: SUCCESS!" + ); + await waitFor(500); + + elizaLogger.info("fetchTraderGainersLosers"); + await birdeyeProvider.fetchTraderGainersLosers({ + ...sampleParams, + type: "today", + sort_type: "asc", + }); + elizaLogger.success("fetchTraderGainersLosers: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchTraderTransactionsSeek"); + await birdeyeProvider.fetchTraderTransactionsSeek({ + ...sampleParams, + tx_type: "all", + before_time: undefined, + }); + elizaLogger.success("fetchTraderTransactionsSeek: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("fetchPairOverviewSingle"); + await birdeyeProvider.fetchPairOverviewSingle({ ...sampleParams }); + elizaLogger.success("fetchPairOverviewSingle: SUCCESS!"); + await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchMultiPairOverview"); + // await birdeyeProvider.fetchMultiPairOverview({ ...sampleParams }); + // elizaLogger.success("fetchMultiPairOverview: SUCCESS!"); + // await waitFor(500); + + // this endpoint is for enterprise users only + // elizaLogger.info("fetchPairOverviewMultiple"); + // await birdeyeProvider.fetchPairOverviewMultiple({ + // ...sampleParams, + // }); + // elizaLogger.success("fetchPairOverviewMultiple: SUCCESS!"); + // await waitFor(500); + + elizaLogger.info("fetchSearchTokenMarketData"); + await birdeyeProvider.fetchSearchTokenMarketData({ + ...sampleParams, + sort_type: "asc", + }); + elizaLogger.success("fetchSearchTokenMarketData: SUCCESS!"); + await waitFor(500); + + elizaLogger.info("All endpoints tested successfully"); + callback?.({ text: "All endpoints tested successfully!" }); + return true; + } catch (error) { + console.error("Error in testAllEndpointsAction:", error.message); + callback?.({ text: `Error: ${error.message}` }); + return false; + } + }, + validate: async (_runtime: IAgentRuntime, message: Memory) => { + // only run if explicitly triggered by user + return message.content.text.includes("BIRDEYE_TEST_ALL_ENDPOINTS"); + }, + examples: [ + [ + { + user: "user", + content: { + text: "I want you to BIRDEYE_TEST_ALL_ENDPOINTS", + action: "BIRDEYE_TEST_ALL_ENDPOINTS", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-birdeye/src/actions/token-search-address.ts b/packages/plugin-birdeye/src/actions/token-search-address.ts new file mode 100644 index 00000000000..783e5ae06d7 --- /dev/null +++ b/packages/plugin-birdeye/src/actions/token-search-address.ts @@ -0,0 +1,289 @@ +import { + Action, + ActionExample, + elizaLogger, + formatTimestamp, + IAgentRuntime, + Memory, + State, +} from "@elizaos/core"; +import { BirdeyeProvider } from "../birdeye"; +import { + TokenMarketDataResponse, + TokenOverviewResponse, + TokenSecurityResponse, + TokenTradeDataSingleResponse, +} from "../types/api/token"; +import { BaseAddress } from "../types/shared"; +import { + extractAddresses, + formatPercentChange, + formatPrice, + formatValue, + shortenAddress, +} from "../utils"; + +type TokenAddressSearchResult = { + overview: TokenOverviewResponse; + tradeData: TokenTradeDataSingleResponse; + security: TokenSecurityResponse; + marketData: TokenMarketDataResponse; +}; + +export const tokenSearchAddressAction = { + name: "TOKEN_SEARCH_ADDRESS", + similes: [ + "SEARCH_TOKEN_ADDRESS", + "FIND_TOKEN_ADDRESS", + "LOOKUP_TOKEN_ADDRESS", + "CHECK_TOKEN_ADDRESS", + "GET_TOKEN_BY_ADDRESS", + "TOKEN_ADDRESS_INFO", + "TOKEN_ADDRESS_LOOKUP", + "TOKEN_ADDRESS_SEARCH", + "TOKEN_ADDRESS_CHECK", + "TOKEN_ADDRESS_DETAILS", + "TOKEN_CONTRACT_SEARCH", + "TOKEN_CONTRACT_LOOKUP", + "TOKEN_CONTRACT_INFO", + "TOKEN_CONTRACT_CHECK", + "VERIFY_TOKEN_ADDRESS", + "VALIDATE_TOKEN_ADDRESS", + "GET_TOKEN_INFO", + "TOKEN_INFO", + "TOKEN_REPORT", + "TOKEN_ANALYSIS", + "TOKEN_OVERVIEW", + "TOKEN_SUMMARY", + "TOKEN_INSIGHT", + "TOKEN_DATA", + "TOKEN_STATS", + "TOKEN_METRICS", + "TOKEN_PROFILE", + "TOKEN_REVIEW", + "TOKEN_CHECK", + "TOKEN_LOOKUP", + "TOKEN_FIND", + "TOKEN_DISCOVER", + "TOKEN_EXPLORE", + ], + description: + "Search for detailed token information including security and trade data by address", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback?: any + ) => { + try { + const provider = new BirdeyeProvider(runtime.cacheManager); + + // get all contract addresses from the message + const addresses = extractAddresses(message.content.text); + + elizaLogger.info( + `Searching Birdeye provider for ${addresses.length} addresses` + ); + + // for each symbol, do a search in Birdeye. This will return a list of token results that may be amatch to the token symbol. + const results: TokenAddressSearchResult[] = await Promise.all( + addresses.map(async ({ address, chain: addressChain }) => { + // address detection can't distinguish between evm chains, so we currently only do address search on ETH for EVM addresses. Future support will be added for other chains if the user requests it. + const chain = + addressChain === "evm" ? "ethereum" : addressChain; + + const [overview, marketData, security, tradeData] = + await Promise.all([ + provider.fetchTokenOverview( + { + address, + }, + { + headers: { + "x-chain": chain, + }, + } + ), + provider.fetchTokenMarketData( + { + address, + }, + { + headers: { + "x-chain": chain, + }, + } + ), + provider.fetchTokenSecurityByAddress( + { + address, + }, + { + headers: { + "x-chain": chain, + }, + } + ), + provider.fetchTokenTradeDataSingle( + { + address, + }, + { + headers: { + "x-chain": chain, + }, + } + ), + ]); + + return { + overview, + marketData, + security, + tradeData, + }; + }) + ); + + console.log(results); + + const completeResults = `I performed a search for the token addresses you requested and found the following results:\n\n${results + .map( + (result, i) => + `${formatTokenReport(addresses[i], i, result)}` + ) + .join("\n\n")}`; + + callback?.({ text: completeResults }); + return true; + } catch (error) { + console.error("Error in searchTokens handler:", error.message); + callback?.({ text: `Error: ${error.message}` }); + return false; + } + }, + validate: async (_runtime: IAgentRuntime, message: Memory) => { + const addresses = extractAddresses(message.content.text); + return addresses.length > 0; + }, + examples: [ + [ + { + user: "user", + content: { + text: "Search for 0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + action: "TOKEN_SEARCH_ADDRESS", + }, + }, + { + user: "user", + content: { + text: "Look up contract So11111111111111111111111111111111111111112", + action: "TOKEN_ADDRESS_LOOKUP", + }, + }, + { + user: "user", + content: { + text: "Check this address: 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + action: "CHECK_TOKEN_ADDRESS", + }, + }, + { + user: "user", + content: { + text: "Get info for 0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + action: "TOKEN_ADDRESS_INFO", + }, + }, + { + user: "user", + content: { + text: "Analyze contract 0x514910771af9ca656af840dff83e8264ecf986ca", + action: "TOKEN_CONTRACT_SEARCH", + }, + }, + ], + ] as ActionExample[][], +} as Action; + +// take all the details of the results and present to the user +const formatTokenReport = ( + address: BaseAddress, + index: number, + result: TokenAddressSearchResult +) => { + let output = ``; + + if (result.overview?.data) { + output += `\n`; + output += `Token Overview:\n`; + output += `šŸ“ Name: ${result.overview.data.name}\n`; + output += result.overview.data.symbol + ? `šŸ”– Symbol: ${result.overview.data.symbol.toUpperCase()}\n` + : ""; + output += `šŸ”— Address: ${address.address}\n`; + output += `šŸ”¢ Decimals: ${result.overview.data.decimals}\n`; + output += ``; + if (result.overview.data.extensions) { + const ext = result.overview.data.extensions; + output += `šŸ”— Links & Info:\n`; + if (ext.website) output += ` ā€¢ Website: ${ext.website}\n`; + if (ext.twitter) output += ` ā€¢ Twitter: ${ext.twitter}\n`; + if (ext.telegram) output += ` ā€¢ Telegram: ${ext.telegram}\n`; + if (ext.discord) output += ` ā€¢ Discord: ${ext.discord}\n`; + if (ext.medium) output += ` ā€¢ Medium: ${ext.medium}\n`; + if (ext.coingeckoId) + output += ` ā€¢ CoinGecko ID: ${ext.coingeckoId}\n`; + if (ext.serumV3Usdc) + output += ` ā€¢ Serum V3 USDC: ${ext.serumV3Usdc}\n`; + if (ext.serumV3Usdt) + output += ` ā€¢ Serum V3 USDT: ${ext.serumV3Usdt}\n`; + } + output += `šŸ’§ Liquidity: ${formatValue(result.overview.data.liquidity)}\n`; + output += `ā° Last Trade Time: ${formatTimestamp(new Date(result.overview.data.lastTradeHumanTime).getTime() / 1000)}\n`; + output += `šŸ’µ Price: ${formatPrice(result.overview.data.price)}\n`; + output += `šŸ“œ Description: ${result.overview.data.extensions?.description ?? "N/A"}\n`; + } + + if (result.marketData?.data) { + output += `\n`; + output += `Market Data:\n`; + output += `šŸ’§ Liquidity: ${formatValue(result.marketData.data.liquidity)}\n`; + output += `šŸ’µ Price: ${formatPrice(result.marketData.data.price)}\n`; + output += `šŸ“¦ Supply: ${formatValue(result.marketData.data.supply)}\n`; + output += `šŸ’° Market Cap: ${formatValue(result.marketData.data.marketcap)}\n`; + output += `šŸ”„ Circulating Supply: ${formatValue(result.marketData.data.circulating_supply)}\n`; + output += `šŸ’° Circulating Market Cap: ${formatValue(result.marketData.data.circulating_marketcap)}\n`; + } + + if (result.tradeData?.data) { + output += `\n`; + output += `Trade Data:\n`; + output += `šŸ‘„ Holders: ${result.tradeData.data.holder}\n`; + output += `šŸ“Š Unique Wallets (24h): ${result.tradeData.data.unique_wallet_24h}\n`; + output += `šŸ“‰ Price Change (24h): ${formatPercentChange(result.tradeData.data.price_change_24h_percent)}\n`; + output += `šŸ’ø Volume (24h USD): ${formatValue(result.tradeData.data.volume_24h_usd)}\n`; + output += `šŸ’µ Current Price: $${formatPrice(result.tradeData.data.price)}\n`; + } + + if (result.security?.data) { + output += `\n`; + output += `Ownership Distribution:\n`; + output += `šŸ  Owner Address: ${shortenAddress(result.security.data.ownerAddress)}\n`; + output += `šŸ‘Øā€šŸ’¼ Creator Address: ${shortenAddress(result.security.data.creatorAddress)}\n`; + output += `šŸ“¦ Total Supply: ${formatValue(result.security.data.totalSupply)}\n`; + output += result.security.data.proxied + ? `šŸŒæ Mintable: ${result.security.data.mintable ?? "N/A"}\n` + : ""; + output += result.security.data.proxy + ? `šŸ”„ Proxied: ${result.security.data.proxy ?? "N/A"}\n` + : ""; + output += result.security.data.securityChecks + ? `šŸ” Security Checks: ${JSON.stringify(result.security.data.securityChecks)}\n` + : ""; + } + + return output ?? `No results found for ${address.address}`; +}; diff --git a/packages/plugin-birdeye/src/actions/token-search-symbol.ts b/packages/plugin-birdeye/src/actions/token-search-symbol.ts new file mode 100644 index 00000000000..bcf7e33c6d3 --- /dev/null +++ b/packages/plugin-birdeye/src/actions/token-search-symbol.ts @@ -0,0 +1,225 @@ +import { + Action, + ActionExample, + elizaLogger, + IAgentRuntime, + Memory, + State, +} from "@elizaos/core"; +import { BirdeyeProvider } from "../birdeye"; +import { TokenResult } from "../types/api/search"; +import { + extractSymbols, + formatPercentChange, + formatPrice, + formatValue, +} from "../utils"; + +// "strict" requires a $ prefix and will match $SOL, $ai16z, $BTC, etc. +// "loose" will match $SOL, SOL, SOLANA, etc. and does not require a $ prefix but may interpret any other acronyms as symbols to search for +const SYMBOL_SEARCH_MODE = "strict"; + +export const tokenSearchSymbolAction = { + name: "TOKEN_SEARCH_SYMBOL", + similes: [ + "SEARCH_TOKEN_SYMBOL", + "FIND_TOKEN_SYMBOL", + "LOOKUP_TOKEN_SYMBOL", + "CHECK_TOKEN_SYMBOL", + "GET_TOKEN_BY_SYMBOL", + "SYMBOL_SEARCH", + "SYMBOL_LOOKUP", + "SYMBOL_CHECK", + "TOKEN_SYMBOL_INFO", + "TOKEN_SYMBOL_DETAILS", + "TOKEN_SYMBOL_LOOKUP", + "TOKEN_SYMBOL_SEARCH", + "TOKEN_SYMBOL_CHECK", + "TOKEN_SYMBOL_QUERY", + "TOKEN_SYMBOL_FIND", + "GET_TOKEN_INFO", + "TOKEN_INFO", + "TOKEN_REPORT", + "TOKEN_ANALYSIS", + "TOKEN_OVERVIEW", + "TOKEN_SUMMARY", + "TOKEN_INSIGHT", + "TOKEN_DATA", + "TOKEN_STATS", + "TOKEN_METRICS", + "TOKEN_PROFILE", + "TOKEN_REVIEW", + "TOKEN_CHECK", + "TOKEN_LOOKUP", + "TOKEN_FIND", + "TOKEN_DISCOVER", + "TOKEN_EXPLORE", + ], + description: + "Search for detailed token information including security and trade data by symbol", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback?: any + ) => { + try { + const provider = new BirdeyeProvider(runtime.cacheManager); + + // get all symbols from the message that match (i.e. $SOL, $ETH, $BTC, etc.). If you want to match more loosely, use "loose" instead of "strict" and it will match $SOL, SOL, SOLANA, etc. + const symbols = extractSymbols( + message.content.text, + SYMBOL_SEARCH_MODE + ); + + elizaLogger.info( + `Searching Birdeye provider for ${symbols.length} symbols` + ); + + // for each symbol, do a search in Birdeye. This will return a list of token results that may be amatch to the token symbol. + const results = await Promise.all( + symbols.map((symbol) => + provider.fetchSearchTokenMarketData({ + keyword: symbol, + sort_by: "volume_24h_usd", + sort_type: "desc", + chain: "all", + limit: 5, + }) + ) + ); + + // get filter the resuls to only include the token results and then filter the results to only include the ones that match the symbol + const validResults = results.map((r, i) => + r.data.items + .filter((item) => item.type === "token" && item.result) + .flatMap((item) => + (item.result as TokenResult[]).filter( + (r) => + r.symbol?.toLowerCase() === + symbols[i].toLowerCase() + ) + ) + ) as TokenResult[][]; + + if (validResults.length === 0) { + return true; + } + + const completeResults = `I performed a search for the token symbols you requested and found the following results (for more details search by contract address):\n\n${validResults + .map( + (result, i) => + `${formatTokenSummary(symbols[i], i, result)}` + ) + .join("\n")}`; + + callback?.({ text: completeResults }); + return true; + } catch (error) { + console.error("Error in searchTokens handler:", error.message); + callback?.({ text: `Error: ${error.message}` }); + return false; + } + }, + validate: async (_runtime: IAgentRuntime, message: Memory) => { + const symbols = extractSymbols( + message.content.text, + SYMBOL_SEARCH_MODE + ); + return symbols.length > 0; + }, + examples: [ + [ + { + user: "user", + content: { + text: "Search for $SOL and $ETH", + action: "SEARCH_TOKENS", + }, + }, + { + user: "user", + content: { + text: "Find information about $BTC", + action: "TOKEN_SEARCH", + }, + }, + { + user: "user", + content: { + text: "Look up $WETH token", + action: "LOOKUP_TOKENS", + }, + }, + { + user: "user", + content: { + text: "Tell me about SOL", + action: "CHECK_TOKEN", + }, + }, + { + user: "user", + content: { + text: "Give me details on $ADA", + action: "TOKEN_DETAILS", + }, + }, + { + user: "user", + content: { + text: "What can you tell me about $DOGE?", + action: "TOKEN_INFO", + }, + }, + { + user: "user", + content: { + text: "I need a report on $XRP", + action: "TOKEN_REPORT", + }, + }, + { + user: "user", + content: { + text: "Analyze $BNB for me", + action: "TOKEN_ANALYSIS", + }, + }, + { + user: "user", + content: { + text: "Overview of $LTC", + action: "TOKEN_OVERVIEW", + }, + }, + ], + ] as ActionExample[][], +} as Action; + +const formatTokenSummary = ( + symbol: string, + index: number, + tokens: TokenResult[] +) => { + return tokens + .map((token, i) => { + let output = ``; + if (i === 0) { + output += `Search Results for ${symbol}:\n\n`; + } + output += `Search Result #${tokens.length > 0 ? i + 1 : ""}:\n`; + output += `šŸ”– Symbol: $${token.symbol.toUpperCase()}\n`; + output += `šŸ”— Address: ${token.address}\n`; + output += `šŸŒ Network: ${token.network.toUpperCase()}\n`; + output += `šŸ’µ Price: ${formatPrice(token.price)} (${formatPercentChange(token.price_change_24h_percent)})\n`; + output += `šŸ’ø Volume (24h USD): ${formatValue(token.volume_24h_usd)}\n`; + output += token.market_cap + ? `šŸ’° Market Cap: ${formatValue(token.market_cap)}\n` + : ""; + output += token.fdv ? `šŸŒŠ FDV: ${formatValue(token.fdv)}\n` : ""; + return output; + }) + .join("\n"); +}; diff --git a/packages/plugin-birdeye/src/actions/wallet-search-address.ts b/packages/plugin-birdeye/src/actions/wallet-search-address.ts new file mode 100644 index 00000000000..1ec2fc20eec --- /dev/null +++ b/packages/plugin-birdeye/src/actions/wallet-search-address.ts @@ -0,0 +1,176 @@ +import { + Action, + ActionExample, + elizaLogger, + IAgentRuntime, + Memory, + State, +} from "@elizaos/core"; +import { BirdeyeProvider } from "../birdeye"; +import { WalletPortfolioResponse } from "../types/api/wallet"; +import { BaseAddress } from "../types/shared"; +import { extractAddresses } from "../utils"; + +export const walletSearchAddressAction = { + name: "WALLET_SEARCH_ADDRESS", + similes: [ + "SEARCH_WALLET_ADDRESS", + "FIND_WALLET_ADDRESS", + "LOOKUP_WALLET_ADDRESS", + "CHECK_WALLET_ADDRESS", + "GET_WALLET_BY_ADDRESS", + "WALLET_ADDRESS_INFO", + "WALLET_ADDRESS_LOOKUP", + "WALLET_ADDRESS_SEARCH", + "WALLET_ADDRESS_CHECK", + "WALLET_ADDRESS_DETAILS", + "WALLET_CONTRACT_SEARCH", + "WALLET_CONTRACT_LOOKUP", + "WALLET_CONTRACT_INFO", + "WALLET_CONTRACT_CHECK", + "VERIFY_WALLET_ADDRESS", + "VALIDATE_WALLET_ADDRESS", + "GET_WALLET_INFO", + "WALLET_INFO", + "WALLET_REPORT", + "WALLET_ANALYSIS", + "WALLET_OVERVIEW", + "WALLET_SUMMARY", + "WALLET_INSIGHT", + "WALLET_DATA", + "WALLET_STATS", + "WALLET_METRICS", + "WALLET_PROFILE", + "WALLET_REVIEW", + "WALLET_CHECK", + "WALLET_LOOKUP", + "WALLET_FIND", + "WALLET_DISCOVER", + "WALLET_EXPLORE", + ], + description: + "Search for detailed wallet information including portfolio and transaction data by address", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: any, + callback?: any + ) => { + try { + const provider = new BirdeyeProvider(runtime.cacheManager); + + // get all wallet addresses from the message + const addresses = extractAddresses(message.content.text); + + elizaLogger.info( + `Searching Birdeye provider for ${addresses.length} addresses` + ); + + // for each symbol, do a search in Birdeye. This will return a list of token results that may be amatch to the token symbol. + const results: WalletPortfolioResponse[] = await Promise.all( + addresses.map(async ({ address, chain: addressChain }) => { + // address detection can't distinguish between evm chains, so we currently only do address search on ETH for EVM addresses. Future support will be added for other chains if the user requests it. + const chain = + addressChain === "evm" ? "ethereum" : addressChain; + return provider.fetchWalletPortfolio( + { + wallet: address, + }, + { + headers: { + chain: chain, + }, + } + ); + }) + ); + + console.log(results); + + const completeResults = `I performed a search for the wallet addresses you requested and found the following results:\n\n${results + .map( + (result, i) => + `${formatWalletReport(addresses[i], results.length, i, result)}` + ) + .join("\n\n")}`; + + callback?.({ text: completeResults }); + return true; + } catch (error) { + console.error("Error in searchTokens handler:", error.message); + callback?.({ text: `Error: ${error.message}` }); + return false; + } + }, + validate: async (_runtime: IAgentRuntime, message: Memory) => { + const addresses = extractAddresses(message.content.text); + return addresses.length > 0; + }, + examples: [ + [ + { + user: "user", + content: { + text: "Search wallet 0x1234567890abcdef1234567890abcdef12345678", + action: "WALLET_SEARCH_ADDRESS", + }, + }, + { + user: "user", + content: { + text: "Look up wallet address HN7cABqLq46Es1jh92dQQisAq662SmxELLLsHHe4YWrH", + action: "WALLET_ADDRESS_LOOKUP", + }, + }, + { + user: "user", + content: { + text: "Check this address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + action: "CHECK_WALLET_ADDRESS", + }, + }, + { + user: "user", + content: { + text: "Get wallet info for 5yBYpGQRHPz4i5FkVnP9h9VTJBMnwgHRe5L5gw2bwp9q", + action: "WALLET_INFO", + }, + }, + { + user: "user", + content: { + text: "Show me portfolio for 0x3cD751E6b0078Be393132286c442345e5DC49699", + action: "WALLET_OVERVIEW", + }, + }, + ], + ] as ActionExample[][], +} as Action; + +// take all the details of the results and present to the user +const formatWalletReport = ( + address: BaseAddress, + totalResults: number, + index: number, + result: WalletPortfolioResponse +) => { + const tokens = result.data.items.slice(0, 10) || []; + const totalValue = tokens.reduce( + (sum, token) => sum + (token.valueUsd || 0), + 0 + ); + + let header = `Wallet Result ${totalResults > 1 ? `#${index + 1}` : ""}\n`; + header += `šŸ‘› Address ${address.address}*\n`; + header += `šŸ’° Total Value: $${totalValue.toLocaleString()}\n`; + header += `šŸ”– Top Holdings:`; + const tokenList = tokens + .map( + (token) => + `ā€¢ $${token.symbol.toUpperCase()}: $${token.valueUsd?.toLocaleString()} (${token.uiAmount?.toFixed(4)} tokens)` + ) + .join("\n"); + + return `${header}\n${tokenList}`; +}; diff --git a/packages/plugin-birdeye/src/birdeye.ts b/packages/plugin-birdeye/src/birdeye.ts new file mode 100644 index 00000000000..a1e55bce88e --- /dev/null +++ b/packages/plugin-birdeye/src/birdeye.ts @@ -0,0 +1,803 @@ +import { elizaLogger, ICacheManager, settings } from "@elizaos/core"; +import NodeCache from "node-cache"; +import * as path from "path"; +import { + API_BASE_URL, + BIRDEYE_ENDPOINTS, + DEFAULT_MAX_RETRIES, + DEFAULT_SUPPORTED_SYMBOLS, + RETRY_DELAY_MS, +} from "./constants"; +import { BirdeyeApiParams, BirdeyeApiResponse } from "./types/api/common"; +import { + BaseQuoteParams, + BaseQuoteResponse, + DefiHistoryPriceParams, + DefiHistoryPriceResponse, + DefiMultiPriceParams, + DefiMultiPriceParamsPOST, + DefiMultiPriceResponse, + DefiNetworksResponse, + DefiPriceParams, + DefiPriceResponse, + DefiTradesTokenParams, + DefiTradesTokenResponse, + HistoricalPriceUnixParams, + HistoricalPriceUnixResponse, + MultiPriceVolumeParams, + MultiPriceVolumeResponse, + OHLCVParams, + OHLCVResponse, + PriceVolumeParams, + PriceVolumeResponse, +} from "./types/api/defi"; +import { + OHLCVPairParams, + OHLCVPairResponse, + PairOverviewMultiParams, + PairOverviewMultiResponse, + PairOverviewSingleParams, + PairOverviewSingleResponse, +} from "./types/api/pair"; +import { + TokenMarketSearchParams, + TokenMarketSearchResponse, +} from "./types/api/search"; +import { + AllMarketsParams, + AllMarketsResponse, + MintBurnParams, + MintBurnResponse, + NewListingParams, + NewListingResponse, + TokenCreationInfoParams, + TokenCreationInfoResponse, + TokenHoldersParams, + TokenHoldersResponse, + TokenListParams, + TokenListResponse, + TokenListV2Response, + TokenMarketDataParams, + TokenMarketDataResponse, + TokenMetadataMultiParams, + TokenMetadataMultiResponse, + TokenMetadataSingleParams, + TokenMetadataSingleResponse, + TokenOverviewParams, + TokenOverviewResponse, + TokenSecurityParams, + TokenSecurityResponse, + TokenTradeDataMultiParams, + TokenTradeDataMultiResponse, + TokenTradeDataSingleParams, + TokenTradeDataSingleResponse, + TokenTrendingParams, + TokenTrendingResponse, + TopTradersParams, + TopTradersResponse, +} from "./types/api/token"; +import { + GainersLosersParams, + GainersLosersResponse, + TraderTransactionsSeekParams, + TraderTransactionsSeekResponse, +} from "./types/api/trader"; +import { + WalletPortfolioMultichainParams, + WalletPortfolioMultichainResponse, + WalletPortfolioParams, + WalletPortfolioResponse, + WalletSimulationParams, + WalletSimulationResponse, + WalletTokenBalanceParams, + WalletTokenBalanceResponse, + WalletTransactionHistoryMultichainParams, + WalletTransactionHistoryMultichainResponse, + WalletTransactionHistoryParams, + WalletTransactionHistoryResponse, +} from "./types/api/wallet"; +import { convertToStringParams, waitFor } from "./utils"; + +type FetchParams = T & { + headers?: Record; +}; + +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; + } + + /* + * COMMON FETCH FUNCTIONS + */ + private async fetchWithRetry( + url: string, + options: RequestInit = {} + ): Promise { + let attempts = 0; + + // allow the user to override the chain + const chain = + options.headers?.["x-chain"] || settings.BIRDEYE_CHAIN || "solana"; + + while (attempts < this.maxRetries) { + attempts++; + try { + const resp = await fetch(url, { + ...options, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "x-chain": chain, + "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 rawData = await resp.json(); + // If the response already has data and success fields, return it + if ( + rawData.data !== undefined && + rawData.success !== undefined + ) { + return rawData as T; + } + // Otherwise wrap the response in the expected format + return { + data: rawData, + success: true, + } as T; + } catch (error) { + if (attempts === this.maxRetries) { + // failed after all + throw error; + } + await waitFor(RETRY_DELAY_MS); + } + } + } + + private async fetchWithCacheAndRetry({ + url, + params, + headers, + method = "GET", + }: { + url: string; + params?: BirdeyeApiParams; + headers?: Record; + method?: "GET" | "POST"; + }): Promise { + const stringParams = convertToStringParams(params); + const fullUrl = `${API_BASE_URL}${url}`; + const cacheKey = + method === "GET" + ? `${url}?${new URLSearchParams(stringParams)}` + : `${url}:${JSON.stringify(params)}`; + + const val = await this.readFromCache(cacheKey); + if (val) return val as T; + + const urlWithParams = + method === "GET" && params + ? `${fullUrl}?${new URLSearchParams(stringParams)}` + : fullUrl; + + elizaLogger.info(`Birdeye fetch: ${urlWithParams}`); + + const data = await this.fetchWithRetry(urlWithParams, { + method, + headers, + ...(method === "POST" && + params && { body: JSON.stringify(params) }), + }); + + await this.writeToCache(cacheKey, data); + return data as T; + } + + /* + * DEFI FETCH FUNCTIONS + */ + + // Get a list of all supported networks. + public async fetchDefiSupportedNetworks() { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.networks, + }); + } + + // Get price update of a token. + public async fetchDefiPrice( + params: DefiPriceParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.price, + params, + headers: options.headers, + }); + } + + // Get price updates of multiple tokens in a single API call. Maximum 100 tokens + public async fetchDefiPriceMultiple( + params: DefiMultiPriceParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.price_multi, + params, + headers: options.headers, + }); + } + + // Get price updates of multiple tokens in a single API call. Maximum 100 tokens + public async fetchDefiPriceMultiple_POST( + params: DefiMultiPriceParamsPOST, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.price_multi_POST, + params, + headers: options.headers, + method: "POST", + }); + } + + // Get historical price line chart of a token. + public async fetchDefiPriceHistorical( + params: DefiHistoryPriceParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.history_price, + params, + headers: options.headers, + }); + } + + // Get historical price by unix timestamp + public async fetchDefiPriceHistoricalByUnixTime( + params: HistoricalPriceUnixParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.historical_price_unix, + params, + headers: options.headers, + }); + } + + // Get list of trades of a certain token. + public async fetchDefiTradesToken( + params: DefiTradesTokenParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.trades_token, + params, + headers: options.headers, + }); + } + + // Get list of trades of a certain pair or market. + public async fetchDefiTradesPair( + params: DefiTradesTokenParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.trades_token, + params, + headers: options.headers, + }); + } + + // Get list of trades of a token with time bound option. + public async fetchDefiTradesTokenSeekByTime( + params: DefiTradesTokenParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.trades_token_seek, + params, + headers: options.headers, + }); + } + + // Get list of trades of a certain pair or market with time bound option. + public async fetchDefiTradesPairSeekByTime( + params: DefiTradesTokenParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.trades_pair_seek, + params, + headers: options.headers, + }); + } + + // Get OHLCV price of a token. + public async fetchDefiOHLCV( + params: OHLCVParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.ohlcv, + params, + headers: options.headers, + }); + } + + // Get OHLCV price of a pair. + public async fetchDefiOHLCVPair( + params: OHLCVPairParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.ohlcv_pair, + params, + headers: options.headers, + }); + } + + // Get OHLCV price of a base-quote pair. + public async fetchDefiOHLCVBaseQuote( + params: BaseQuoteParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.ohlcv_base_quote, + params, + headers: options.headers, + }); + } + + // Get price and volume of a token. + public async fetchDefiPriceVolume( + params: PriceVolumeParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.price_volume, + params, + headers: options.headers, + }); + } + + // Get price and volume updates of maximum 50 tokens + public async fetchDefiPriceVolumeMulti_POST( + params: MultiPriceVolumeParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.price_volume_multi_POST, + params, + headers: options.headers, + method: "POST", + }); + } + + /* + * TOKEN FETCH FUNCTIONS + */ + + // Get token list of any supported chains. + public async fetchTokenList( + params: TokenListParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.list_all, + params, + headers: options.headers, + }); + } + + // Get token security of any supported chains. + public async fetchTokenSecurityByAddress( + params: TokenSecurityParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.security, + params, + headers: options.headers, + }); + } + + // Get overview of a token. + public async fetchTokenOverview( + params: TokenOverviewParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.overview, + params, + headers: options.headers, + }); + } + + // Get creation info of token + public async fetchTokenCreationInfo( + params: TokenCreationInfoParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.creation_info, + params, + headers: options.headers, + }); + } + + // Retrieve a dynamic and up-to-date list of trending tokens based on specified sorting criteria. + public async fetchTokenTrending( + params?: TokenTrendingParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.trending, + params, + headers: options.headers, + }); + } + + // This endpoint facilitates the retrieval of a list of tokens on a specified blockchain network. This upgraded version is exclusive to business and enterprise packages. By simply including the header for the requested blockchain without any query parameters, business and enterprise users can get the full list of tokens on the specified blockchain in the URL returned in the response. This removes the need for the limit response of the previous version and reduces the workload of making multiple calls. + public async fetchTokenListV2_POST( + params: FetchParams> + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.list_all_v2_POST, + params, + headers: params.headers, + method: "POST", + }); + } + + // Get newly listed tokens of any supported chains. + public async fetchTokenNewListing( + params?: NewListingParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.new_listing, + params, + headers: options?.headers, + }); + } + + // Get top traders of given token. + public async fetchTokenTopTraders( + params: TopTradersParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.top_traders, + params, + headers: options.headers, + }); + } + + // The API provides detailed information about the markets for a specific cryptocurrency token on a specified blockchain. Users can retrieve data for one or multiple markets related to a single token. This endpoint requires the specification of a token address and the blockchain to filter results. Additionally, it supports optional query parameters such as offset, limit, and required sorting by liquidity or sort type (ascending or descending) to refine the output. + public async fetchTokenAllMarketsList( + params: AllMarketsParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.all_markets, + params, + headers: options.headers, + }); + } + + // Get metadata of single token + public async fetchTokenMetadataSingle( + params: TokenMetadataSingleParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.metadata_single, + params, + headers: options.headers, + }); + } + + // Get metadata of multiple tokens + public async fetchTokenMetadataMulti( + params: TokenMetadataMultiParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.metadata_multi, + params, + headers: options.headers, + }); + } + + // Get market data of single token + public async fetchTokenMarketData( + params: TokenMarketDataParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.market_data, + params, + headers: options.headers, + }); + } + + // Get trade data of single token + public async fetchTokenTradeDataSingle( + params: TokenTradeDataSingleParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.trade_data_single, + params, + headers: options.headers, + }); + } + + // Get trade data of multiple tokens + public async fetchTokenTradeDataMultiple( + params: TokenTradeDataMultiParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.trade_data_multi, + params, + headers: options.headers, + }); + } + + // Get top holder list of the given token + public async fetchTokenHolders( + params: TokenHoldersParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.holders, + params, + headers: options.headers, + }); + } + + // Get mint/burn transaction list of the given token. Only support solana currently + public async fetchTokenMintBurn( + params: MintBurnParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.token.mint_burn, + params, + headers: options.headers, + }); + } + + /* + * WALLET FETCH FUNCTIONS + */ + public async fetchWalletSupportedNetworks( + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.defi.networks, + headers: options.headers, + }); + } + + public async fetchWalletPortfolio( + params: WalletPortfolioParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.wallet.portfolio, + params, + headers: options.headers, + }); + } + + /** + * @deprecated This endpoint will be decommissioned on Feb 1st, 2025. + */ + public async fetchWalletPortfolioMultichain( + params: WalletPortfolioMultichainParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.wallet.portfolio_multichain, + params, + headers: options.headers, + }); + } + + public async fetchWalletTokenBalance( + params: WalletTokenBalanceParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.wallet.token_balance, + params, + headers: options.headers, + }); + } + + public async fetchWalletTransactionHistory( + params: WalletTransactionHistoryParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.wallet.transaction_history, + params, + headers: options.headers, + }); + } + + /** + * @deprecated This endpoint will be decommissioned on Feb 1st, 2025. + */ + public async fetchWalletTransactionHistoryMultichain( + params: WalletTransactionHistoryMultichainParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry( + { + url: BIRDEYE_ENDPOINTS.wallet.transaction_history_multichain, + params, + headers: options.headers, + } + ); + } + + public async fetchWalletTransactionSimulate_POST( + params: WalletSimulationParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.wallet.transaction_simulation_POST, + params, + headers: options.headers, + method: "POST", + }); + } + + /* + * TRADER FETCH FUNCTIONS + */ + + // The API provides detailed information top gainers/losers + public async fetchTraderGainersLosers( + params: GainersLosersParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.trader.gainers_losers, + params, + headers: options.headers, + }); + } + + // Get list of trades of a trader with time bound option. + public async fetchTraderTransactionsSeek( + params: TraderTransactionsSeekParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.trader.trades_seek, + params, + headers: options.headers, + }); + } + + /* + * PAIR FETCH FUNCTIONS + */ + public async fetchPairOverviewSingle( + params: PairOverviewSingleParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.pair.overview_single, + params, + headers: options.headers, + }); + } + + // Get overview of multiple pairs + public async fetchMultiPairOverview( + params: PairOverviewMultiParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.pair.overview_multi, + params, + headers: options.headers, + }); + } + + public async fetchPairOverviewMultiple( + params: PairOverviewMultiParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.pair.overview_multi, + params, + headers: options.headers, + }); + } + + /* + * SEARCH FETCH FUNCTIONS + */ + public async fetchSearchTokenMarketData( + params: TokenMarketSearchParams, + options: { headers?: Record } = {} + ) { + return this.fetchWithCacheAndRetry({ + url: BIRDEYE_ENDPOINTS.search.token_market, + params, + headers: options.headers, + }); + } +} diff --git a/packages/plugin-birdeye/src/constants.ts b/packages/plugin-birdeye/src/constants.ts new file mode 100644 index 00000000000..e9b74ad1856 --- /dev/null +++ b/packages/plugin-birdeye/src/constants.ts @@ -0,0 +1,71 @@ +export const DEFAULT_MAX_RETRIES = 3; + +export const DEFAULT_SUPPORTED_SYMBOLS = { + SOL: "So11111111111111111111111111111111111111112", + BTC: "qfnqNqs3nCAHjnyCgLRDbBtq4p2MtHZxw8YjSyYhPoL", + ETH: "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", + Example: "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh", +}; + +export const API_BASE_URL = "https://public-api.birdeye.so"; + +export const RETRY_DELAY_MS = 2_000; + +export const BIRDEYE_ENDPOINTS = { + defi: { + networks: "/defi/networks", // https://docs.birdeye.so/reference/get_defi-networks + price: "/defi/price", // https://docs.birdeye.so/reference/get_defi-price + price_multi: "/defi/multi_price", // https://docs.birdeye.so/reference/get_defi-multi-price + price_multi_POST: "/defi/multi_price", // https://docs.birdeye.so/reference/post_defi-multi-price + history_price: "/defi/history_price", // https://docs.birdeye.so/reference/get_defi-history-price + historical_price_unix: "/defi/historical_price_unix", // https://docs.birdeye.so/reference/get_defi-historical-price-unix + trades_token: "/defi/txs/token", // https://docs.birdeye.so/reference/get_defi-txs-token + trades_pair: "/defi/txs/pair", // https://docs.birdeye.so/reference/get_defi-txs-pair + trades_token_seek: "/defi/txs/token/seek_by_time", // https://docs.birdeye.so/reference/get_defi-txs-token-seek-by-time + trades_pair_seek: "/defi/txs/pair/seek_by_time", // https://docs.birdeye.so/reference/get_defi-txs-pair-seek-by-time + ohlcv: "/defi/ohlcv", // https://docs.birdeye.so/reference/get_defi-ohlcv + ohlcv_pair: "/defi/ohlcv/pair", // https://docs.birdeye.so/reference/get_defi-ohlcv-pair + ohlcv_base_quote: "/defi/ohlcv/base_quote", // https://docs.birdeye.so/reference/get_defi-ohlcv-base-quote + price_volume: "/defi/price_volume/single", // https://docs.birdeye.so/reference/get_defi-price-volume-single + price_volume_multi: "/defi/price_volume/multi", // https://docs.birdeye.so/reference/get_defi-price-volume-multi + price_volume_multi_POST: "/defi/price_volume/multi", // https://docs.birdeye.so/reference/post_defi-price-volume-multi + }, + token: { + list_all: "/defi/tokenlist", // https://docs.birdeye.so/reference/get_defi-tokenlist + security: "/defi/token_security", // https://docs.birdeye.so/reference/get_defi-token-security + overview: "/defi/token_overview", // https://docs.birdeye.so/reference/get_defi-token-overview + creation_info: "/defi/token_creation_info", // https://docs.birdeye.so/reference/get_defi-token-creation-info + trending: "/defi/token_trending", // https://docs.birdeye.so/reference/get_defi-token-trending + list_all_v2_POST: "/defi/v2/tokens/all", // https://docs.birdeye.so/reference/post_defi-v2-tokens-all + new_listing: "/defi/v2/tokens/new_listing", // https://docs.birdeye.so/reference/get_defi-v2-tokens-new-listing + top_traders: "/defi/v2/tokens/top_traders", // https://docs.birdeye.so/reference/get_defi-v2-tokens-top-traders + all_markets: "/defi/v2/markets", // https://docs.birdeye.so/reference/get_defi-v2-markets + metadata_single: "/defi/v3/token/meta-data/single", // https://docs.birdeye.so/reference/get_defi-v3-token-meta-data-single + metadata_multi: "/defi/v3/token/meta-data/multiple", // https://docs.birdeye.so/reference/get_defi-v3-token-meta-data-multiple + market_data: "/defi/v3/token/market-data", // https://docs.birdeye.so/reference/get_defi-v3-token-market-data + trade_data_single: "/defi/v3/token/trade-data/single", // https://docs.birdeye.so/reference/get_defi-v3-token-trade-data-single + trade_data_multi: "/defi/v3/token/trade-data/multiple", // https://docs.birdeye.so/reference/get_defi-v3-token-trade-data-multiple + holders: "/defi/v3/token/holder", // https://docs.birdeye.so/reference/get_defi-v3-token-holder + mint_burn: "/defi/v3/token/mint-burn-txs", // https://docs.birdeye.so/reference/get_defi-v3-token-mint-burn-txs + }, + wallet: { + networks: "/v1/wallet/list_supported_chain", // https://docs.birdeye.so/reference/get_v1-wallet-list-supported-chain + portfolio: "/v1/wallet/token_list", // https://docs.birdeye.so/reference/get_v1-wallet-token-list + portfolio_multichain: "/v1/wallet/multichain_token_list", // https://docs.birdeye.so/reference/get_v1-wallet-multichain-token-list + token_balance: "/v1/wallet/token_balance", // https://docs.birdeye.so/reference/get_v1-wallet-token-balance + transaction_history: "/v1/wallet/tx_list", // https://docs.birdeye.so/reference/get_v1-wallet-tx-list + transaction_history_multichain: "/v1/wallet/multichain_tx_list", // https://docs.birdeye.so/reference/get_v1-wallet-multichain-tx-list + transaction_simulation_POST: "/v1/wallet/simulate", // https://docs.birdeye.so/reference/post_v1-wallet-simulate + }, + trader: { + gainers_losers: "/trader/gainers-losers", // https://docs.birdeye.so/reference/get_trader-gainers-losers + trades_seek: "/trader/txs/seek_by_time", // https://docs.birdeye.so/reference/get_trader-txs-seek-by-time + }, + pair: { + overview_multi: "/defi/v3/pair/overview/multiple", // https://docs.birdeye.so/reference/get_defi-v3-pair-overview-multiple + overview_single: "/defi/v3/pair/overview/single", // https://docs.birdeye.so/reference/get_defi-v3-pair-overview-single + }, + search: { + token_market: "/defi/v3/search", // https://docs.birdeye.so/reference/get_defi-v3-search + }, +}; diff --git a/packages/plugin-birdeye/src/index.ts b/packages/plugin-birdeye/src/index.ts new file mode 100644 index 00000000000..15983520228 --- /dev/null +++ b/packages/plugin-birdeye/src/index.ts @@ -0,0 +1,20 @@ +import { Plugin } from "@elizaos/core"; +import { tokenSearchAddressAction } from "./actions/token-search-address"; +import { tokenSearchSymbolAction } from "./actions/token-search-symbol"; +import { walletSearchAddressAction } from "./actions/wallet-search-address"; +import { agentPortfolioProvider } from "./providers/agent-portfolio-provider"; + +export const birdeyePlugin: Plugin = { + name: "birdeye", + description: "Birdeye Plugin for token data and analytics", + actions: [ + tokenSearchSymbolAction, + tokenSearchAddressAction, + walletSearchAddressAction, + // testAllEndpointsAction, // this action can be used to optionally test all endpoints + ], + evaluators: [], + providers: [agentPortfolioProvider], +}; + +export default birdeyePlugin; diff --git a/packages/plugin-birdeye/src/providers/agent-portfolio-provider.ts b/packages/plugin-birdeye/src/providers/agent-portfolio-provider.ts new file mode 100644 index 00000000000..04460780f2a --- /dev/null +++ b/packages/plugin-birdeye/src/providers/agent-portfolio-provider.ts @@ -0,0 +1,52 @@ +import { IAgentRuntime, Memory, Provider, State } from "@elizaos/core"; +import { BirdeyeProvider } from "../birdeye"; +import { extractChain, formatPortfolio } from "../utils"; + +/** + * Agent portfolio data provider that queries Birdeye API for the agent's wallet address. + * When a wallet address is set, this provider fetches portfolio data to give the agent + * context about the agent's holdings when responding to queries. + * + * The provider: + * - Validates the agent's wallet address + * - Fetches current portfolio data from Birdeye including token balances and metadata + * - Makes this portfolio context available to the agent for responding to user queries + * about their holdings, token values, etc. + */ +export const agentPortfolioProvider: Provider = { + get: async ( + runtime: IAgentRuntime, + _message: Memory, + _state?: State + ): Promise => { + try { + const provider = new BirdeyeProvider(runtime.cacheManager); + const walletAddr = runtime.getSetting("BIRDEYE_WALLET_ADDR"); + + if (!walletAddr) { + console.warn("No Birdeye wallet was specified"); + return ""; + } + + const chain = extractChain(walletAddr); + + const resp = await provider.fetchWalletPortfolio( + { + wallet: walletAddr, + }, + { + headers: { + chain, + }, + } + ); + + const portfolioText = formatPortfolio(resp); + + return `This is your wallet address: ${walletAddr}\n\nThis is your portfolio: [${portfolioText}]`; + } 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/src/tests/birdeye.test.ts b/packages/plugin-birdeye/src/tests/birdeye.test.ts new file mode 100644 index 00000000000..cb9ac8d9f87 --- /dev/null +++ b/packages/plugin-birdeye/src/tests/birdeye.test.ts @@ -0,0 +1,510 @@ +import { ICacheManager } from "@elizaos/core"; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from "vitest"; +import { BirdeyeProvider } from "../birdeye"; +import { + API_BASE_URL, + BIRDEYE_ENDPOINTS, + DEFAULT_SUPPORTED_SYMBOLS, +} from "../constants"; +import { convertToStringParams } from "../utils"; + +describe("BirdeyeProvider", () => { + let cacheManager: ICacheManager; + let provider: BirdeyeProvider; + + beforeEach(() => { + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + provider = new BirdeyeProvider(cacheManager); + global.fetch = vi.fn(); + + vi.mock("@elizaos/core", () => ({ + settings: { + get: vi.fn().mockImplementation((key) => { + if (key === "BIRDEYE_API_KEY") + return process.env.BIRDEYE_API_KEY || "test-api-key"; + if (key === "BIRDEYE_CHAIN") return "solana"; + return undefined; + }), + }, + ICacheManager: vi.fn(), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + const mockSuccessResponse = (data: any) => { + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => ({ data, success: true }), + }); + }; + + const expectFetchCall = ( + endpoint: string, + params?: any, + method = "GET" + ) => { + const url = `${API_BASE_URL}${endpoint}${ + params && method === "GET" + ? `?${new URLSearchParams(convertToStringParams(params))}` + : "" + }`; + + expect(fetch).toHaveBeenCalledWith(url, expect.anything()); + }; + + describe("Defi Endpoints", () => { + it("should fetch supported networks", async () => { + const mockData = { chains: ["solana", "ethereum"] }; + mockSuccessResponse(mockData); + const result = await provider.fetchDefiSupportedNetworks(); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.defi.networks); + }); + + it("should fetch price", async () => { + const mockData = { value: 100 }; + mockSuccessResponse(mockData); + const result = await provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.defi.price, { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + }); + + it("should fetch multiple prices", async () => { + const mockData = { prices: {} }; + mockSuccessResponse(mockData); + const result = await provider.fetchDefiPriceMultiple({ + list_address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.defi.price_multi, { + list_address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + }); + + it("should fetch multiple prices via POST", async () => { + const mockData = { prices: {} }; + mockSuccessResponse(mockData); + const result = await provider.fetchDefiPriceMultiple_POST({ + list_address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall( + BIRDEYE_ENDPOINTS.defi.price_multi_POST, + { list_address: DEFAULT_SUPPORTED_SYMBOLS.SOL }, + "POST" + ); + }); + + it("should fetch historical price", async () => { + const mockData = { items: [] }; + mockSuccessResponse(mockData); + const result = await provider.fetchDefiPriceHistorical({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + type: "1H", + address_type: "token", + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.defi.history_price, { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + type: "1H", + address_type: "token", + }); + }); + }); + + describe("Token Endpoints", () => { + it("should fetch token list", async () => { + const mockData = { tokens: [] }; + mockSuccessResponse(mockData); + const result = await provider.fetchTokenList({}); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.token.list_all, {}); + }); + + it("should fetch token security", async () => { + const mockData = { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + totalSupply: 1000000, + mintable: false, + proxied: false, + ownerAddress: "owner123", + creatorAddress: "creator123", + securityChecks: { + honeypot: false, + trading_cooldown: false, + transfer_pausable: false, + is_blacklisted: false, + is_whitelisted: false, + is_proxy: false, + is_mintable: false, + can_take_back_ownership: false, + hidden_owner: false, + anti_whale_modifiable: false, + is_anti_whale: false, + trading_pausable: false, + can_be_blacklisted: false, + is_true_token: true, + is_airdrop_scam: false, + slippage_modifiable: false, + is_honeypot: false, + transfer_pausable_time: false, + is_wrapped: false, + }, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchTokenSecurityByAddress({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.token.security, { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + }); + + it("should fetch token overview", async () => { + const mockData = { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + decimals: 9, + symbol: "SOL", + name: "Solana", + extensions: { + coingeckoId: "solana", + website: "https://solana.com", + telegram: "solana", + twitter: "solana", + description: "Solana blockchain token", + }, + logoURI: "https://example.com/sol.png", + liquidity: 1000000, + price: 100, + priceChange24hPercent: 5, + uniqueWallet24h: 1000, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchTokenOverview({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.token.overview, { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + }); + + it("should fetch token trending", async () => { + const mockData = { + updateUnixTime: 1234567890, + updateTime: "2024-01-01T00:00:00Z", + tokens: [], + total: 0, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchTokenTrending(); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.token.trending); + }); + }); + + describe("Wallet Endpoints", () => { + it("should fetch wallet portfolio", async () => { + const mockData = { + wallet: "test-wallet", + totalUsd: 1000, + items: [ + { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + name: "Solana", + symbol: "SOL", + decimals: 9, + balance: "1000000000", + uiAmount: 1, + chainId: "solana", + logoURI: "https://example.com/sol.png", + priceUsd: 100, + valueUsd: 100, + }, + ], + }; + mockSuccessResponse(mockData); + const result = await provider.fetchWalletPortfolio({ + wallet: "test-wallet", + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.wallet.portfolio, { + wallet: "test-wallet", + }); + }); + + it("should fetch wallet token balance", async () => { + const mockData = { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + name: "Solana", + symbol: "SOL", + decimals: 9, + balance: 1000000000, + uiAmount: 1, + chainId: "solana", + priceUsd: 100, + valueUsd: 100, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchWalletTokenBalance({ + wallet: "test-wallet", + token_address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.wallet.token_balance, { + wallet: "test-wallet", + token_address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + }); + }); + + describe("Pair Endpoints", () => { + it("should fetch pair overview", async () => { + const mockData = { + address: "pair-address", + name: "SOL/USDC", + base: { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + decimals: 9, + icon: "https://example.com/sol.png", + symbol: "SOL", + }, + quote: { + address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + icon: "https://example.com/usdc.png", + symbol: "USDC", + }, + created_at: "2024-01-01T00:00:00Z", + source: "Raydium", + liquidity: 1000000, + liquidity_change_percentage_24h: 5, + price: 100, + volume_24h: 1000000, + volume_24h_change_percentage_24h: 10, + trade_24h: 1000, + trade_24h_change_percent: 15, + unique_wallet_24h: 500, + unique_wallet_24h_change_percent: 20, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchPairOverviewSingle({ + address: "pair-address", + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.pair.overview_single, { + address: "pair-address", + }); + }); + + it("should fetch multiple pair overview", async () => { + const mockData = { + "pair-1": { + address: "pair-1", + name: "SOL/USDC", + base: { + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + decimals: 9, + icon: "https://example.com/sol.png", + symbol: "SOL", + }, + quote: { + address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + icon: "https://example.com/usdc.png", + symbol: "USDC", + }, + created_at: "2024-01-01T00:00:00Z", + source: "Raydium", + liquidity: 1000000, + liquidity_change_percentage_24h: 5, + price: 100, + volume_24h: 1000000, + volume_24h_change_percentage_24h: 10, + trade_24h: 1000, + trade_24h_change_percent: 15, + unique_wallet_24h: 500, + unique_wallet_24h_change_percent: 20, + }, + "pair-2": { + address: "pair-2", + name: "BTC/USDC", + base: { + address: DEFAULT_SUPPORTED_SYMBOLS.BTC, + decimals: 8, + icon: "https://example.com/btc.png", + symbol: "BTC", + }, + quote: { + address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + decimals: 6, + icon: "https://example.com/usdc.png", + symbol: "USDC", + }, + created_at: "2024-01-01T00:00:00Z", + source: "Raydium", + liquidity: 2000000, + liquidity_change_percentage_24h: 3, + price: 50000, + volume_24h: 2000000, + volume_24h_change_percentage_24h: 8, + trade_24h: 500, + trade_24h_change_percent: 12, + unique_wallet_24h: 300, + unique_wallet_24h_change_percent: 15, + }, + }; + mockSuccessResponse(mockData); + const result = await provider.fetchMultiPairOverview({ + list_address: "pair-1,pair-2", + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.pair.overview_multi, { + list_address: "pair-1,pair-2", + }); + }); + }); + + describe("Search Endpoints", () => { + it("should fetch token market search", async () => { + const mockData = { + items: [ + { + type: "token", + result: [ + { + name: "Solana", + symbol: "SOL", + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + fdv: 1000000000, + market_cap: 500000000, + liquidity: 1000000, + volume_24h_change_percent: 10, + price: 100, + price_change_24h_percent: 5, + buy_24h: 500, + buy_24h_change_percent: 15, + sell_24h: 300, + sell_24h_change_percent: -10, + trade_24h: 800, + trade_24h_change_percent: 8, + unique_wallet_24h: 1000, + unique_view_24h_change_percent: 20, + last_trade_human_time: "2024-01-01T00:00:00Z", + last_trade_unix_time: 1704067200, + creation_time: "2020-01-01T00:00:00Z", + volume_24h_usd: 1000000, + logo_uri: "https://example.com/sol.png", + }, + ], + }, + ], + }; + mockSuccessResponse(mockData); + const result = await provider.fetchSearchTokenMarketData({ + keyword: "test", + }); + expect(result.data).toEqual(mockData); + expectFetchCall(BIRDEYE_ENDPOINTS.search.token_market, { + keyword: "test", + }); + }); + }); + + describe("Caching", () => { + beforeEach(() => { + // Reset the provider with a fresh cache manager for each test + cacheManager = { + get: vi.fn(), + set: vi.fn(), + } as unknown as ICacheManager; + provider = new BirdeyeProvider(cacheManager); + }); + + it("should use file system cache when available", async () => { + const mockResponse = { data: { value: 100 }, success: true }; + (cacheManager.get as Mock).mockResolvedValue(mockResponse); + + const result = await provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + + expect(result).toEqual(mockResponse); + expect(fetch).not.toHaveBeenCalled(); + expect(cacheManager.get).toHaveBeenCalled(); + }); + + it("should fetch and cache when cache misses", async () => { + const mockResponse = { data: { value: 100 }, success: true }; + (cacheManager.get as Mock).mockResolvedValue(null); + (fetch as Mock).mockResolvedValue({ + ok: true, + json: async () => mockResponse, + }); + + const result = await provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + + expect(result).toEqual(mockResponse); + expect(fetch).toHaveBeenCalledTimes(1); + expect(cacheManager.set).toHaveBeenCalled(); + }); + }); + + describe("Error Handling", () => { + it("should retry on failure", async () => { + (fetch as Mock) + .mockRejectedValueOnce(new Error("Network error")) + .mockRejectedValueOnce(new Error("Network error")) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ data: { value: 100 }, success: true }), + }); + + const result = await provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }); + + expect(result).toEqual({ data: { value: 100 }, success: true }); + expect(fetch).toHaveBeenCalledTimes(3); + }); + + it("should throw after max retries", async () => { + (fetch as Mock).mockRejectedValue(new Error("Network error")); + + await expect( + provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }) + ).rejects.toThrow("Network error"); + expect(fetch).toHaveBeenCalledTimes(3); // Default max retries + }); + + it("should handle non-200 responses", async () => { + (fetch as Mock).mockResolvedValue({ + ok: false, + status: 404, + text: async () => "Not found", + }); + + await expect( + provider.fetchDefiPrice({ + address: DEFAULT_SUPPORTED_SYMBOLS.SOL, + }) + ).rejects.toThrow("HTTP error! status: 404, message: Not found"); + expect(fetch).toHaveBeenCalledTimes(3); // Should still retry on HTTP errors + }); + }); +}); diff --git a/packages/plugin-birdeye/src/types/api/common.ts b/packages/plugin-birdeye/src/types/api/common.ts new file mode 100644 index 00000000000..59b5d9903b9 --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/common.ts @@ -0,0 +1,301 @@ +import { + BaseQuoteParams, + DefiHistoryPriceParams, + DefiMultiPriceParams, + DefiPriceParams, + HistoricalPriceUnixParams, + MultiPriceVolumeParams, + OHLCVParams, + PriceVolumeParams, +} from "./defi"; +import { + OHLCVPairParams, + PairOverviewMultiParams, + PairOverviewSingleParams, + PairTradesParams, +} from "./pair"; +import { TokenMarketSearchParams } from "./search"; +import { + AllMarketsParams, + MintBurnParams, + NewListingParams, + TokenCreationInfoParams, + TokenHoldersParams, + TokenListV2Params, + TokenMarketDataParams, + TokenMetadataMultiParams, + TokenMetadataSingleParams, + TokenOverviewParams, + TokenSecurityParams, + TokenTradeDataMultiParams, + TokenTradeDataSingleParams, + TokenTradesParams, + TopTradersParams, +} from "./token"; +import { GainersLosersParams, TraderTransactionsSeekParams } from "./trader"; +import { + WalletPortfolioMultichainParams, + WalletPortfolioParams, + WalletSimulationParams, + WalletTokenBalanceParams, + WalletTransactionHistoryMultichainParams, + WalletTransactionHistoryParams, +} from "./wallet"; + +export type BirdeyeApiParams = + | DefiPriceParams + | DefiMultiPriceParams + | DefiHistoryPriceParams + | HistoricalPriceUnixParams + | OHLCVParams + | PriceVolumeParams + | MultiPriceVolumeParams + | PairTradesParams + | OHLCVPairParams + | PairOverviewMultiParams + | PairOverviewSingleParams + | TokenMarketSearchParams + | TokenTradesParams + | TokenSecurityParams + | TokenOverviewParams + | TokenCreationInfoParams + | TokenListV2Params + | TokenMetadataMultiParams + | TokenTradeDataMultiParams + | GainersLosersParams + | TraderTransactionsSeekParams + | WalletPortfolioParams + | WalletTokenBalanceParams + | WalletTransactionHistoryParams + | BaseQuoteParams + | TokenHoldersParams + | MintBurnParams + | TopTradersParams + | AllMarketsParams + | NewListingParams + | TokenMetadataSingleParams + | TokenMarketDataParams + | TokenTradeDataSingleParams + | WalletPortfolioMultichainParams + | WalletTransactionHistoryMultichainParams + | WalletSimulationParams + | Record; + +export interface BirdeyeApiResponseWrapper { + data: T; + success: boolean; +} + +export type BirdeyeApiResponse = BirdeyeApiResponseWrapper; + +export type TimeInterval = + | "1m" + | "3m" + | "5m" + | "15m" + | "30m" + | "1H" + | "2H" + | "4H" + | "6H" + | "8H" + | "12H" + | "1D" + | "3D" + | "1W" + | "1M" + | "30m" + | "1h" + | "2h" + | "4h" + | "6h" + | "8h" + | "12h" + | "24h"; + +export interface TokenTradeData { + address: string; + holder: number; + market: number; + last_trade_unix_time: number; + last_trade_human_time: string; + price: number; + history_30m_price: number; + price_change_30m_percent: number; + history_1h_price: number; + price_change_1h_percent: number; + history_2h_price: number; + price_change_2h_percent: number; + history_4h_price: number; + price_change_4h_percent: number; + history_6h_price: number; + price_change_6h_percent: number; + history_8h_price: number; + price_change_8h_percent: number; + history_12h_price: number; + price_change_12h_percent: number; + history_24h_price: number; + price_change_24h_percent: number; + unique_wallet_30m: number; + unique_wallet_history_30m: number; + unique_wallet_30m_change_percent: number | null; + unique_wallet_1h: number; + unique_wallet_history_1h: number; + unique_wallet_1h_change_percent: number | null; + unique_wallet_2h: number; + unique_wallet_history_2h: number; + unique_wallet_2h_change_percent: number | null; + unique_wallet_4h: number; + unique_wallet_history_4h: number; + unique_wallet_4h_change_percent: number | null; + unique_wallet_8h: number; + unique_wallet_history_8h: number; + unique_wallet_8h_change_percent: number | null; + unique_wallet_24h: number; + unique_wallet_history_24h: number; + unique_wallet_24h_change_percent: number | null; + trade_30m: number; + trade_history_30m: number; + trade_30m_change_percent: number; + sell_30m: number; + sell_history_30m: number; + sell_30m_change_percent: number; + buy_30m: number; + buy_history_30m: number; + buy_30m_change_percent: number; + volume_30m: number; + volume_30m_usd: number; + volume_history_30m: number; + volume_history_30m_usd: number; + volume_30m_change_percent: number; + volume_buy_30m: number; + volume_buy_30m_usd: number; + volume_buy_history_30m: number; + volume_buy_history_30m_usd: number; + volume_buy_30m_change_percent: number; + volume_sell_30m: number; + volume_sell_30m_usd: number; + volume_sell_history_30m: number; + volume_sell_history_30m_usd: number; + volume_sell_30m_change_percent: number; + trade_1h: number; + trade_history_1h: number; + trade_1h_change_percent: number; + sell_1h: number; + sell_history_1h: number; + sell_1h_change_percent: number; + buy_1h: number; + buy_history_1h: number; + buy_1h_change_percent: number; + volume_1h: number; + volume_1h_usd: number; + volume_history_1h: number; + volume_history_1h_usd: number; + volume_1h_change_percent: number; + volume_buy_1h: number; + volume_buy_1h_usd: number; + volume_buy_history_1h: number; + volume_buy_history_1h_usd: number; + volume_buy_1h_change_percent: number; + volume_sell_1h: number; + volume_sell_1h_usd: number; + volume_sell_history_1h: number; + volume_sell_history_1h_usd: number; + volume_sell_1h_change_percent: number; + trade_2h: number; + trade_history_2h: number; + trade_2h_change_percent: number; + sell_2h: number; + sell_history_2h: number; + sell_2h_change_percent: number; + buy_2h: number; + buy_history_2h: number; + buy_2h_change_percent: number; + volume_2h: number; + volume_2h_usd: number; + volume_history_2h: number; + volume_history_2h_usd: number; + volume_2h_change_percent: number; + volume_buy_2h: number; + volume_buy_2h_usd: number; + volume_buy_history_2h: number; + volume_buy_history_2h_usd: number; + volume_buy_2h_change_percent: number; + volume_sell_2h: number; + volume_sell_2h_usd: number; + volume_sell_history_2h: number; + volume_sell_history_2h_usd: number; + volume_sell_2h_change_percent: number; + trade_4h: number; + trade_history_4h: number; + trade_4h_change_percent: number; + sell_4h: number; + sell_history_4h: number; + sell_4h_change_percent: number; + buy_4h: number; + buy_history_4h: number; + buy_4h_change_percent: number; + volume_4h: number; + volume_4h_usd: number; + volume_history_4h: number; + volume_history_4h_usd: number; + volume_4h_change_percent: number; + volume_buy_4h: number; + volume_buy_4h_usd: number; + volume_buy_history_4h: number; + volume_buy_history_4h_usd: number; + volume_buy_4h_change_percent: number; + volume_sell_4h: number; + volume_sell_4h_usd: number; + volume_sell_history_4h: number; + volume_sell_history_4h_usd: number; + volume_sell_4h_change_percent: number; + trade_8h: number; + trade_history_8h: number; + trade_8h_change_percent: number; + sell_8h: number; + sell_history_8h: number; + sell_8h_change_percent: number; + buy_8h: number; + buy_history_8h: number; + buy_8h_change_percent: number; + volume_8h: number; + volume_8h_usd: number; + volume_history_8h: number; + volume_history_8h_usd: number; + volume_8h_change_percent: number; + volume_buy_8h: number; + volume_buy_8h_usd: number; + volume_buy_history_8h: number; + volume_buy_history_8h_usd: number; + volume_buy_8h_change_percent: number; + volume_sell_8h: number; + volume_sell_8h_usd: number; + volume_sell_history_8h: number; + volume_sell_history_8h_usd: number; + volume_sell_8h_change_percent: number; + trade_24h: number; + trade_history_24h: number; + trade_24h_change_percent: number; + sell_24h: number; + sell_history_24h: number; + sell_24h_change_percent: number; + buy_24h: number; + buy_history_24h: number; + buy_24h_change_percent: number; + volume_24h: number; + volume_24h_usd: number; + volume_history_24h: number; + volume_history_24h_usd: number; + volume_24h_change_percent: number; + volume_buy_24h: number; + volume_buy_24h_usd: number; + volume_buy_history_24h: number; + volume_buy_history_24h_usd: number; + volume_buy_24h_change_percent: number; + volume_sell_24h: number; + volume_sell_24h_usd: number; + volume_sell_history_24h: number; + volume_sell_history_24h_usd: number; + volume_sell_24h_change_percent: number; +} diff --git a/packages/plugin-birdeye/src/types/api/defi.ts b/packages/plugin-birdeye/src/types/api/defi.ts new file mode 100644 index 00000000000..896991df6c3 --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/defi.ts @@ -0,0 +1,219 @@ +import { TimeInterval } from "./common"; + +// Network Types +export interface DefiNetworksResponse { + success: boolean; + data: { + chains: string[]; + }; +} + +// Price Types +export interface DefiPriceParams { + address: string; + check_liquidity?: number; + include_liquidity?: boolean; +} + +export interface DefiPriceResponse { + success: boolean; + data: { + value: number; + updateUnixTime?: number; + updateHumanTime?: string; + liquidity?: number; + }; +} + +// Multi Price Types +export interface DefiMultiPriceParams { + list_address: string; + check_liquidity?: number; + include_liquidity?: boolean; +} + +export interface DefiMultiPriceResponse { + success: boolean; + data: { + [address: string]: { + value?: number; + updateUnixTime?: number; + updateHumanTime?: string; + priceChange24h?: number; + }; + }; +} + +// Multi Price Types POST +export interface DefiMultiPriceParamsPOST { + check_liquidity?: number; + include_liquidity?: boolean; + list_address: string; +} + +// History Price Types +export interface DefiHistoryPriceParams { + address: string; + address_type: "token" | "pair"; + type: TimeInterval; + time_from?: number; + time_to?: number; +} + +export interface DefiHistoryPriceResponse { + success: boolean; + data: { + items: Array<{ + unixTime?: number; + value?: number; + }>; + }; +} + +// Historical Price Unix Types +export interface HistoricalPriceUnixParams { + address: string; + unixtime?: number; +} + +export interface HistoricalPriceUnixResponse { + success: boolean; + data: { + value?: number; + updateUnixTime?: number; + priceChange24h?: string; + }; +} + +// OHLCV Types +export interface OHLCVParams { + address: string; + type?: TimeInterval; + time_from?: number; + time_to?: number; +} + +export interface OHLCVResponse { + success: boolean; + data: { + items: Array<{ + unixTime?: number; + address?: string; + type?: TimeInterval; + o?: number; + h?: number; + l?: number; + c?: number; + v?: number; + }>; + }; +} + +// Price Volume Types +export interface PriceVolumeParams { + address: string; + type?: TimeInterval; +} + +export interface PriceVolumeResponse { + success: boolean; + data: { + price?: number; + updateUnixTime?: number; + updateHumanTime?: string; + volumeUSD?: number; + volumeChangePercent?: number; + priceChangePercent?: number; + }; +} + +// Multi Price Volume Types +export interface MultiPriceVolumeParams { + list_address: string; + type?: TimeInterval; +} + +export interface MultiPriceVolumeResponse { + success: boolean; + data: { + [address: string]: { + price?: number; + updateUnixTime?: number; + updateHumanTime?: string; + volumeUSD?: number; + volumeChangePercent?: number; + priceChangePercent?: number; + }; + }; +} + +// Base Quote Types +export interface BaseQuoteParams { + base_address: string; + quote_address: string; + type?: TimeInterval; + time_from?: number; + time_to?: number; +} + +export interface BaseQuoteResponse { + success: boolean; + data: { + unixTime?: number; + vBase?: number; + vQuote?: number; + o?: number; + h?: number; + l?: number; + c?: number; + }; +} + +// Token Trades Types +export interface DefiTradesTokenParams { + address: string; + limit?: number; + offset?: number; + tx_type?: "swap" | "add" | "remove" | "all"; + sort_type?: "asc" | "desc"; + before_time?: number; + after_time?: number; +} + +export interface DefiTradesTokenInfo { + symbol: string; + decimals: number; + address: string; + amount: number; + uiAmount: number; + price: number | null; + nearestPrice: number | null; + changeAmount: number; + uiChangeAmount: number; + feeInfo?: any | null; +} + +export interface DefiTradesTokenResponse { + success: boolean; + data: { + items: Array<{ + quote?: DefiTradesTokenInfo; + base?: DefiTradesTokenInfo; + basePrice?: number | null; + quotePrice?: number | null; + txHash?: string; + source?: string; + blockUnixTime?: number; + txType?: string; + owner?: string; + side?: string; + alias?: string | null; + pricePair?: number; + from?: DefiTradesTokenInfo; + to?: DefiTradesTokenInfo; + tokenPrice?: number | null; + poolId?: string; + }>; + hasNext?: boolean; + }; +} diff --git a/packages/plugin-birdeye/src/types/api/pair.ts b/packages/plugin-birdeye/src/types/api/pair.ts new file mode 100644 index 00000000000..91a89c9c36b --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/pair.ts @@ -0,0 +1,199 @@ +import { TimeInterval } from "./common"; + +// Pair Trades Types +export interface PairTradesParams { + pair: string; + limit?: number; + offset?: number; +} + +export interface PairTradesResponse { + success: boolean; + data: { + items: Array<{ + signature?: string; + blockNumber?: number; + unixTime?: number; + type?: "buy" | "sell"; + tokenAddress?: string; + tokenAmount?: number; + tokenAmountUI?: number; + tokenSymbol?: string; + tokenDecimals?: number; + priceUsd?: number; + volumeUsd?: number; + maker?: string; + taker?: string; + txType?: string; + poolAddress?: string; + poolName?: string; + dex?: string; + }>; + }; +} + +// OHLCV Pair Types +export interface OHLCVPairParams { + address: string; + type?: TimeInterval; + time_from?: number; + time_to?: number; +} + +export interface OHLCVPairResponse { + success: boolean; + data: { + items: Array<{ + unixTime?: number; + address?: string; + type?: TimeInterval; + o?: number; + h?: number; + l?: number; + c?: number; + v?: number; + }>; + }; +} + +// Pair Overview Types +export interface PairOverviewMultiParams { + list_address: string; + before_time?: number; +} + +export interface PairOverviewSingleParams { + address: string; +} + +interface PairOverviewData { + address: string; + name: string; + base: { + address: string; + decimals: number; + icon: string; + symbol: string; + }; + quote: { + address: string; + decimals: number; + icon: string; + symbol: string; + }; + created_at: string; + source: string; + liquidity: number; + liquidity_change_percentage_24h: number | null; + price: number; + volume_24h: number; + volume_24h_change_percentage_24h: number | null; + trade_24h: number; + trade_24h_change_percent: number; + unique_wallet_24h: number; + unique_wallet_24h_change_percent: number | null; + + // Time-based metrics + trade_30m: number; + trade_1h: number; + trade_2h: number; + trade_4h: number; + trade_8h: number; + trade_12h: number; + + trade_30m_change_percent: number; + trade_1h_change_percent: number; + trade_2h_change_percent: number; + trade_4h_change_percent: number; + trade_8h_change_percent: number; + trade_12h_change_percent: number; + + volume_30m: number; + volume_1h: number; + volume_2h: number; + volume_4h: number; + volume_8h: number; + volume_12h: number; + + volume_30m_quote: number; + volume_1h_quote: number; + volume_2h_quote: number; + volume_4h_quote: number; + volume_8h_quote: number; + volume_12h_quote: number; + + volume_30m_base: number; + volume_1h_base: number; + volume_2h_base: number; + volume_4h_base: number; + volume_8h_base: number; + volume_12h_base: number; +} + +export interface PairOverviewSingleResponse { + success: boolean; + data: { + address?: string; + name?: string; + base?: { + address?: string; + decimals?: number; + icon?: string; + symbol?: string; + }; + quote?: { + address?: string; + decimals?: number; + icon?: string; + symbol?: string; + }; + created_at?: string; + source?: string; + liquidity?: number; + liquidity_change_percentage_24h?: number | null; + price?: number; + volume_24h?: number; + volume_24h_change_percentage_24h?: number | null; + trade_24h?: number; + trade_24h_change_percent?: number; + unique_wallet_24h?: number; + unique_wallet_24h_change_percent?: number | null; + trade_30m?: number; + trade_1h?: number; + trade_2h?: number; + trade_4h?: number; + trade_8h?: number; + trade_12h?: number; + trade_30m_change_percent?: number; + trade_1h_change_percent?: number; + trade_2h_change_percent?: number; + trade_4h_change_percent?: number; + trade_8h_change_percent?: number; + trade_12h_change_percent?: number; + volume_30m?: number; + volume_1h?: number; + volume_2h?: number; + volume_4h?: number; + volume_8h?: number; + volume_12h?: number; + volume_30m_quote?: number; + volume_1h_quote?: number; + volume_2h_quote?: number; + volume_4h_quote?: number; + volume_8h_quote?: number; + volume_12h_quote?: number; + volume_30m_base?: number; + volume_1h_base?: number; + volume_2h_base?: number; + volume_4h_base?: number; + volume_8h_base?: number; + volume_12h_base?: number; + }; +} + +export interface PairOverviewMultiResponse { + success: boolean; + data: { + [pair: string]: PairOverviewData; + }; +} diff --git a/packages/plugin-birdeye/src/types/api/search.ts b/packages/plugin-birdeye/src/types/api/search.ts new file mode 100644 index 00000000000..bdfd5410ef2 --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/search.ts @@ -0,0 +1,85 @@ +import { BirdeyeSupportedChain } from "../shared"; + +// Search Types +export interface TokenMarketSearchParams { + chain?: BirdeyeSupportedChain | "all"; + keyword?: string; + target?: "token" | "market" | "all"; + sort_by?: + | "fdv" + | "marketcap" + | "liquidity" + | "price" + | "price_change_24h_percent" + | "trade_24h" + | "trade_24h_change_percent" + | "buy_24h" + | "buy_24h_change_percent" + | "sell_24h" + | "sell_24h_change_percent" + | "unique_wallet_24h" + | "unique_view_24h_change_percent" + | "last_trade_unix_time" + | "volume_24h_usd" + | "volume_24h_change_percent"; + sort_type?: "asc" | "desc"; + verify_token?: boolean; + markets?: string; + offset?: number; + limit?: number; +} + +export interface TokenMarketSearchResponse { + success: boolean; + data: { + items: Array<{ + type?: "token" | "market"; + result?: Array; + }>; + }; +} + +export interface TokenResult { + name?: string; + symbol?: string; + address?: string; + network?: string; + fdv?: number; + market_cap?: number; + liquidity?: number; + volume_24h_change_percent?: number; + price?: number; + price_change_24h_percent?: number; + buy_24h?: number; + buy_24h_change_percent?: number; + sell_24h?: number; + sell_24h_change_percent?: number; + trade_24h?: number; + trade_24h_change_percent?: number; + unique_wallet_24h?: number; + unique_view_24h_change_percent?: number; + last_trade_human_time?: string; + last_trade_unix_time?: number; + creation_time?: string; + volume_24h_usd?: number; + logo_uri?: string; +} + +export interface MarketResult { + name: string; + address: string; + liquidity: number; + source: string; + trade_24h: number; + trade_24h_change_percent: number; + unique_wallet_24h: number; + unique_wallet_24h_change_percent: number; + last_trade_human_time: string; + last_trade_unix_time: number; + base_mint: string; + quote_mint: string; + amount_base: number; + amout_quote: number; // Note: typo in API response + creation_time: string; + volume_24h_usd: number; +} diff --git a/packages/plugin-birdeye/src/types/api/token.ts b/packages/plugin-birdeye/src/types/api/token.ts new file mode 100644 index 00000000000..6922ee22350 --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/token.ts @@ -0,0 +1,634 @@ +import { TimeInterval, TokenTradeData } from "./common"; + +// Token Trades Types +export interface TokenTradesParams { + address: string; + limit?: number; + offset?: number; + type?: "buy" | "sell" | "all"; +} + +export interface TokenTradesResponse { + success: boolean; + data: { + items: Array<{ + signature?: string; + blockNumber?: number; + unixTime?: number; + type?: "buy" | "sell"; + tokenAddress?: string; + tokenAmount?: number; + tokenAmountUI?: number; + tokenSymbol?: string; + tokenDecimals?: number; + priceUsd?: number; + volumeUsd?: number; + maker?: string; + taker?: string; + txType?: string; + poolAddress?: string; + poolName?: string; + dex?: string; + }>; + }; +} + +export interface TokenListParams { + sort_by?: "mc" | "v24hUSD" | "v24hChangePercent"; + sort_type?: "asc" | "desc"; + offset?: number; + limit?: number; + min_liquidity?: number; +} + +// Token List Types +export interface TokenListResponse { + success: boolean; + data: { + tokens: Array<{ + address?: string; + symbol?: string; + name?: string; + decimals?: number; + logoURI?: string; + coingeckoId?: string; + volume24h?: number; + priceChange24h?: number; + price?: number; + }>; + }; +} + +// Token Security Types +export interface TokenSecurityParams { + address: string; +} + +export interface TokenSecurityResponse { + success: boolean; + data: { + address?: string; + totalSupply?: number; + mintable?: boolean; + proxied?: boolean; + proxy?: string; + ownerAddress?: string; + creatorAddress?: string; + securityChecks?: { + honeypot?: boolean; + trading_cooldown?: boolean; + transfer_pausable?: boolean; + is_blacklisted?: boolean; + is_whitelisted?: boolean; + is_proxy?: boolean; + is_mintable?: boolean; + can_take_back_ownership?: boolean; + hidden_owner?: boolean; + anti_whale_modifiable?: boolean; + is_anti_whale?: boolean; + trading_pausable?: boolean; + can_be_blacklisted?: boolean; + is_true_token?: boolean; + is_airdrop_scam?: boolean; + slippage_modifiable?: boolean; + is_honeypot?: boolean; + transfer_pausable_time?: boolean; + is_wrapped?: boolean; + }; + }; +} + +// Token Overview Types +export interface TokenOverviewParams { + address: string; +} + +export interface TokenOverviewResponse { + success: boolean; + data: { + address?: string; + decimals?: number; + symbol?: string; + name?: string; + extensions?: { + coingeckoId?: string; + serumV3Usdc?: string; + serumV3Usdt?: string; + website?: string; + telegram?: string | null; + twitter?: string; + description?: string; + discord?: string; + medium?: string; + }; + logoURI?: string; + liquidity?: number; + lastTradeUnixTime?: number; + lastTradeHumanTime?: string; + price?: number; + history30mPrice?: number; + priceChange30mPercent?: number; + history1hPrice?: number; + priceChange1hPercent?: number; + history2hPrice?: number; + priceChange2hPercent?: number; + history4hPrice?: number; + priceChange4hPercent?: number; + history6hPrice?: number; + priceChange6hPercent?: number; + history8hPrice?: number; + priceChange8hPercent?: number; + history12hPrice?: number; + priceChange12hPercent?: number; + history24hPrice?: number; + priceChange24hPercent?: number; + uniqueWallet30m?: number; + uniqueWalletHistory30m?: number; + uniqueWallet30mChangePercent?: number; + uniqueWallet1h?: number; + uniqueWalletHistory1h?: number; + uniqueWallet1hChangePercent?: number; + uniqueWallet2h?: number; + uniqueWalletHistory2h?: number; + uniqueWallet2hChangePercent?: number; + uniqueWallet4h?: number; + uniqueWalletHistory4h?: number; + uniqueWallet4hChangePercent?: number; + uniqueWallet8h?: number; + uniqueWalletHistory8h?: number; + uniqueWallet8hChangePercent?: number; + uniqueWallet24h?: number; + uniqueWalletHistory24h?: number; + uniqueWallet24hChangePercent?: number; + supply?: number; + mc?: number; + circulatingSupply?: number; + realMc?: number; + holder?: number; + trade30m?: number; + tradeHistory30m?: number; + trade30mChangePercent?: number; + sell30m?: number; + sellHistory30m?: number; + sell30mChangePercent?: number; + buy30m?: number; + buyHistory30m?: number; + buy30mChangePercent?: number; + v30m?: number; + v30mUSD?: number; + vHistory30m?: number; + vHistory30mUSD?: number; + v30mChangePercent?: number; + vBuy30m?: number; + vBuy30mUSD?: number; + vBuyHistory30m?: number; + vBuyHistory30mUSD?: number; + vBuy30mChangePercent?: number; + vSell30m?: number; + vSell30mUSD?: number; + vSellHistory30m?: number; + vSellHistory30mUSD?: number; + vSell30mChangePercent?: number; + trade1h?: number; + tradeHistory1h?: number; + trade1hChangePercent?: number; + sell1h?: number; + sellHistory1h?: number; + sell1hChangePercent?: number; + buy1h?: number; + buyHistory1h?: number; + buy1hChangePercent?: number; + v1h?: number; + v1hUSD?: number; + vHistory1h?: number; + vHistory1hUSD?: number; + v1hChangePercent?: number; + vBuy1h?: number; + vBuy1hUSD?: number; + vBuyHistory1h?: number; + vBuyHistory1hUSD?: number; + vBuy1hChangePercent?: number; + vSell1h?: number; + vSell1hUSD?: number; + vSellHistory1h?: number; + vSellHistory1hUSD?: number; + vSell1hChangePercent?: number; + trade2h?: number; + tradeHistory2h?: number; + trade2hChangePercent?: number; + sell2h?: number; + sellHistory2h?: number; + sell2hChangePercent?: number; + buy2h?: number; + buyHistory2h?: number; + buy2hChangePercent?: number; + v2h?: number; + v2hUSD?: number; + vHistory2h?: number; + vHistory2hUSD?: number; + v2hChangePercent?: number; + vBuy2h?: number; + vBuy2hUSD?: number; + vBuyHistory2h?: number; + vBuyHistory2hUSD?: number; + vBuy2hChangePercent?: number; + vSell2h?: number; + vSell2hUSD?: number; + vSellHistory2h?: number; + vSellHistory2hUSD?: number; + vSell2hChangePercent?: number; + trade4h?: number; + tradeHistory4h?: number; + trade4hChangePercent?: number; + sell4h?: number; + sellHistory4h?: number; + sell4hChangePercent?: number; + buy4h?: number; + buyHistory4h?: number; + buy4hChangePercent?: number; + v4h?: number; + v4hUSD?: number; + vHistory4h?: number; + vHistory4hUSD?: number; + v4hChangePercent?: number; + vBuy4h?: number; + vBuy4hUSD?: number; + vBuyHistory4h?: number; + vBuyHistory4hUSD?: number; + vBuy4hChangePercent?: number; + vSell4h?: number; + vSell4hUSD?: number; + vSellHistory4h?: number; + vSellHistory4hUSD?: number; + vSell4hChangePercent?: number; + trade8h?: number; + tradeHistory8h?: number; + trade8hChangePercent?: number; + sell8h?: number; + sellHistory8h?: number; + sell8hChangePercent?: number; + buy8h?: number; + buyHistory8h?: number; + buy8hChangePercent?: number; + v8h?: number; + v8hUSD?: number; + vHistory8h?: number; + vHistory8hUSD?: number; + v8hChangePercent?: number; + vBuy8h?: number; + vBuy8hUSD?: number; + vBuyHistory8h?: number; + vBuyHistory8hUSD?: number; + vBuy8hChangePercent?: number; + vSell8h?: number; + vSell8hUSD?: number; + vSellHistory8h?: number; + vSellHistory8hUSD?: number; + vSell8hChangePercent?: number; + trade24h?: number; + tradeHistory24h?: number; + trade24hChangePercent?: number; + sell24h?: number; + sellHistory24h?: number; + sell24hChangePercent?: number; + buy24h?: number; + buyHistory24h?: number; + buy24hChangePercent?: number; + v24h?: number; + v24hUSD?: number; + vHistory24h?: number; + vHistory24hUSD?: number; + v24hChangePercent?: number; + vBuy24h?: number; + vBuy24hUSD?: number; + vBuyHistory24h?: number; + vBuyHistory24hUSD?: number; + vBuy24hChangePercent?: number; + vSell24h?: number; + vSell24hUSD?: number; + vSellHistory24h?: number; + vSellHistory24hUSD?: number; + vSell24hChangePercent?: number; + watch?: null; + numberMarkets?: number; + }; +} + +// Token Creation Info Types +export interface TokenCreationInfoParams { + address: string; +} + +export interface TokenCreationInfoResponse { + success: boolean; + data: { + txHash?: string; + slot?: number; + tokenAddress?: string; + decimals?: number; + owner?: string; + blockUnixTime?: number; + blockHumanTime?: string; + }; +} + +export interface TokenTrendingParams { + sort_by?: "rank" | "volume24hUSD" | "liquidity"; + sort_type?: "asc" | "desc"; + offset?: number; + limit?: number; +} + +// Token Trending Types +export interface TokenTrendingResponse { + success: boolean; + data: { + updateUnixTime?: number; + updateTime?: string; + tokens: Array<{ + address?: string; + symbol?: string; + name?: string; + decimals?: number; + liquidity?: number; + logoURI?: string; + volume24hUSD?: number; + rank?: number; + price?: number; + }>; + total?: number; + }; +} + +// Token List V2 Types +export interface TokenListV2Params { + offset?: number; + limit?: number; + sortBy?: string; + sortOrder?: "asc" | "desc"; +} + +// this endpoint is for enterprise only and the response is not documented +export interface TokenListV2Response { + success: boolean; + data: any; +} + +export interface TokenMetadataMultiParams { + list_addresses: string; +} + +export interface TokenMetadataMultiResponse { + success: boolean; + data: { + [address: string]: { + address?: string; + symbol?: string; + name?: string; + decimals?: number; + extensions?: { + coingecko_id?: string; + website?: string; + twitter?: string; + discord?: string; + medium?: string; + }; + logo_uri?: string; + }; + }; +} + +export interface TokenTradeDataMultiParams { + list_addresses: string; +} + +export interface TokenTradeDataMultiResponse { + success: boolean; + data: { + [address: string]: TokenTradeData; + }; +} + +// Token Metadata Single Types +export interface TokenMetadataSingleParams { + address: string; +} + +export interface TokenMetadataSingleResponse { + success: boolean; + data: { + address?: string; + symbol?: string; + name?: string; + decimals?: number; + extensions?: { + coingecko_id?: string; + website?: string; + twitter?: string; + discord?: string; + medium?: string; + }; + logo_uri?: string; + }; +} + +// Token Market Data Types +export interface TokenMarketDataParams { + address: string; +} + +export interface TokenMarketDataResponse { + success: boolean; + data: { + address?: string; + liquidity?: number; + price?: number; + supply?: number; + marketcap?: number; + circulating_supply?: number; + circulating_marketcap?: number; + }; +} + +// Token Trade Data Single Types +export interface TokenTradeDataSingleParams { + address: string; +} + +export interface TokenTradeDataSingleResponse { + success: boolean; + data: TokenTradeData; +} + +// Token Market Stats Types +export interface TokenMarketStatsResponse { + success: boolean; + data: { + address: string; + liquidity: number; + price: number; + supply: number; + marketcap: number; + circulating_supply: number; + circulating_marketcap: number; + }; +} + +// Token Holders Types +export interface TokenHoldersParams { + address: string; + offset?: number; + limit?: number; +} + +export interface TokenHoldersResponse { + success: boolean; + data: { + items: Array<{ + amount?: string; + decimals?: number; + mint?: string; + owner?: string; + token_account?: string; + ui_amount?: number; + }>; + }; +} + +// Token Mint Burn Types +export interface MintBurnParams { + address: string; + sort_by: "block_time"; + sort_type: "asc" | "desc"; + type: "mint" | "burn" | "all"; + after_time?: number; + before_time?: number; + offset?: number; + limit?: number; +} + +export interface MintBurnResponse { + success: boolean; + data: { + items: Array<{ + amount?: string; + block_human_time?: string; + block_time?: number; + common_type?: "mint" | "burn"; + decimals?: number; + mint?: string; + program_id?: string; + slot?: number; + tx_hash?: string; + ui_amount?: number; + ui_amount_string?: string; + }>; + }; +} + +// New Listing Types +export interface NewListingParams { + time_to: number; + meme_platform_enabled: boolean; + limit?: number; +} + +export interface NewListingResponse { + success: boolean; + data: { + items: Array<{ + address: string; + symbol: string; + name: string; + decimals: number; + source: string; + liquidityAddedAt: string; + logoURI: string | null; + liquidity: number; + }>; + }; +} + +// Top Traders Types +export interface TopTradersParams { + address: string; + time_frame?: TimeInterval; + sort_type?: "asc" | "desc"; + sort_by?: "volume" | "trade"; + offset?: number; + limit?: number; +} + +export interface TopTradersResponse { + success: boolean; + data: { + items: Array<{ + trader: string; + volume24h: number; + trades24h: number; + profit24h: number; + }>; + total: number; + }; +} + +// All Markets Types +export interface AllMarketsParams { + address: string; + time_frame: TimeInterval; + sort_type: "asc" | "desc"; + sort_by: "volume24h" | "liquidity"; + offset?: number; + limit?: number; +} + +export interface AllMarketsResponse { + success: boolean; + data: { + items: Array<{ + address: string; + base: { + address: string; + decimals: number; + symbol: string; + icon?: string; + }; + quote: { + address: string; + decimals: number; + symbol: string; + icon?: string; + }; + createdAt: string; + liquidity: number; + name: string; + price: number | null; + source: string; + trade24h: number; + trade24hChangePercent: number; + uniqueWallet24h: number; + uniqueWallet24hChangePercent: number; + volume24h: number; + }>; + total: number; + }; +} + +// Token Volume By Owner Types +export interface TokenVolumeByOwnerResponse { + success: boolean; + data: { + items: Array<{ + tokenAddress: string; + owner: string; + tags: string[]; + type: string; + volume: number; + trade: number; + tradeBuy: number; + tradeSell: number; + volumeBuy: number; + volumeSell: number; + }>; + }; +} diff --git a/packages/plugin-birdeye/src/types/api/trader.ts b/packages/plugin-birdeye/src/types/api/trader.ts new file mode 100644 index 00000000000..c338f2fad4f --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/trader.ts @@ -0,0 +1,75 @@ +// Trader Gainers Losers Types +export interface GainersLosersParams { + type: "yesterday" | "today" | "1W"; + sort_by: "PnL"; + sort_type: "asc" | "desc"; + offset?: number; + limit?: number; +} + +export interface GainersLosersResponse { + success: boolean; + data: { + items: Array<{ + network?: string; + address?: string; + pnl?: number; + trade_count?: number; + volume?: number; + }>; + }; +} + +// Trader Transactions Seek Types +export interface TraderTransactionsSeekParams { + address: string; + offset?: number; + limit?: number; + tx_type?: "swap" | "add" | "remove" | "all"; + before_time?: number; + after_time?: number; +} + +export interface TraderTransactionsSeekResponse { + success: boolean; + data: { + items: Array<{ + quote?: { + symbol?: string; + decimals?: number; + address?: string; + amount?: number; + type?: string; + type_swap?: "from" | "to"; + ui_amount?: number; + price?: number | null; + nearest_price?: number; + change_amount?: number; + ui_change_amount?: number; + }; + base?: { + symbol?: string; + decimals?: number; + address?: string; + amount?: number; + type?: string; + type_swap?: "from" | "to"; + fee_info?: any | null; + ui_amount?: number; + price?: number | null; + nearest_price?: number; + change_amount?: number; + ui_change_amount?: number; + }; + base_price?: number | null; + quote_price?: number | null; + tx_hash?: string; + source?: string; + block_unix_time?: number; + tx_type?: string; + address?: string; + owner?: string; + }>; + hasNext?: boolean; + }; +} diff --git a/packages/plugin-birdeye/src/types/api/wallet.ts b/packages/plugin-birdeye/src/types/api/wallet.ts new file mode 100644 index 00000000000..ff3c06b8232 --- /dev/null +++ b/packages/plugin-birdeye/src/types/api/wallet.ts @@ -0,0 +1,180 @@ +// Wallet Portfolio Types +export interface WalletPortfolioParams { + wallet: string; +} + +export interface WalletPortfolioResponse { + success: boolean; + data: { + wallet?: string; + totalUsd?: number; + items: Array<{ + address?: string; + name?: string; + symbol?: string; + decimals?: number; + balance?: string; + uiAmount?: number; + chainId?: string; + logoURI?: string; + priceUsd?: number; + valueUsd?: number; + }>; + }; +} + +// Wallet Token Balance Types +export interface WalletTokenBalanceParams { + wallet: string; + token_address: string; +} + +export interface WalletTokenBalanceResponse { + success: boolean; + data: { + address?: string; + name?: string; + symbol?: string; + decimals?: number; + balance?: number; + uiAmount?: number; + chainId?: string; + priceUsd?: number; + valueUsd?: number; + }; +} + +// Wallet Transaction History Types +export interface WalletTransactionHistoryParams { + wallet: string; + limit?: number; + before?: string; +} + +export interface WalletTransactionHistoryResponse { + success: boolean; + data: { + [chain: string]: Array<{ + txHash?: string; + blockNumber?: number; + blockTime?: string; + status?: boolean; + from?: string; + to?: string; + gasUsed?: number; + gasPrice?: number; + fee?: string; + feeUsd?: number; + value?: string; + contractLabel?: { + address?: string; + name?: string; + metadata?: Record; + }; + mainAction?: string; + balanceChange?: Array<{ + name?: string; + symbol?: string; + logoURI?: string; + address?: string; + amount?: number; + decimals?: number; + }>; + }>; + }; +} + +// Wallet Networks Types +export interface WalletNetworksResponse { + success: boolean; + data: { + chains?: string[]; + }; +} + +// Wallet Portfolio Multichain Types +export interface WalletPortfolioMultichainParams { + wallet: string; +} + +export interface WalletPortfolioMultichainResponse { + success: boolean; + data: { + items: Array<{ + chain?: string; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + price?: number; + priceChange24h?: number; + value?: number; + amount?: number; + }>; + total?: number; + totalValue?: number; + }; +} + +// Wallet Transaction History Multichain Types +export interface WalletTransactionHistoryMultichainParams { + wallet: string; +} + +export interface WalletTransactionHistoryMultichainResponse { + success: boolean; + data: { + [chain: string]: Array<{ + txHash?: string; + blockNumber?: number; + blockTime?: string; + status?: boolean; + from?: string; + to?: string; + gasUsed?: number; + gasPrice?: number; + fee?: string; + feeUsd?: number; + value?: string; + contractLabel?: { + address?: string; + name?: string; + metadata?: Record; + }; + mainAction?: string; + balanceChange?: Array<{ + name?: string; + symbol?: string; + logoURI?: string; + address?: string; + amount?: number; + decimals?: number; + }>; + }>; + }; +} + +// Wallet Transaction Simulation Types +export interface WalletSimulationParams { + from?: string; + to?: string; + data?: string; + value?: string; +} + +export interface WalletSimulationResponse { + success: boolean; + data: { + balanceChange: Array<{ + index?: number; + before?: number; + after?: number; + address?: string; + name?: string; + symbol?: string; + logoURI?: string; + decimals?: number; + }>; + gasUsed?: number; + }; +} diff --git a/packages/plugin-birdeye/src/types/shared.ts b/packages/plugin-birdeye/src/types/shared.ts new file mode 100644 index 00000000000..190945b31f4 --- /dev/null +++ b/packages/plugin-birdeye/src/types/shared.ts @@ -0,0 +1,23 @@ +import { BIRDEYE_SUPPORTED_CHAINS } from "../utils"; + +// Types +export type BirdeyeSupportedChain = (typeof BIRDEYE_SUPPORTED_CHAINS)[number]; + +export interface BaseAddress { + type?: "wallet" | "token" | "contract"; + symbol?: string; + address: string; + chain: BirdeyeSupportedChain; +} + +export interface WalletAddress extends BaseAddress { + type: "wallet"; +} + +export interface TokenAddress extends BaseAddress { + type: "token"; +} + +export interface ContractAddress extends BaseAddress { + type: "contract"; +} diff --git a/packages/plugin-birdeye/src/utils.ts b/packages/plugin-birdeye/src/utils.ts new file mode 100644 index 00000000000..024c5258029 --- /dev/null +++ b/packages/plugin-birdeye/src/utils.ts @@ -0,0 +1,613 @@ +import { elizaLogger } from "@elizaos/core"; +import { BirdeyeApiParams } from "./types/api/common"; +import { TokenMarketSearchResponse, TokenResult } from "./types/api/search"; +import { TokenMetadataSingleResponse } from "./types/api/token"; +import { WalletPortfolioResponse } from "./types/api/wallet"; +import { BaseAddress, BirdeyeSupportedChain } from "./types/shared"; + +// Constants +export const BASE_URL = "https://public-api.birdeye.so"; + +export const BIRDEYE_SUPPORTED_CHAINS = [ + "solana", + "ethereum", + "arbitrum", + "avalanche", + "bsc", + "optimism", + "polygon", + "base", + "zksync", + "sui", + "solana", + "evm", // EVM-compatible chains but we don't know the chain +] as const; + +// Chain abbreviations and alternative names mapping +export const CHAIN_ALIASES: Record = { + // Solana + sol: "solana", + + // Ethereum + eth: "ethereum", + ether: "ethereum", + + // Arbitrum + arb: "arbitrum", + arbitrumone: "arbitrum", + + // Avalanche + avax: "avalanche", + + // BSC + bnb: "bsc", + binance: "bsc", + "binance smart chain": "bsc", + + // Optimism + op: "optimism", + opti: "optimism", + + // Polygon + matic: "polygon", + poly: "polygon", + + // Base + // no common abbreviations + + // zkSync + zks: "zksync", + zk: "zksync", + + // Sui + // no common abbreviations +} as const; + +export class BirdeyeApiError extends Error { + constructor( + public status: number, + message: string + ) { + super(message); + this.name = "BirdeyeApiError"; + } +} + +export interface ApiResponse { + success: boolean; + data: T; + error?: string; +} + +// Time-related types and constants +export const TIME_UNITS = { + second: 1, + minute: 60, + hour: 3600, + day: 86400, + week: 604800, + month: 2592000, +} as const; + +export const TIMEFRAME_KEYWORDS = { + "1m": 60, + "3m": 180, + "5m": 300, + "15m": 900, + "30m": 1800, + "1h": 3600, + "2h": 7200, + "4h": 14400, + "6h": 21600, + "12h": 43200, + "1d": 86400, + "1w": 604800, +} as const; + +export type TimeUnit = keyof typeof TIME_UNITS; +export type Timeframe = keyof typeof TIMEFRAME_KEYWORDS; + +// Helper functions +export const extractChain = (text: string): BirdeyeSupportedChain => { + // Check for SUI address (0x followed by 64 hex chars) + if (text.match(/0x[a-fA-F0-9]{64}/)) { + return "sui"; + } + // Check for EVM address (0x followed by 40 hex chars) + if (text.match(/0x[a-fA-F0-9]{40}/)) { + return "ethereum"; + } + // Default to solana + return "solana"; +}; + +export const extractAddresses = (text: string): BaseAddress[] => { + const addresses: BaseAddress[] = []; + + // EVM-compatible chains (Ethereum, Arbitrum, Avalanche, BSC, Optimism, Polygon, Base, zkSync) + const evmAddresses = text.match(/0x[a-fA-F0-9]{40}/g); + if (evmAddresses) { + addresses.push( + ...evmAddresses.map((address) => ({ + address, + chain: "evm" as BirdeyeSupportedChain, // we don't yet know the chain but can assume it's EVM-compatible + })) + ); + } + + // Solana addresses (base58 strings) + const solAddresses = text.match(/[1-9A-HJ-NP-Za-km-z]{32,44}/g); + if (solAddresses) { + addresses.push( + ...solAddresses.map((address) => ({ + address, + chain: "solana" as BirdeyeSupportedChain, + })) + ); + } + + // Sui addresses (0x followed by 64 hex chars) + const suiAddresses = text.match(/0x[a-fA-F0-9]{64}/g); + if (suiAddresses) { + addresses.push( + ...suiAddresses.map((address) => ({ + address, + chain: "sui" as BirdeyeSupportedChain, + })) + ); + } + + return addresses; +}; + +// Time extraction and analysis +export const extractTimeframe = (text: string): Timeframe => { + // First, check for explicit timeframe mentions + const timeframe = Object.keys(TIMEFRAME_KEYWORDS).find((tf) => + text.toLowerCase().includes(tf.toLowerCase()) + ); + if (timeframe) return timeframe as Timeframe; + + // Check for semantic timeframe hints + const semanticMap = { + "short term": "15m", + "medium term": "1h", + "long term": "1d", + intraday: "1h", + daily: "1d", + weekly: "1w", + detailed: "5m", + quick: "15m", + overview: "1d", + } as const; + + for (const [hint, tf] of Object.entries(semanticMap)) { + if (text.toLowerCase().includes(hint)) { + return tf as Timeframe; + } + } + + // Analyze for time-related words + if (text.match(/minute|min|minutes/i)) return "15m"; + if (text.match(/hour|hourly|hours/i)) return "1h"; + if (text.match(/day|daily|24h/i)) return "1d"; + if (text.match(/week|weekly/i)) return "1w"; + + // Default based on context + if (text.match(/trade|trades|trading|recent/i)) return "15m"; + if (text.match(/trend|analysis|analyze/i)) return "1h"; + if (text.match(/history|historical|long|performance/i)) return "1d"; + + return "1h"; // Default timeframe +}; + +export const extractTimeRange = ( + text: string +): { start: number; end: number } => { + const now = Math.floor(Date.now() / 1000); + + // Check for specific date ranges + const dateRangeMatch = text.match( + /from\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})/i + ); + if (dateRangeMatch) { + const start = new Date(dateRangeMatch[1]).getTime() / 1000; + const end = new Date(dateRangeMatch[2]).getTime() / 1000; + return { start, end }; + } + + // Check for relative time expressions + const timeRegex = /(\d+)\s*(second|minute|hour|day|week|month)s?\s*ago/i; + const match = text.match(timeRegex); + if (match) { + const amount = parseInt(match[1]); + const unit = match[2].toLowerCase() as TimeUnit; + const start = now - amount * TIME_UNITS[unit]; + return { start, end: now }; + } + + // Check for semantic time ranges + const semanticRanges: Record = { + today: TIME_UNITS.day, + "this week": TIME_UNITS.week, + "this month": TIME_UNITS.month, + recent: TIME_UNITS.hour * 4, + latest: TIME_UNITS.hour, + "last hour": TIME_UNITS.hour, + "last day": TIME_UNITS.day, + "last week": TIME_UNITS.week, + "last month": TIME_UNITS.month, + }; + + for (const [range, duration] of Object.entries(semanticRanges)) { + if (text.toLowerCase().includes(range)) { + return { start: now - duration, end: now }; + } + } + + // Analyze context for appropriate default range + if (text.match(/trend|analysis|performance/i)) { + return { start: now - TIME_UNITS.week, end: now }; // 1 week for analysis + } + if (text.match(/trade|trades|trading|recent/i)) { + return { start: now - TIME_UNITS.day, end: now }; // 1 day for trading + } + if (text.match(/history|historical|long term/i)) { + return { start: now - TIME_UNITS.month, end: now }; // 1 month for history + } + + // Default to last 24 hours + return { start: now - TIME_UNITS.day, end: now }; +}; + +export const extractLimit = (text: string): number => { + // Check for explicit limit mentions + const limitMatch = text.match( + /\b(show|display|get|fetch|limit)\s+(\d+)\b/i + ); + if (limitMatch) { + const limit = parseInt(limitMatch[2]); + return Math.min(Math.max(limit, 1), 100); // Clamp between 1 and 100 + } + + // Check for semantic limit hints + if (text.match(/\b(all|everything|full|complete)\b/i)) return 100; + if (text.match(/\b(brief|quick|summary|overview)\b/i)) return 5; + if (text.match(/\b(detailed|comprehensive)\b/i)) return 50; + + // Default based on context + if (text.match(/\b(trade|trades|trading)\b/i)) return 10; + if (text.match(/\b(analysis|analyze|trend)\b/i)) return 24; + if (text.match(/\b(history|historical)\b/i)) return 50; + + return 10; // Default limit +}; + +// Formatting helpers +export const formatValue = (value?: number): string => { + if (!value) return "N/A"; + if (value && value >= 1_000_000_000) { + return `$${(value / 1_000_000_000).toFixed(2)}B`; + } + if (value >= 1_000_000) { + return `$${(value / 1_000_000).toFixed(2)}M`; + } + if (value >= 1_000) { + return `$${(value / 1_000).toFixed(2)}K`; + } + return `$${value.toFixed(2)}`; +}; + +export const formatPercentChange = (change?: number): string => { + if (change === undefined) return "N/A"; + const symbol = change >= 0 ? "ā†‘" : "ā†“"; + return `${symbol} ${Math.abs(change).toFixed(2)}%`; +}; + +export const shortenAddress = (address?: string): string => { + if (!address || address.length <= 12) return address || "Unknown"; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +}; + +export const formatTimestamp = (timestamp?: number): string => { + return timestamp ? new Date(timestamp * 1000).toLocaleString() : "N/A"; +}; + +export const formatPrice = (price?: number): string => { + return price + ? price < 0.01 + ? price.toExponential(2) + : price.toFixed(2) + : "N/A"; +}; + +// API helpers +export async function makeApiRequest( + url: string, + options: { + apiKey: string; + chain?: BirdeyeSupportedChain; + method?: "GET" | "POST"; + body?: any; + } +): Promise { + const { apiKey, chain = "solana", method = "GET", body } = options; + + try { + const response = await fetch(url, { + method, + headers: { + "X-API-KEY": apiKey, + "x-chain": chain, + ...(body && { "Content-Type": "application/json" }), + }, + ...(body && { body: JSON.stringify(body) }), + }); + + if (!response.ok) { + if (response.status === 404) { + throw new BirdeyeApiError(404, "Resource not found"); + } + if (response.status === 429) { + throw new BirdeyeApiError(429, "Rate limit exceeded"); + } + throw new BirdeyeApiError( + response.status, + `HTTP error! status: ${response.status}` + ); + } + + const responseJson: T = await response.json(); + + return responseJson; + } catch (error) { + if (error instanceof BirdeyeApiError) { + elizaLogger.error(`API Error (${error.status}):`, error.message); + } else { + elizaLogger.error("Error making API request:", error); + } + throw error; + } +} + +// Formatting helpers +export const formatTokenInfo = ( + token: TokenResult, + metadata?: TokenMetadataSingleResponse +): string => { + const priceFormatted = + token.price != null + ? token.price < 0.01 + ? token.price.toExponential(2) + : token.price.toFixed(2) + : "N/A"; + + const volume = + token.volume_24h_usd != null + ? `$${(token.volume_24h_usd / 1_000_000).toFixed(2)}M` + : "N/A"; + + const liquidity = + token.liquidity != null + ? `$${(token.liquidity / 1_000_000).toFixed(2)}M` + : "N/A"; + + const fdv = + token.fdv != null ? `$${(token.fdv / 1_000_000).toFixed(2)}M` : "N/A"; + + const priceChange = + token.price_change_24h_percent != null + ? `${token.price_change_24h_percent > 0 ? "+" : ""}${token.price_change_24h_percent.toFixed(2)}%` + : "N/A"; + + const trades = token.trade_24h != null ? token.trade_24h.toString() : "N/A"; + + const age = token.creation_time + ? `${Math.floor((Date.now() - new Date(token.creation_time).getTime()) / (1000 * 60 * 60 * 24))}d` + : "N/A"; + + let output = + `šŸŖ™ ${token.name} @ ${token.symbol}\n` + + `šŸ’° USD: $${priceFormatted} (${priceChange})\n` + + `šŸ’Ž FDV: ${fdv}\n` + + `šŸ’¦ MCap: ${token.market_cap ? `$${(token.market_cap / 1_000_000).toFixed(2)}M` : "N/A"}\n` + + `šŸ’¦ Liq: ${liquidity}\n` + + `šŸ“Š Vol: ${volume}\n` + + `šŸ•°ļø Age: ${age}\n` + + `šŸ”„ Trades: ${trades}\n` + + `šŸ”— Address: ${token.address}`; + + // Add metadata if available + if (metadata?.success) { + const { extensions } = metadata.data; + const links: string[] = []; + + if (extensions.website) + links.push(`šŸŒ [Website](${extensions.website})`); + if (extensions.twitter) + links.push(`šŸ¦ [Twitter](${extensions.twitter})`); + if (extensions.discord) + links.push(`šŸ’¬ [Discord](${extensions.discord})`); + if (extensions.medium) links.push(`šŸ“ [Medium](${extensions.medium})`); + if (extensions.coingecko_id) + links.push( + `šŸ¦Ž [CoinGecko](https://www.coingecko.com/en/coins/${extensions.coingecko_id})` + ); + + if (links.length > 0) { + output += "\n\nšŸ“± Social Links:\n" + links.join("\n"); + } + } + + return output; +}; + +// Extract symbols from text +export const extractSymbols = ( + text: string, + // loose mode will try to extract more symbols but may include false positives + // strict mode will only extract symbols that are clearly formatted as a symbol using $SOL format + mode: "strict" | "loose" = "loose" +): string[] => { + const symbols = new Set(); + + // Match patterns - this may + const patterns = + mode === "strict" + ? [ + // $SYMBOL format + /\$([A-Z0-9]{2,10})\b/gi, + // $SYMBOL format with lowercase + /\$([a-z0-9]{2,10})\b/gi, + ] + : [ + // $SYMBOL format + /\$([A-Z0-9]{2,10})\b/gi, + // After articles (a/an) + /\b(?:a|an)\s+([A-Z0-9]{2,10})\b/gi, + // // Standalone caps + /\b[A-Z0-9]{2,10}\b/g, + // // Quoted symbols + /["']([A-Z0-9]{2,10})["']/gi, + // // Common price patterns + /\b([A-Z0-9]{2,10})\/USD\b/gi, + /\b([A-Z0-9]{2,10})-USD\b/gi, + ]; + + // Extract all matches + patterns.forEach((pattern) => { + const matches = text.matchAll(pattern); + for (const match of matches) { + const symbol = (match[1] || match[0]).toUpperCase(); + symbols.add(symbol); + } + }); + + return Array.from(symbols); +}; + +export const formatMetadataResponse = ( + data: TokenMetadataSingleResponse, + chain: BirdeyeSupportedChain +): string => { + const tokenData = data.data; + const chainName = chain.charAt(0).toUpperCase() + chain.slice(1); + const chainExplorer = (() => { + switch (chain) { + case "solana": + return `https://solscan.io/token/${tokenData.address}`; + case "ethereum": + return `https://etherscan.io/token/${tokenData.address}`; + case "arbitrum": + return `https://arbiscan.io/token/${tokenData.address}`; + case "avalanche": + return `https://snowtrace.io/token/${tokenData.address}`; + case "bsc": + return `https://bscscan.com/token/${tokenData.address}`; + case "optimism": + return `https://optimistic.etherscan.io/token/${tokenData.address}`; + case "polygon": + return `https://polygonscan.com/token/${tokenData.address}`; + case "base": + return `https://basescan.org/token/${tokenData.address}`; + case "zksync": + return `https://explorer.zksync.io/address/${tokenData.address}`; + case "sui": + return `https://suiscan.xyz/mainnet/object/${tokenData.address}`; + default: + return null; + } + })(); + + let response = `Token Metadata for ${tokenData.name} (${tokenData.symbol}) on ${chainName}\n\n`; + + // Basic Information + response += "šŸ“ Basic Information\n"; + response += `ā€¢ Name: ${tokenData.name}\n`; + response += `ā€¢ Symbol: ${tokenData.symbol}\n`; + response += `ā€¢ Address: ${tokenData.address}\n`; + response += `ā€¢ Decimals: ${tokenData.decimals}\n`; + if (chainExplorer) { + response += `ā€¢ Explorer: [View on ${chainName} Explorer](${chainExplorer})\n`; + } + + // Social Links + response += "\nšŸ”— Social Links & Extensions\n"; + response += formatSocialLinks(tokenData) + "\n"; + + // Logo + if (tokenData.logo_uri) { + response += "\nšŸ–¼ļø Logo\n"; + response += tokenData.logo_uri; + } + + return response; +}; + +const formatSocialLinks = ( + data: TokenMetadataSingleResponse["data"] +): string => { + const links: string[] = []; + const { extensions } = data; + + if (!extensions) { + return "No social links available"; + } + + if (extensions.website) { + links.push(`šŸŒ [Website](${extensions.website})`); + } + if (extensions.twitter) { + links.push(`šŸ¦ [Twitter](${extensions.twitter})`); + } + if (extensions.discord) { + links.push(`šŸ’¬ [Discord](${extensions.discord})`); + } + if (extensions.medium) { + links.push(`šŸ“ [Medium](${extensions.medium})`); + } + if (extensions.coingecko_id) { + links.push( + `šŸ¦Ž [CoinGecko](https://www.coingecko.com/en/coins/${extensions.coingecko_id})` + ); + } + + return links.length > 0 ? links.join("\n") : "No social links available"; +}; + +export const waitFor = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +export const formatPortfolio = (response: WalletPortfolioResponse) => { + const { items } = response.data; + if (!items?.length) return "No tokens found in portfolio"; + + return items + .map((item) => { + const value = item?.priceUsd?.toFixed(2); + const amount = item?.uiAmount?.toFixed(4); + return ( + `ā€¢ ${item.symbol || "Unknown Token"}: ${amount} tokens` + + `${value !== "0.00" ? ` (Value: $${value || "unknown"})` : ""}` + ); + }) + .join("\n"); +}; + +export const convertToStringParams = (params: BirdeyeApiParams) => { + return Object.entries(params || {}).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value?.toString() || "", + }), + {} as Record + ); +}; + +export const getTokenResultFromSearchResponse = ( + response: TokenMarketSearchResponse +): TokenResult[] | undefined => { + return response.data.items + .filter((item) => item.type === "token") + .flatMap((item) => item.result); +}; diff --git a/packages/plugin-birdeye/tsconfig.json b/packages/plugin-birdeye/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /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 00000000000..dd25475bb63 --- /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 + ], +});