@elizaos/plugin-wallet 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/auto-enable.ts +76 -0
  4. package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
  5. package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
  6. package/dist/aerodrome-CfnESC32.mjs +890 -0
  7. package/dist/chunk-hT5z_Zn9.mjs +35 -0
  8. package/dist/index.d.mts +34727 -0
  9. package/dist/index.mjs +21590 -0
  10. package/dist/lib/server-wallet-trade.d.mts +34 -0
  11. package/dist/lib/server-wallet-trade.mjs +306 -0
  12. package/dist/meteora-BPX39hZo.mjs +22640 -0
  13. package/dist/orca-Bybp1HXO.mjs +249 -0
  14. package/dist/pancakeswp-CkEXlXti.mjs +604 -0
  15. package/dist/plugin-ZO_MTyd0.mjs +529 -0
  16. package/dist/raydium-rfaM9yEf.mjs +539 -0
  17. package/dist/sdk/index.d.mts +32492 -0
  18. package/dist/sdk/index.mjs +6415 -0
  19. package/dist/types-D5252NZk.mjs +487 -0
  20. package/dist/uniswap-CReXgXVN.mjs +573 -0
  21. package/dist/wallet-action.d.mts +6 -0
  22. package/dist/wallet-action.mjs +820 -0
  23. package/package.json +152 -0
  24. package/src/actions/failure-codes.ts +79 -0
  25. package/src/actions/index.ts +1 -0
  26. package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
  27. package/src/analytics/birdeye/birdeye-task.ts +175 -0
  28. package/src/analytics/birdeye/birdeye.ts +813 -0
  29. package/src/analytics/birdeye/constants.ts +74 -0
  30. package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
  31. package/src/analytics/birdeye/providers/market.ts +227 -0
  32. package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
  33. package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
  34. package/src/analytics/birdeye/providers/trending.ts +365 -0
  35. package/src/analytics/birdeye/providers/wallet.ts +14 -0
  36. package/src/analytics/birdeye/search-category.test.ts +207 -0
  37. package/src/analytics/birdeye/search-category.ts +506 -0
  38. package/src/analytics/birdeye/service.ts +992 -0
  39. package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
  40. package/src/analytics/birdeye/types/api/common.ts +305 -0
  41. package/src/analytics/birdeye/types/api/defi.ts +220 -0
  42. package/src/analytics/birdeye/types/api/pair.ts +200 -0
  43. package/src/analytics/birdeye/types/api/search.ts +86 -0
  44. package/src/analytics/birdeye/types/api/token.ts +635 -0
  45. package/src/analytics/birdeye/types/api/trader.ts +76 -0
  46. package/src/analytics/birdeye/types/api/wallet.ts +181 -0
  47. package/src/analytics/birdeye/types/shared.ts +106 -0
  48. package/src/analytics/birdeye/utils.ts +700 -0
  49. package/src/analytics/dexscreener/errors.ts +28 -0
  50. package/src/analytics/dexscreener/index.ts +3 -0
  51. package/src/analytics/dexscreener/search-category.test.ts +49 -0
  52. package/src/analytics/dexscreener/search-category.ts +42 -0
  53. package/src/analytics/dexscreener/service.ts +595 -0
  54. package/src/analytics/dexscreener/types.ts +128 -0
  55. package/src/analytics/lpinfo/index.d.ts +7 -0
  56. package/src/analytics/lpinfo/index.ts +52 -0
  57. package/src/analytics/lpinfo/kamino/README.md +102 -0
  58. package/src/analytics/lpinfo/kamino/index.ts +24 -0
  59. package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
  60. package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
  61. package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
  62. package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
  63. package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
  64. package/src/analytics/lpinfo/steer/README.md +169 -0
  65. package/src/analytics/lpinfo/steer/index.ts +23 -0
  66. package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
  67. package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
  68. package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
  69. package/src/analytics/news/index.ts +52 -0
  70. package/src/analytics/news/interfaces/types.ts +222 -0
  71. package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
  72. package/src/analytics/news/services/newsDataService.ts +332 -0
  73. package/src/analytics/news/utils/formatters.ts +151 -0
  74. package/src/analytics/token-info/action.ts +240 -0
  75. package/src/analytics/token-info/index.ts +3 -0
  76. package/src/analytics/token-info/params.ts +215 -0
  77. package/src/analytics/token-info/providers.ts +681 -0
  78. package/src/analytics/token-info/service.ts +168 -0
  79. package/src/analytics/token-info/types.ts +74 -0
  80. package/src/audit/audit-log.ts +45 -0
  81. package/src/browser-shim/build-shim.ts +123 -0
  82. package/src/browser-shim/index.ts +5 -0
  83. package/src/browser-shim/shim.template.js +563 -0
  84. package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
  85. package/src/chains/evm/LICENSE +21 -0
  86. package/src/chains/evm/README.md +106 -0
  87. package/src/chains/evm/actions/helpers.ts +147 -0
  88. package/src/chains/evm/actions/swap.ts +839 -0
  89. package/src/chains/evm/actions/transfer.ts +254 -0
  90. package/src/chains/evm/biome.json +61 -0
  91. package/src/chains/evm/bridge-router.ts +660 -0
  92. package/src/chains/evm/build.ts +89 -0
  93. package/src/chains/evm/chain-handler.ts +416 -0
  94. package/src/chains/evm/constants.ts +23 -0
  95. package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
  96. package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
  97. package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
  98. package/src/chains/evm/dex/aerodrome/index.ts +34 -0
  99. package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
  100. package/src/chains/evm/dex/aerodrome/types.ts +318 -0
  101. package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
  102. package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
  103. package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
  104. package/src/chains/evm/dex/uniswap/index.ts +35 -0
  105. package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
  106. package/src/chains/evm/dex/uniswap/types.ts +390 -0
  107. package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
  108. package/src/chains/evm/generated/specs/specs.ts +151 -0
  109. package/src/chains/evm/gov-router.ts +250 -0
  110. package/src/chains/evm/index.browser.ts +16 -0
  111. package/src/chains/evm/index.ts +31 -0
  112. package/src/chains/evm/prompts.ts +193 -0
  113. package/src/chains/evm/providers/get-balance.ts +123 -0
  114. package/src/chains/evm/providers/wallet.ts +715 -0
  115. package/src/chains/evm/routes/sign.ts +333 -0
  116. package/src/chains/evm/rpc-providers.ts +410 -0
  117. package/src/chains/evm/service.ts +140 -0
  118. package/src/chains/evm/templates/index.ts +10 -0
  119. package/src/chains/evm/types/index.ts +432 -0
  120. package/src/chains/evm/vitest.config.ts +18 -0
  121. package/src/chains/registry.ts +668 -0
  122. package/src/chains/solana/README.md +367 -0
  123. package/src/chains/wallet-action.ts +533 -0
  124. package/src/chains/wallet-router.test.ts +296 -0
  125. package/src/contracts.ts +65 -0
  126. package/src/core-augmentation.ts +10 -0
  127. package/src/index.ts +71 -0
  128. package/src/lib/server-wallet-trade.ts +192 -0
  129. package/src/lib/wallet-export-guard.ts +330 -0
  130. package/src/lp/actions/liquidity.ts +827 -0
  131. package/src/lp/e2e/real-token-tests.ts +428 -0
  132. package/src/lp/e2e/scenarios.ts +470 -0
  133. package/src/lp/e2e/test-utils.ts +145 -0
  134. package/src/lp/lp-manager-entry.ts +303 -0
  135. package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
  136. package/src/lp/services/DexInteractionService.ts +226 -0
  137. package/src/lp/services/LpManagementService.test.ts +148 -0
  138. package/src/lp/services/LpManagementService.ts +632 -0
  139. package/src/lp/services/UserLpProfileService.ts +163 -0
  140. package/src/lp/services/VaultService.ts +153 -0
  141. package/src/lp/services/YieldOptimizationService.ts +344 -0
  142. package/src/lp/services/__tests__/MockLpService.ts +146 -0
  143. package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
  144. package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
  145. package/src/lp/types.ts +582 -0
  146. package/src/lp/utils/solanaClient.ts +143 -0
  147. package/src/plugin.ts +125 -0
  148. package/src/policy/policy.ts +19 -0
  149. package/src/providers/canonical-provider.ts +27 -0
  150. package/src/providers/unified-wallet-provider.ts +79 -0
  151. package/src/register-routes.ts +11 -0
  152. package/src/routes/plugin.ts +47 -0
  153. package/src/routes/wallet-market-overview-route.ts +869 -0
  154. package/src/sdk/abi.ts +258 -0
  155. package/src/sdk/bridge/abis.ts +126 -0
  156. package/src/sdk/bridge/client.ts +518 -0
  157. package/src/sdk/bridge/index.ts +56 -0
  158. package/src/sdk/bridge/solana.ts +604 -0
  159. package/src/sdk/bridge/types.ts +202 -0
  160. package/src/sdk/convenience.ts +347 -0
  161. package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
  162. package/src/sdk/escrow/types.ts +64 -0
  163. package/src/sdk/escrow/verifiers.ts +73 -0
  164. package/src/sdk/identity/erc8004.ts +692 -0
  165. package/src/sdk/identity/reputation.ts +449 -0
  166. package/src/sdk/identity/uaid.ts +497 -0
  167. package/src/sdk/identity/validation.ts +372 -0
  168. package/src/sdk/index.ts +763 -0
  169. package/src/sdk/policy/SpendingPolicy.ts +260 -0
  170. package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
  171. package/src/sdk/router/PaymentRouter.ts +215 -0
  172. package/src/sdk/router/index.ts +8 -0
  173. package/src/sdk/swap/SwapModule.ts +310 -0
  174. package/src/sdk/swap/abi.ts +117 -0
  175. package/src/sdk/swap/index.ts +34 -0
  176. package/src/sdk/swap/types.ts +135 -0
  177. package/src/sdk/tokens/decimals.ts +140 -0
  178. package/src/sdk/tokens/registry.ts +911 -0
  179. package/src/sdk/tokens/solana.ts +419 -0
  180. package/src/sdk/tokens/transfers.ts +327 -0
  181. package/src/sdk/types.ts +158 -0
  182. package/src/sdk/wallet-core.ts +115 -0
  183. package/src/sdk/x402/budget.ts +168 -0
  184. package/src/sdk/x402/chains/abstract/index.ts +280 -0
  185. package/src/sdk/x402/client.ts +320 -0
  186. package/src/sdk/x402/index.ts +46 -0
  187. package/src/sdk/x402/middleware.ts +92 -0
  188. package/src/sdk/x402/multi-asset.ts +144 -0
  189. package/src/sdk/x402/types.ts +156 -0
  190. package/src/services/wallet-backend-service.ts +328 -0
  191. package/src/types/wallet-router.ts +227 -0
  192. package/src/utils/intent-trajectory.ts +106 -0
  193. package/src/wallet/backend.ts +62 -0
  194. package/src/wallet/errors.ts +49 -0
  195. package/src/wallet/index.ts +27 -0
  196. package/src/wallet/local-eoa-backend.ts +201 -0
  197. package/src/wallet/pending.ts +60 -0
  198. package/src/wallet/select-backend.ts +47 -0
  199. package/src/wallet/steward-backend.ts +161 -0
  200. package/src/wallet-action.ts +1 -0
@@ -0,0 +1,700 @@
1
+ // @ts-nocheck — legacy code from absorbed plugins (lp-manager, lpinfo, dexscreener, defi-news, birdeye); strict types pending cleanup
2
+ import { logger } from "@elizaos/core";
3
+ import type { BirdeyeApiParams } from "./types/api/common";
4
+ import type {
5
+ TokenMarketSearchResponse,
6
+ TokenResult,
7
+ } from "./types/api/search";
8
+ import type { TokenMetadataSingleResponse } from "./types/api/token";
9
+ import type { BaseAddress, BirdeyeSupportedChain } from "./types/shared";
10
+
11
+ // Constants
12
+ export const BASE_URL = "https://public-api.birdeye.so";
13
+
14
+ export const BIRDEYE_SUPPORTED_CHAINS = [
15
+ "solana",
16
+ "ethereum",
17
+ "arbitrum",
18
+ "avalanche",
19
+ "bsc",
20
+ "optimism",
21
+ "polygon",
22
+ "base",
23
+ "zksync",
24
+ "sui",
25
+ "evm", // EVM-compatible chains but we don't know the chain
26
+ ] as const;
27
+
28
+ // Chain abbreviations and alternative names mapping
29
+ export const CHAIN_ALIASES: Record<string, BirdeyeSupportedChain> = {
30
+ // Solana
31
+ sol: "solana",
32
+
33
+ // Ethereum
34
+ eth: "ethereum",
35
+ ether: "ethereum",
36
+
37
+ // Arbitrum
38
+ arb: "arbitrum",
39
+ arbitrumone: "arbitrum",
40
+
41
+ // Avalanche
42
+ avax: "avalanche",
43
+
44
+ // BSC
45
+ bnb: "bsc",
46
+ binance: "bsc",
47
+ "binance smart chain": "bsc",
48
+
49
+ // Optimism
50
+ op: "optimism",
51
+ opti: "optimism",
52
+
53
+ // Polygon
54
+ matic: "polygon",
55
+ poly: "polygon",
56
+
57
+ // Base
58
+ // no common abbreviations
59
+
60
+ // zkSync
61
+ zks: "zksync",
62
+ zk: "zksync",
63
+
64
+ // Sui
65
+ // no common abbreviations
66
+ } as const;
67
+
68
+ export class BirdeyeApiError extends Error {
69
+ constructor(
70
+ public status: number,
71
+ message: string,
72
+ ) {
73
+ super(message);
74
+ this.name = "BirdeyeApiError";
75
+ }
76
+ }
77
+
78
+ export interface ApiResponse<T> {
79
+ success: boolean;
80
+ data: T;
81
+ error?: string;
82
+ }
83
+
84
+ // Time-related types and constants
85
+ export const TIME_UNITS = {
86
+ second: 1,
87
+ minute: 60,
88
+ hour: 3600,
89
+ day: 86400,
90
+ week: 604800,
91
+ month: 2592000,
92
+ } as const;
93
+
94
+ export const TIMEFRAME_KEYWORDS = {
95
+ "1m": 60,
96
+ "3m": 180,
97
+ "5m": 300,
98
+ "15m": 900,
99
+ "30m": 1800,
100
+ "1h": 3600,
101
+ "2h": 7200,
102
+ "4h": 14400,
103
+ "6h": 21600,
104
+ "12h": 43200,
105
+ "1d": 86400,
106
+ "1w": 604800,
107
+ } as const;
108
+
109
+ export type TimeUnit = keyof typeof TIME_UNITS;
110
+ export type Timeframe = keyof typeof TIMEFRAME_KEYWORDS;
111
+
112
+ // Helper functions
113
+ /**
114
+ * Extract blockchain chain from address with optional explicit chain setting
115
+ * @param text - The wallet address to analyze (optional if explicitChain is provided)
116
+ * @param explicitChain - Optional explicit chain specification (from BIRDEYE_CHAIN setting)
117
+ * @returns The detected or specified chain
118
+ * @throws Error if address format is invalid or EVM address without explicit chain
119
+ */
120
+ export const extractChain = (
121
+ text?: string,
122
+ explicitChain?: string,
123
+ ): BirdeyeSupportedChain => {
124
+ // First, check for explicit chain setting
125
+ if (explicitChain) {
126
+ const normalizedChain = explicitChain.toLowerCase();
127
+ // Check if it's a valid chain or alias
128
+ if (
129
+ BIRDEYE_SUPPORTED_CHAINS.includes(
130
+ normalizedChain as BirdeyeSupportedChain,
131
+ )
132
+ ) {
133
+ return normalizedChain as BirdeyeSupportedChain;
134
+ }
135
+ // Check aliases
136
+ if (CHAIN_ALIASES[normalizedChain]) {
137
+ return CHAIN_ALIASES[normalizedChain];
138
+ }
139
+ throw new Error(
140
+ `Invalid chain: "${explicitChain}". Must be one of: ${BIRDEYE_SUPPORTED_CHAINS.join(", ")}`,
141
+ );
142
+ }
143
+
144
+ // Validate input
145
+ if (!text || typeof text !== "string" || text.trim().length === 0) {
146
+ throw new Error("Invalid address: empty or non-string value provided");
147
+ }
148
+
149
+ const trimmedText = text.trim();
150
+
151
+ // Check for SUI address (0x followed by 64 hex chars)
152
+ if (trimmedText.match(/^0x[a-fA-F0-9]{64}$/)) {
153
+ return "sui";
154
+ }
155
+
156
+ // Check for EVM address (0x followed by 40 hex chars)
157
+ if (trimmedText.match(/^0x[a-fA-F0-9]{40}$/)) {
158
+ // Build EVM chain list dynamically from supported chains (exclude solana and sui)
159
+ const evmChains = BIRDEYE_SUPPORTED_CHAINS.filter(
160
+ (chain) => chain !== "solana" && chain !== "sui",
161
+ ).join(", ");
162
+ throw new Error(
163
+ `EVM address detected but specific chain unknown. Please set BIRDEYE_CHAIN environment variable to one of: ${evmChains}`,
164
+ );
165
+ }
166
+
167
+ // Check for Solana address (base58, typically 32-44 chars, no 0x prefix)
168
+ if (trimmedText.match(/^[1-9A-HJ-NP-Za-km-z]{32,44}$/)) {
169
+ return "solana";
170
+ }
171
+
172
+ // Invalid address format
173
+ throw new Error(
174
+ `Invalid address format: "${trimmedText}". Expected Solana (base58), EVM (0x + 40 hex), or Sui (0x + 64 hex) address.`,
175
+ );
176
+ };
177
+
178
+ export const extractAddresses = (text: string): BaseAddress[] => {
179
+ if (!text?.match) return [];
180
+ const addresses: BaseAddress[] = [];
181
+
182
+ // Sui addresses (0x followed by 64 hex chars). Extract first so the EVM
183
+ // matcher does not take the 40-char prefix of a Sui address.
184
+ const suiAddresses = text.match(/0x[a-fA-F0-9]{64}(?![a-fA-F0-9])/g);
185
+ if (suiAddresses) {
186
+ addresses.push(
187
+ ...suiAddresses.map((address) => ({
188
+ address,
189
+ chain: "sui" as BirdeyeSupportedChain,
190
+ })),
191
+ );
192
+ }
193
+
194
+ // EVM-compatible chains (Ethereum, Arbitrum, Avalanche, BSC, Optimism, Polygon, Base, zkSync)
195
+ const evmAddresses = text.match(/0x[a-fA-F0-9]{40}(?![a-fA-F0-9])/g);
196
+ if (evmAddresses) {
197
+ addresses.push(
198
+ ...evmAddresses
199
+ .filter(
200
+ (address) =>
201
+ !addresses.some(
202
+ (existing) =>
203
+ existing.chain === "sui" &&
204
+ existing.address.startsWith(address),
205
+ ),
206
+ )
207
+ .map((address) => ({
208
+ address,
209
+ chain: "evm" as BirdeyeSupportedChain, // we don't yet know the chain but can assume it's EVM-compatible
210
+ })),
211
+ );
212
+ }
213
+
214
+ // Solana addresses (base58 strings)
215
+ const solAddresses = Array.from(text.matchAll(/[1-9A-HJ-NP-Za-km-z]{32,44}/g))
216
+ .filter((match) => {
217
+ const start = match.index ?? 0;
218
+ const end = start + match[0].length;
219
+ return (
220
+ !/[A-Za-z0-9]/.test(text[start - 1] ?? "") &&
221
+ !/[A-Za-z0-9]/.test(text[end] ?? "")
222
+ );
223
+ })
224
+ .map((match) => match[0]);
225
+ if (solAddresses) {
226
+ addresses.push(
227
+ ...solAddresses.map((address) => ({
228
+ address,
229
+ chain: "solana" as BirdeyeSupportedChain,
230
+ })),
231
+ );
232
+ }
233
+
234
+ return addresses;
235
+ };
236
+
237
+ // Time extraction and analysis
238
+ export const extractTimeframe = (text: string): Timeframe => {
239
+ // First, check for explicit timeframe mentions
240
+ const timeframe = Object.keys(TIMEFRAME_KEYWORDS).find((tf) =>
241
+ text.toLowerCase().includes(tf.toLowerCase()),
242
+ );
243
+ if (timeframe) return timeframe as Timeframe;
244
+
245
+ // Check for semantic timeframe hints
246
+ const semanticMap = {
247
+ "short term": "15m",
248
+ "medium term": "1h",
249
+ "long term": "1d",
250
+ intraday: "1h",
251
+ daily: "1d",
252
+ weekly: "1w",
253
+ detailed: "5m",
254
+ quick: "15m",
255
+ overview: "1d",
256
+ } as const;
257
+
258
+ for (const [hint, tf] of Object.entries(semanticMap)) {
259
+ if (text.toLowerCase().includes(hint)) {
260
+ return tf as Timeframe;
261
+ }
262
+ }
263
+
264
+ // Analyze for time-related words
265
+ if (text.match(/minute|min|minutes/i)) return "15m";
266
+ if (text.match(/hour|hourly|hours/i)) return "1h";
267
+ if (text.match(/day|daily|24h/i)) return "1d";
268
+ if (text.match(/week|weekly/i)) return "1w";
269
+
270
+ // Default based on context
271
+ if (text.match(/trade|trades|trading|recent/i)) return "15m";
272
+ if (text.match(/trend|analysis|analyze/i)) return "1h";
273
+ if (text.match(/history|historical|long|performance/i)) return "1d";
274
+
275
+ return "1h"; // Default timeframe
276
+ };
277
+
278
+ export const extractTimeRange = (
279
+ text: string,
280
+ ): { start: number; end: number } => {
281
+ const now = Math.floor(Date.now() / 1000);
282
+
283
+ // Check for specific date ranges
284
+ const dateRangeMatch = text.match(
285
+ /from\s+(\d{4}-\d{2}-\d{2})\s+to\s+(\d{4}-\d{2}-\d{2})/i,
286
+ );
287
+ if (dateRangeMatch) {
288
+ const start = new Date(dateRangeMatch[1]).getTime() / 1000;
289
+ const end = new Date(dateRangeMatch[2]).getTime() / 1000;
290
+ return { start, end };
291
+ }
292
+
293
+ // Check for relative time expressions
294
+ const timeRegex = /(\d+)\s*(second|minute|hour|day|week|month)s?\s*ago/i;
295
+ const match = text.match(timeRegex);
296
+ if (match) {
297
+ const amount = Number.parseInt(match[1], 10);
298
+ const unit = match[2].toLowerCase() as TimeUnit;
299
+ const start = now - amount * TIME_UNITS[unit];
300
+ return { start, end: now };
301
+ }
302
+
303
+ // Check for semantic time ranges
304
+ const semanticRanges: Record<string, number> = {
305
+ today: TIME_UNITS.day,
306
+ "this week": TIME_UNITS.week,
307
+ "this month": TIME_UNITS.month,
308
+ recent: TIME_UNITS.hour * 4,
309
+ latest: TIME_UNITS.hour,
310
+ "last hour": TIME_UNITS.hour,
311
+ "last day": TIME_UNITS.day,
312
+ "last week": TIME_UNITS.week,
313
+ "last month": TIME_UNITS.month,
314
+ };
315
+
316
+ for (const [range, duration] of Object.entries(semanticRanges)) {
317
+ if (text.toLowerCase().includes(range)) {
318
+ return { start: now - duration, end: now };
319
+ }
320
+ }
321
+
322
+ // Analyze context for appropriate default range
323
+ if (text.match(/trend|analysis|performance/i)) {
324
+ return { start: now - TIME_UNITS.week, end: now }; // 1 week for analysis
325
+ }
326
+ if (text.match(/trade|trades|trading|recent/i)) {
327
+ return { start: now - TIME_UNITS.day, end: now }; // 1 day for trading
328
+ }
329
+ if (text.match(/history|historical|long term/i)) {
330
+ return { start: now - TIME_UNITS.month, end: now }; // 1 month for history
331
+ }
332
+
333
+ // Default to last 24 hours
334
+ return { start: now - TIME_UNITS.day, end: now };
335
+ };
336
+
337
+ export const extractLimit = (text: string): number => {
338
+ // Check for explicit limit mentions
339
+ const limitMatch = text.match(/\b(show|display|get|fetch|limit)\s+(\d+)\b/i);
340
+ if (limitMatch) {
341
+ const limit = Number.parseInt(limitMatch[2], 10);
342
+ return Math.min(Math.max(limit, 1), 100); // Clamp between 1 and 100
343
+ }
344
+
345
+ // Check for semantic limit hints
346
+ if (text.match(/\b(all|everything|full|complete)\b/i)) return 100;
347
+ if (text.match(/\b(brief|quick|summary|overview)\b/i)) return 5;
348
+ if (text.match(/\b(detailed|comprehensive)\b/i)) return 50;
349
+
350
+ // Default based on context
351
+ if (text.match(/\b(trade|trades|trading)\b/i)) return 10;
352
+ if (text.match(/\b(analysis|analyze|trend)\b/i)) return 24;
353
+ if (text.match(/\b(history|historical)\b/i)) return 50;
354
+
355
+ return 10; // Default limit
356
+ };
357
+
358
+ // Formatting helpers
359
+ export const formatValue = (value?: number): string => {
360
+ if (!value) return "N/A";
361
+ if (value && value >= 1_000_000_000) {
362
+ return `$${(value / 1_000_000_000).toFixed(2)}B`;
363
+ }
364
+ if (value >= 1_000_000) {
365
+ return `$${(value / 1_000_000).toFixed(2)}M`;
366
+ }
367
+ if (value >= 1_000) {
368
+ return `$${(value / 1_000).toFixed(2)}K`;
369
+ }
370
+ return `$${value.toFixed(2)}`;
371
+ };
372
+
373
+ export const formatPercentChange = (change?: number): string => {
374
+ if (change === undefined) return "N/A";
375
+ const symbol = change >= 0 ? "↑" : "↓";
376
+ return `${symbol} ${Math.abs(change).toFixed(2)}%`;
377
+ };
378
+
379
+ export const shortenAddress = (address?: string): string => {
380
+ if (!address || address.length <= 12) return address || "Unknown";
381
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
382
+ };
383
+
384
+ export const formatTimestamp = (timestamp?: number): string => {
385
+ return timestamp ? new Date(timestamp * 1000).toLocaleString() : "N/A";
386
+ };
387
+
388
+ export const formatPrice = (price?: number): string => {
389
+ return price
390
+ ? price < 0.01
391
+ ? price.toExponential(2)
392
+ : price.toFixed(2)
393
+ : "N/A";
394
+ };
395
+
396
+ export const formatJsonScalar = (value: unknown): string => {
397
+ if (value === undefined || value === null || value === "") {
398
+ return "null";
399
+ }
400
+ if (typeof value === "number" || typeof value === "boolean") {
401
+ return String(value);
402
+ }
403
+ const normalized = String(value).replace(/\s+/g, " ").trim();
404
+ if (/^[A-Za-z0-9._/@:+-]+$/.test(normalized)) {
405
+ return normalized;
406
+ }
407
+ return `"${normalized.replace(/"/g, '\\"')}"`;
408
+ };
409
+
410
+ export const formatJsonTable = (
411
+ label: string,
412
+ rows: Array<Record<string, unknown>>,
413
+ fields: string[],
414
+ ): string => {
415
+ const indent = label.match(/^\s*/)?.[0] ?? "";
416
+ if (!rows.length) {
417
+ return `${label}[0]: []`;
418
+ }
419
+ const lines = [`${label}[${rows.length}]{${fields.join(",")}}:`];
420
+ for (const row of rows) {
421
+ lines.push(
422
+ `${indent} - ${fields.map((field) => formatJsonScalar(row[field])).join(",")}`,
423
+ );
424
+ }
425
+ return lines.join("\n");
426
+ };
427
+
428
+ // API helpers
429
+ export async function makeApiRequest<T>(
430
+ url: string,
431
+ options: {
432
+ apiKey: string;
433
+ chain?: BirdeyeSupportedChain;
434
+ method?: "GET" | "POST";
435
+ body?: unknown;
436
+ },
437
+ ): Promise<T> {
438
+ const { apiKey, chain = "solana", method = "GET", body } = options;
439
+
440
+ try {
441
+ const response = await fetch(url, {
442
+ method,
443
+ headers: {
444
+ "X-API-KEY": apiKey,
445
+ "x-chain": chain,
446
+ ...(body && { "Content-Type": "application/json" }),
447
+ },
448
+ ...(body && { body: JSON.stringify(body) }),
449
+ });
450
+
451
+ if (!response.ok) {
452
+ if (response.status === 404) {
453
+ throw new BirdeyeApiError(404, "Resource not found");
454
+ }
455
+ if (response.status === 429) {
456
+ throw new BirdeyeApiError(429, "Rate limit exceeded");
457
+ }
458
+ throw new BirdeyeApiError(
459
+ response.status,
460
+ `HTTP error! status: ${response.status}`,
461
+ );
462
+ }
463
+
464
+ const responseJson: T = await response.json();
465
+
466
+ return responseJson;
467
+ } catch (error) {
468
+ if (error instanceof BirdeyeApiError) {
469
+ logger.error(`API Error (${error.status}):`, error.message);
470
+ } else {
471
+ logger.error({ error }, "Error making API request:");
472
+ }
473
+ throw error;
474
+ }
475
+ }
476
+
477
+ // Formatting helpers
478
+ export const formatTokenInfo = (
479
+ token: TokenResult,
480
+ metadata?: TokenMetadataSingleResponse,
481
+ ): string => {
482
+ const priceFormatted =
483
+ token.price != null
484
+ ? token.price < 0.01
485
+ ? token.price.toExponential(2)
486
+ : token.price.toFixed(2)
487
+ : "N/A";
488
+
489
+ const volume =
490
+ token.volume_24h_usd != null
491
+ ? `$${(token.volume_24h_usd / 1_000_000).toFixed(2)}M`
492
+ : "N/A";
493
+
494
+ const liquidity =
495
+ token.liquidity != null
496
+ ? `$${(token.liquidity / 1_000_000).toFixed(2)}M`
497
+ : "N/A";
498
+
499
+ const fdv =
500
+ token.fdv != null ? `$${(token.fdv / 1_000_000).toFixed(2)}M` : "N/A";
501
+
502
+ const priceChange =
503
+ token.price_change_24h_percent != null
504
+ ? `${token.price_change_24h_percent > 0 ? "+" : ""}${token.price_change_24h_percent.toFixed(2)}%`
505
+ : "N/A";
506
+
507
+ const trades = token.trade_24h != null ? token.trade_24h.toString() : "N/A";
508
+
509
+ const age = token.creation_time
510
+ ? `${Math.floor((Date.now() - new Date(token.creation_time).getTime()) / (1000 * 60 * 60 * 24))}d`
511
+ : "N/A";
512
+
513
+ let output =
514
+ `šŸŖ™ ${token.name} @ ${token.symbol}\n` +
515
+ `šŸ’° USD: $${priceFormatted} (${priceChange})\n` +
516
+ `šŸ’Ž FDV: ${fdv}\n` +
517
+ `šŸ’¦ MCap: ${token.market_cap ? `$${(token.market_cap / 1_000_000).toFixed(2)}M` : "N/A"}\n` +
518
+ `šŸ’¦ Liq: ${liquidity}\n` +
519
+ `šŸ“Š Vol: ${volume}\n` +
520
+ `šŸ•°ļø Age: ${age}\n` +
521
+ `šŸ”„ Trades: ${trades}\n` +
522
+ `šŸ”— Address: ${token.address}`;
523
+
524
+ // Add metadata if available
525
+ if (metadata?.success) {
526
+ const { extensions } = metadata.data;
527
+ const links: string[] = [];
528
+
529
+ if (extensions) {
530
+ if (extensions.website) links.push(`🌐 [Website](${extensions.website})`);
531
+ if (extensions.twitter) links.push(`🐦 [Twitter](${extensions.twitter})`);
532
+ if (extensions.discord) links.push(`šŸ’¬ [Discord](${extensions.discord})`);
533
+ if (extensions.medium) links.push(`šŸ“ [Medium](${extensions.medium})`);
534
+ if (extensions.coingecko_id)
535
+ links.push(
536
+ `šŸ¦Ž [CoinGecko](https://www.coingecko.com/en/coins/${extensions.coingecko_id})`,
537
+ );
538
+ }
539
+
540
+ if (links.length > 0) {
541
+ output += `\n\nšŸ“± Social Links:\n${links.join("\n")}`;
542
+ }
543
+ }
544
+
545
+ return output;
546
+ };
547
+
548
+ // Extract symbols from text
549
+ export const extractSymbols = (
550
+ text: string,
551
+ // loose mode will try to extract more symbols but may include false positives
552
+ // strict mode will only extract symbols that are clearly formatted as a symbol using $SOL format
553
+ mode: "strict" | "loose" = "loose",
554
+ ): string[] => {
555
+ if (!text?.matchAll) return [];
556
+ const symbols = new Set<string>();
557
+
558
+ // Match patterns
559
+ const patterns =
560
+ mode === "strict"
561
+ ? [
562
+ // $SYMBOL format (case-insensitive due to 'i' flag)
563
+ /\$([A-Z0-9]{2,10})\b/gi,
564
+ ]
565
+ : [
566
+ // $SYMBOL format
567
+ /\$([A-Z0-9]{2,10})\b/gi,
568
+ // After articles (a/an)
569
+ /\b(?:a|an)\s+([A-Z0-9]{2,10})\b/gi,
570
+ // // Standalone caps
571
+ /\b[A-Z0-9]{2,10}\b/g,
572
+ // // Quoted symbols
573
+ /["']([A-Z0-9]{2,10})["']/gi,
574
+ // // Common price patterns
575
+ /\b([A-Z0-9]{2,10})\/USD\b/gi,
576
+ /\b([A-Z0-9]{2,10})-USD\b/gi,
577
+ ];
578
+
579
+ // Extract all matches
580
+ patterns.forEach((pattern) => {
581
+ const matches = Array.from(text.matchAll(pattern));
582
+ for (const match of matches) {
583
+ const symbol = (match[1] || match[0]).toUpperCase();
584
+ symbols.add(symbol);
585
+ }
586
+ });
587
+
588
+ return Array.from(symbols);
589
+ };
590
+
591
+ export const formatMetadataResponse = (
592
+ data: TokenMetadataSingleResponse,
593
+ chain: BirdeyeSupportedChain,
594
+ ): string => {
595
+ const tokenData = data.data;
596
+ const chainName = chain.charAt(0).toUpperCase() + chain.slice(1);
597
+ const chainExplorer = (() => {
598
+ switch (chain) {
599
+ case "solana":
600
+ return `https://solscan.io/token/${tokenData.address}`;
601
+ case "ethereum":
602
+ return `https://etherscan.io/token/${tokenData.address}`;
603
+ case "arbitrum":
604
+ return `https://arbiscan.io/token/${tokenData.address}`;
605
+ case "avalanche":
606
+ return `https://snowtrace.io/token/${tokenData.address}`;
607
+ case "bsc":
608
+ return `https://bscscan.com/token/${tokenData.address}`;
609
+ case "optimism":
610
+ return `https://optimistic.etherscan.io/token/${tokenData.address}`;
611
+ case "polygon":
612
+ return `https://polygonscan.com/token/${tokenData.address}`;
613
+ case "base":
614
+ return `https://basescan.org/token/${tokenData.address}`;
615
+ case "zksync":
616
+ return `https://explorer.zksync.io/address/${tokenData.address}`;
617
+ case "sui":
618
+ return `https://suiscan.xyz/mainnet/object/${tokenData.address}`;
619
+ default:
620
+ return null;
621
+ }
622
+ })();
623
+
624
+ let response = `Token Metadata for ${tokenData.name} (${tokenData.symbol}) on ${chainName}\n\n`;
625
+
626
+ // Basic Information
627
+ response += "šŸ“ Basic Information\n";
628
+ response += `• Name: ${tokenData.name}\n`;
629
+ response += `• Symbol: ${tokenData.symbol}\n`;
630
+ response += `• Address: ${tokenData.address}\n`;
631
+ response += `• Decimals: ${tokenData.decimals}\n`;
632
+ if (chainExplorer) {
633
+ response += `• Explorer: [View on ${chainName} Explorer](${chainExplorer})\n`;
634
+ }
635
+
636
+ // Social Links
637
+ response += "\nšŸ”— Social Links & Extensions\n";
638
+ response += `${formatSocialLinks(tokenData)}\n`;
639
+
640
+ // Logo
641
+ if (tokenData.logo_uri) {
642
+ response += "\nšŸ–¼ļø Logo\n";
643
+ response += tokenData.logo_uri;
644
+ }
645
+
646
+ return response;
647
+ };
648
+
649
+ const formatSocialLinks = (
650
+ data: TokenMetadataSingleResponse["data"],
651
+ ): string => {
652
+ const links: string[] = [];
653
+ const { extensions } = data;
654
+
655
+ if (!extensions) {
656
+ return "No social links available";
657
+ }
658
+
659
+ if (extensions.website) {
660
+ links.push(`🌐 [Website](${extensions.website})`);
661
+ }
662
+ if (extensions.twitter) {
663
+ links.push(`🐦 [Twitter](${extensions.twitter})`);
664
+ }
665
+ if (extensions.discord) {
666
+ links.push(`šŸ’¬ [Discord](${extensions.discord})`);
667
+ }
668
+ if (extensions.medium) {
669
+ links.push(`šŸ“ [Medium](${extensions.medium})`);
670
+ }
671
+ if (extensions.coingecko_id) {
672
+ links.push(
673
+ `šŸ¦Ž [CoinGecko](https://www.coingecko.com/en/coins/${extensions.coingecko_id})`,
674
+ );
675
+ }
676
+
677
+ return links.length > 0 ? links.join("\n") : "No social links available";
678
+ };
679
+
680
+ export const waitFor = (ms: number) =>
681
+ new Promise((resolve) => setTimeout(resolve, ms));
682
+
683
+ export const convertToStringParams = (
684
+ params: BirdeyeApiParams,
685
+ ): Record<string, string> => {
686
+ const result: Record<string, string> = {};
687
+ for (const [key, value] of Object.entries(params || {})) {
688
+ result[key] = value?.toString() || "";
689
+ }
690
+ return result;
691
+ };
692
+
693
+ export const getTokenResultFromSearchResponse = (
694
+ response: TokenMarketSearchResponse,
695
+ ): TokenResult[] | undefined => {
696
+ return response.data.items
697
+ .filter((item) => item.type === "token")
698
+ .flatMap((item) => item.result)
699
+ .filter((result): result is TokenResult => result !== undefined);
700
+ };