@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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/auto-enable.ts +76 -0
- package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
- package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
- package/dist/aerodrome-CfnESC32.mjs +890 -0
- package/dist/chunk-hT5z_Zn9.mjs +35 -0
- package/dist/index.d.mts +34727 -0
- package/dist/index.mjs +21590 -0
- package/dist/lib/server-wallet-trade.d.mts +34 -0
- package/dist/lib/server-wallet-trade.mjs +306 -0
- package/dist/meteora-BPX39hZo.mjs +22640 -0
- package/dist/orca-Bybp1HXO.mjs +249 -0
- package/dist/pancakeswp-CkEXlXti.mjs +604 -0
- package/dist/plugin-ZO_MTyd0.mjs +529 -0
- package/dist/raydium-rfaM9yEf.mjs +539 -0
- package/dist/sdk/index.d.mts +32492 -0
- package/dist/sdk/index.mjs +6415 -0
- package/dist/types-D5252NZk.mjs +487 -0
- package/dist/uniswap-CReXgXVN.mjs +573 -0
- package/dist/wallet-action.d.mts +6 -0
- package/dist/wallet-action.mjs +820 -0
- package/package.json +152 -0
- package/src/actions/failure-codes.ts +79 -0
- package/src/actions/index.ts +1 -0
- package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
- package/src/analytics/birdeye/birdeye-task.ts +175 -0
- package/src/analytics/birdeye/birdeye.ts +813 -0
- package/src/analytics/birdeye/constants.ts +74 -0
- package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
- package/src/analytics/birdeye/providers/market.ts +227 -0
- package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
- package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
- package/src/analytics/birdeye/providers/trending.ts +365 -0
- package/src/analytics/birdeye/providers/wallet.ts +14 -0
- package/src/analytics/birdeye/search-category.test.ts +207 -0
- package/src/analytics/birdeye/search-category.ts +506 -0
- package/src/analytics/birdeye/service.ts +992 -0
- package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
- package/src/analytics/birdeye/types/api/common.ts +305 -0
- package/src/analytics/birdeye/types/api/defi.ts +220 -0
- package/src/analytics/birdeye/types/api/pair.ts +200 -0
- package/src/analytics/birdeye/types/api/search.ts +86 -0
- package/src/analytics/birdeye/types/api/token.ts +635 -0
- package/src/analytics/birdeye/types/api/trader.ts +76 -0
- package/src/analytics/birdeye/types/api/wallet.ts +181 -0
- package/src/analytics/birdeye/types/shared.ts +106 -0
- package/src/analytics/birdeye/utils.ts +700 -0
- package/src/analytics/dexscreener/errors.ts +28 -0
- package/src/analytics/dexscreener/index.ts +3 -0
- package/src/analytics/dexscreener/search-category.test.ts +49 -0
- package/src/analytics/dexscreener/search-category.ts +42 -0
- package/src/analytics/dexscreener/service.ts +595 -0
- package/src/analytics/dexscreener/types.ts +128 -0
- package/src/analytics/lpinfo/index.d.ts +7 -0
- package/src/analytics/lpinfo/index.ts +52 -0
- package/src/analytics/lpinfo/kamino/README.md +102 -0
- package/src/analytics/lpinfo/kamino/index.ts +24 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
- package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
- package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
- package/src/analytics/lpinfo/steer/README.md +169 -0
- package/src/analytics/lpinfo/steer/index.ts +23 -0
- package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
- package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
- package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
- package/src/analytics/news/index.ts +52 -0
- package/src/analytics/news/interfaces/types.ts +222 -0
- package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
- package/src/analytics/news/services/newsDataService.ts +332 -0
- package/src/analytics/news/utils/formatters.ts +151 -0
- package/src/analytics/token-info/action.ts +240 -0
- package/src/analytics/token-info/index.ts +3 -0
- package/src/analytics/token-info/params.ts +215 -0
- package/src/analytics/token-info/providers.ts +681 -0
- package/src/analytics/token-info/service.ts +168 -0
- package/src/analytics/token-info/types.ts +74 -0
- package/src/audit/audit-log.ts +45 -0
- package/src/browser-shim/build-shim.ts +123 -0
- package/src/browser-shim/index.ts +5 -0
- package/src/browser-shim/shim.template.js +563 -0
- package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
- package/src/chains/evm/LICENSE +21 -0
- package/src/chains/evm/README.md +106 -0
- package/src/chains/evm/actions/helpers.ts +147 -0
- package/src/chains/evm/actions/swap.ts +839 -0
- package/src/chains/evm/actions/transfer.ts +254 -0
- package/src/chains/evm/biome.json +61 -0
- package/src/chains/evm/bridge-router.ts +660 -0
- package/src/chains/evm/build.ts +89 -0
- package/src/chains/evm/chain-handler.ts +416 -0
- package/src/chains/evm/constants.ts +23 -0
- package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
- package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
- package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
- package/src/chains/evm/dex/aerodrome/index.ts +34 -0
- package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
- package/src/chains/evm/dex/aerodrome/types.ts +318 -0
- package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
- package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
- package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
- package/src/chains/evm/dex/uniswap/index.ts +35 -0
- package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
- package/src/chains/evm/dex/uniswap/types.ts +390 -0
- package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
- package/src/chains/evm/generated/specs/specs.ts +151 -0
- package/src/chains/evm/gov-router.ts +250 -0
- package/src/chains/evm/index.browser.ts +16 -0
- package/src/chains/evm/index.ts +31 -0
- package/src/chains/evm/prompts.ts +193 -0
- package/src/chains/evm/providers/get-balance.ts +123 -0
- package/src/chains/evm/providers/wallet.ts +715 -0
- package/src/chains/evm/routes/sign.ts +333 -0
- package/src/chains/evm/rpc-providers.ts +410 -0
- package/src/chains/evm/service.ts +140 -0
- package/src/chains/evm/templates/index.ts +10 -0
- package/src/chains/evm/types/index.ts +432 -0
- package/src/chains/evm/vitest.config.ts +18 -0
- package/src/chains/registry.ts +668 -0
- package/src/chains/solana/README.md +367 -0
- package/src/chains/wallet-action.ts +533 -0
- package/src/chains/wallet-router.test.ts +296 -0
- package/src/contracts.ts +65 -0
- package/src/core-augmentation.ts +10 -0
- package/src/index.ts +71 -0
- package/src/lib/server-wallet-trade.ts +192 -0
- package/src/lib/wallet-export-guard.ts +330 -0
- package/src/lp/actions/liquidity.ts +827 -0
- package/src/lp/e2e/real-token-tests.ts +428 -0
- package/src/lp/e2e/scenarios.ts +470 -0
- package/src/lp/e2e/test-utils.ts +145 -0
- package/src/lp/lp-manager-entry.ts +303 -0
- package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
- package/src/lp/services/DexInteractionService.ts +226 -0
- package/src/lp/services/LpManagementService.test.ts +148 -0
- package/src/lp/services/LpManagementService.ts +632 -0
- package/src/lp/services/UserLpProfileService.ts +163 -0
- package/src/lp/services/VaultService.ts +153 -0
- package/src/lp/services/YieldOptimizationService.ts +344 -0
- package/src/lp/services/__tests__/MockLpService.ts +146 -0
- package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
- package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
- package/src/lp/types.ts +582 -0
- package/src/lp/utils/solanaClient.ts +143 -0
- package/src/plugin.ts +125 -0
- package/src/policy/policy.ts +19 -0
- package/src/providers/canonical-provider.ts +27 -0
- package/src/providers/unified-wallet-provider.ts +79 -0
- package/src/register-routes.ts +11 -0
- package/src/routes/plugin.ts +47 -0
- package/src/routes/wallet-market-overview-route.ts +869 -0
- package/src/sdk/abi.ts +258 -0
- package/src/sdk/bridge/abis.ts +126 -0
- package/src/sdk/bridge/client.ts +518 -0
- package/src/sdk/bridge/index.ts +56 -0
- package/src/sdk/bridge/solana.ts +604 -0
- package/src/sdk/bridge/types.ts +202 -0
- package/src/sdk/convenience.ts +347 -0
- package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
- package/src/sdk/escrow/types.ts +64 -0
- package/src/sdk/escrow/verifiers.ts +73 -0
- package/src/sdk/identity/erc8004.ts +692 -0
- package/src/sdk/identity/reputation.ts +449 -0
- package/src/sdk/identity/uaid.ts +497 -0
- package/src/sdk/identity/validation.ts +372 -0
- package/src/sdk/index.ts +763 -0
- package/src/sdk/policy/SpendingPolicy.ts +260 -0
- package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
- package/src/sdk/router/PaymentRouter.ts +215 -0
- package/src/sdk/router/index.ts +8 -0
- package/src/sdk/swap/SwapModule.ts +310 -0
- package/src/sdk/swap/abi.ts +117 -0
- package/src/sdk/swap/index.ts +34 -0
- package/src/sdk/swap/types.ts +135 -0
- package/src/sdk/tokens/decimals.ts +140 -0
- package/src/sdk/tokens/registry.ts +911 -0
- package/src/sdk/tokens/solana.ts +419 -0
- package/src/sdk/tokens/transfers.ts +327 -0
- package/src/sdk/types.ts +158 -0
- package/src/sdk/wallet-core.ts +115 -0
- package/src/sdk/x402/budget.ts +168 -0
- package/src/sdk/x402/chains/abstract/index.ts +280 -0
- package/src/sdk/x402/client.ts +320 -0
- package/src/sdk/x402/index.ts +46 -0
- package/src/sdk/x402/middleware.ts +92 -0
- package/src/sdk/x402/multi-asset.ts +144 -0
- package/src/sdk/x402/types.ts +156 -0
- package/src/services/wallet-backend-service.ts +328 -0
- package/src/types/wallet-router.ts +227 -0
- package/src/utils/intent-trajectory.ts +106 -0
- package/src/wallet/backend.ts +62 -0
- package/src/wallet/errors.ts +49 -0
- package/src/wallet/index.ts +27 -0
- package/src/wallet/local-eoa-backend.ts +201 -0
- package/src/wallet/pending.ts +60 -0
- package/src/wallet/select-backend.ts +47 -0
- package/src/wallet/steward-backend.ts +161 -0
- package/src/wallet-action.ts +1 -0
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
// @ts-nocheck — legacy code from absorbed plugins (lp-manager, lpinfo, dexscreener, defi-news, birdeye); strict types pending cleanup
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type RouteSpec,
|
|
5
|
+
resolveCloudRoute,
|
|
6
|
+
toRuntimeSettings,
|
|
7
|
+
} from "@elizaos/cloud-routing";
|
|
8
|
+
import {
|
|
9
|
+
type IAgentRuntime,
|
|
10
|
+
Service,
|
|
11
|
+
type ServiceTypeName,
|
|
12
|
+
} from "@elizaos/core";
|
|
13
|
+
import Birdeye from "./birdeye-task";
|
|
14
|
+
import { BIRDEYE_ENDPOINTS, BIRDEYE_SERVICE_NAME } from "./constants";
|
|
15
|
+
import { searchBirdeyeTokens } from "./search-category";
|
|
16
|
+
import type {
|
|
17
|
+
BirdeyeSupportedChain,
|
|
18
|
+
GetCacheTimedOptions,
|
|
19
|
+
IToken,
|
|
20
|
+
} from "./types/shared";
|
|
21
|
+
import { convertToStringParams, extractChain } from "./utils";
|
|
22
|
+
|
|
23
|
+
/** Route spec for {@link resolveCloudRoute} — local X-API-KEY or Eliza Cloud `/apis/birdeye` proxy. */
|
|
24
|
+
export const BIRDEYE_ROUTE_SPEC: RouteSpec = {
|
|
25
|
+
service: "birdeye",
|
|
26
|
+
localKeySetting: "BIRDEYE_API_KEY",
|
|
27
|
+
upstreamBaseUrl: "https://public-api.birdeye.so",
|
|
28
|
+
localKeyAuth: { kind: "header", headerName: "X-API-KEY" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Cache defaults for backwards compatibility
|
|
32
|
+
const CACHE_DEFAULTS = {
|
|
33
|
+
// Token trade data cache (30 minutes)
|
|
34
|
+
TOKEN_TRADE_DATA_TTL: 30 * 60 * 1000,
|
|
35
|
+
// Token security data cache (30 minutes)
|
|
36
|
+
TOKEN_SECURITY_DATA_TTL: 30 * 60 * 1000,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 'solana' | 'base' | 'ethereum'
|
|
40
|
+
type Chain = string;
|
|
41
|
+
|
|
42
|
+
type CacheWrapper<T> = {
|
|
43
|
+
data: T;
|
|
44
|
+
setAt: number;
|
|
45
|
+
};
|
|
46
|
+
export class BirdeyeService extends Service {
|
|
47
|
+
static serviceType: string = BIRDEYE_SERVICE_NAME;
|
|
48
|
+
capabilityDescription = "BirdEye data access";
|
|
49
|
+
|
|
50
|
+
private readonly access: { baseUrl: string; headers: Record<string, string> };
|
|
51
|
+
|
|
52
|
+
constructor(runtime?: IAgentRuntime) {
|
|
53
|
+
super(runtime);
|
|
54
|
+
if (!this.runtime) {
|
|
55
|
+
throw new Error("BirdeyeService requires a runtime");
|
|
56
|
+
}
|
|
57
|
+
const route = resolveCloudRoute(
|
|
58
|
+
toRuntimeSettings(this.runtime),
|
|
59
|
+
BIRDEYE_ROUTE_SPEC,
|
|
60
|
+
);
|
|
61
|
+
if (route.source === "disabled") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"BirdeyeService requires BIRDEYE_API_KEY or Eliza Cloud (ELIZAOS_CLOUD_API_KEY + ELIZAOS_CLOUD_ENABLED).",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
this.access = {
|
|
67
|
+
baseUrl: route.baseUrl.replace(/\/+$/, ""),
|
|
68
|
+
headers: route.headers,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private birdeyeUrl(path: string): string {
|
|
73
|
+
const suffix = path.replace(/^\/+/, "");
|
|
74
|
+
return `${this.access.baseUrl}/${suffix}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private getBirdeyeFetchOptions(chain = "solana"): RequestInit {
|
|
78
|
+
return {
|
|
79
|
+
headers: {
|
|
80
|
+
accept: "application/json",
|
|
81
|
+
"x-chain": chain,
|
|
82
|
+
...this.access.headers,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async fetchBirdeyeJson(
|
|
88
|
+
path: string,
|
|
89
|
+
params: Record<string, unknown> = {},
|
|
90
|
+
options: { headers?: Record<string, string> } = {},
|
|
91
|
+
) {
|
|
92
|
+
const chain = options.headers?.["x-chain"] ?? "solana";
|
|
93
|
+
const cleanParams = Object.fromEntries(
|
|
94
|
+
Object.entries(params).filter(([, value]) => value !== undefined),
|
|
95
|
+
);
|
|
96
|
+
const query = new URLSearchParams(
|
|
97
|
+
convertToStringParams(cleanParams),
|
|
98
|
+
).toString();
|
|
99
|
+
const suffix = query ? `${path}?${query}` : path;
|
|
100
|
+
const fetchOptions = this.getBirdeyeFetchOptions(chain);
|
|
101
|
+
const response = await fetch(this.birdeyeUrl(suffix), {
|
|
102
|
+
...fetchOptions,
|
|
103
|
+
headers: {
|
|
104
|
+
...(fetchOptions.headers as Record<string, string>),
|
|
105
|
+
...(options.headers ?? {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const errorText = await response.text();
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Birdeye API error ${response.status}: ${errorText || response.statusText}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return response.json();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async fetchSearchTokenMarketData(params: Record<string, unknown>) {
|
|
118
|
+
const chain = typeof params.chain === "string" ? params.chain : "solana";
|
|
119
|
+
return this.fetchBirdeyeJson(
|
|
120
|
+
BIRDEYE_ENDPOINTS.search.token_market,
|
|
121
|
+
params,
|
|
122
|
+
{
|
|
123
|
+
headers: { "x-chain": chain },
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async fetchTokenOverview(
|
|
129
|
+
params: Record<string, unknown>,
|
|
130
|
+
options: { headers?: Record<string, string> } = {},
|
|
131
|
+
) {
|
|
132
|
+
return this.fetchBirdeyeJson(
|
|
133
|
+
BIRDEYE_ENDPOINTS.token.overview,
|
|
134
|
+
params,
|
|
135
|
+
options,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async fetchTokenMarketData(
|
|
140
|
+
params: Record<string, unknown>,
|
|
141
|
+
options: { headers?: Record<string, string> } = {},
|
|
142
|
+
) {
|
|
143
|
+
return this.fetchBirdeyeJson(
|
|
144
|
+
BIRDEYE_ENDPOINTS.token.market_data,
|
|
145
|
+
params,
|
|
146
|
+
options,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async fetchTokenSecurityByAddress(
|
|
151
|
+
params: Record<string, unknown>,
|
|
152
|
+
options: { headers?: Record<string, string> } = {},
|
|
153
|
+
) {
|
|
154
|
+
return this.fetchBirdeyeJson(
|
|
155
|
+
BIRDEYE_ENDPOINTS.token.security,
|
|
156
|
+
params,
|
|
157
|
+
options,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async fetchTokenTradeDataSingle(
|
|
162
|
+
params: Record<string, unknown>,
|
|
163
|
+
options: { headers?: Record<string, string> } = {},
|
|
164
|
+
) {
|
|
165
|
+
return this.fetchBirdeyeJson(
|
|
166
|
+
BIRDEYE_ENDPOINTS.token.trade_data_single,
|
|
167
|
+
params,
|
|
168
|
+
options,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async search(query: string, options: Record<string, unknown> = {}) {
|
|
173
|
+
return searchBirdeyeTokens(
|
|
174
|
+
this.runtime,
|
|
175
|
+
{
|
|
176
|
+
query,
|
|
177
|
+
mode: options.mode as "auto" | "symbol" | "address" | undefined,
|
|
178
|
+
filters: options,
|
|
179
|
+
limit: typeof options.limit === "number" ? options.limit : undefined,
|
|
180
|
+
},
|
|
181
|
+
this,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// definitely should take a list of chains
|
|
186
|
+
async getTrending() {
|
|
187
|
+
//console.log('birdeye needs to get trending data');
|
|
188
|
+
return this.runtime.getCache<IToken[]>(`tokens_solana`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async getTrendingTokensForChain(
|
|
192
|
+
chain: Chain,
|
|
193
|
+
options?: { notOlderThan?: number; total?: number },
|
|
194
|
+
): Promise<CacheWrapper<IToken[]>> {
|
|
195
|
+
// Validate chain using extractChain
|
|
196
|
+
let validatedChain: BirdeyeSupportedChain;
|
|
197
|
+
try {
|
|
198
|
+
validatedChain = extractChain(undefined, chain);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.runtime.logger?.warn(
|
|
201
|
+
`getTrendingTokensForChain: ${error instanceof Error ? error.message : String(error)}`,
|
|
202
|
+
);
|
|
203
|
+
return { data: [], setAt: Date.now() };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Don't allow 'evm' as a trending chain (it's just a placeholder)
|
|
207
|
+
if (validatedChain === "evm") {
|
|
208
|
+
this.runtime.logger?.warn(
|
|
209
|
+
`getTrendingTokensForChain: 'evm' is not a specific chain. Use ethereum, arbitrum, polygon, etc.`,
|
|
210
|
+
);
|
|
211
|
+
return { data: [], setAt: Date.now() };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const cacheKey = `tokens_v2_${validatedChain}`;
|
|
215
|
+
const wrapper =
|
|
216
|
+
await this.runtime.getCache<CacheWrapper<IToken[]>>(cacheKey);
|
|
217
|
+
|
|
218
|
+
const freshEnough =
|
|
219
|
+
!!wrapper &&
|
|
220
|
+
(!options?.notOlderThan ||
|
|
221
|
+
Date.now() - wrapper.setAt <= options.notOlderThan);
|
|
222
|
+
|
|
223
|
+
if (freshEnough) {
|
|
224
|
+
this.runtime.logger.debug(
|
|
225
|
+
`birdyeSrv::getTrendingTokensForChain(${chain}) HIT`,
|
|
226
|
+
);
|
|
227
|
+
return wrapper;
|
|
228
|
+
}
|
|
229
|
+
this.runtime.logger.debug(
|
|
230
|
+
`birdyeSrv::getTrendingTokensForChain(${chain}) MISS`,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const BATCH_SIZE = 20; // max birdeye allows
|
|
234
|
+
const TOTAL = options?.total ?? 100;
|
|
235
|
+
const OFFSETS = Array.from(
|
|
236
|
+
{ length: TOTAL / BATCH_SIZE },
|
|
237
|
+
(_, i) => i * BATCH_SIZE,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const birdeyeFetchOptions: RequestInit = this.getBirdeyeFetchOptions(chain);
|
|
241
|
+
// Build all offset requests inline (no inner function)
|
|
242
|
+
const settled = await Promise.allSettled(
|
|
243
|
+
OFFSETS.map(async (offset) => {
|
|
244
|
+
const res = await fetch(
|
|
245
|
+
`${this.birdeyeUrl("defi/token_trending")}?sort_by=rank&sort_type=asc&offset=${offset}&limit=${BATCH_SIZE}`,
|
|
246
|
+
birdeyeFetchOptions,
|
|
247
|
+
);
|
|
248
|
+
const resp = await res.json();
|
|
249
|
+
const data = resp?.data;
|
|
250
|
+
|
|
251
|
+
if (!data?.tokens?.length) return [] as IToken[];
|
|
252
|
+
|
|
253
|
+
const last_updated = new Date((data.updateUnixTime ?? 0) * 1000);
|
|
254
|
+
|
|
255
|
+
interface RawBirdeyeToken {
|
|
256
|
+
address: string;
|
|
257
|
+
decimals?: number;
|
|
258
|
+
liquidity?: number;
|
|
259
|
+
logoURI?: string;
|
|
260
|
+
name?: string;
|
|
261
|
+
symbol: string;
|
|
262
|
+
volume24hUSD?: number;
|
|
263
|
+
rank?: number;
|
|
264
|
+
price?: number;
|
|
265
|
+
price24hChangePercent?: number;
|
|
266
|
+
}
|
|
267
|
+
return (data.tokens as RawBirdeyeToken[]).map((token) => ({
|
|
268
|
+
address: token.address,
|
|
269
|
+
chain,
|
|
270
|
+
provider: "birdeye",
|
|
271
|
+
decimals: token.decimals || 0,
|
|
272
|
+
liquidity: token.liquidity || 0,
|
|
273
|
+
logoURI: token.logoURI || "",
|
|
274
|
+
name: token.name || token.symbol,
|
|
275
|
+
symbol: token.symbol,
|
|
276
|
+
marketcap: 0,
|
|
277
|
+
volume24hUSD: token.volume24hUSD || 0,
|
|
278
|
+
rank: token.rank || 0,
|
|
279
|
+
price: token.price || 0,
|
|
280
|
+
price24hChangePercent: token.price24hChangePercent || 0,
|
|
281
|
+
last_updated,
|
|
282
|
+
})) as IToken[];
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const fetched = settled
|
|
287
|
+
.filter(
|
|
288
|
+
(r): r is PromiseFulfilledResult<IToken[]> => r.status === "fulfilled",
|
|
289
|
+
)
|
|
290
|
+
.flatMap((r) => r.value);
|
|
291
|
+
|
|
292
|
+
const byKey = new Map<string, IToken>();
|
|
293
|
+
for (const t of fetched) {
|
|
294
|
+
const key = t.address?.toLowerCase?.() || `${t.chain}:${t.rank}`;
|
|
295
|
+
if (!byKey.has(key)) {
|
|
296
|
+
byKey.set(key, t);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const merged = Array.from(byKey.values());
|
|
300
|
+
|
|
301
|
+
// Save to cache (ignore cache errors; don’t block the return path)
|
|
302
|
+
const output = { data: merged, setAt: Date.now() };
|
|
303
|
+
try {
|
|
304
|
+
await this.runtime.setCache<CacheWrapper<IToken[]>>(cacheKey, output);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
this.runtime.logger?.warn(
|
|
307
|
+
`setCache failed for ${chain}: ${e instanceof Error ? e.message : String(e)}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// options.depth 5
|
|
314
|
+
// options.notOlderThanMsecs
|
|
315
|
+
async getTrendingTokens(
|
|
316
|
+
chains: Chain[],
|
|
317
|
+
options?: { notOlderThan?: number },
|
|
318
|
+
): Promise<Record<Chain, IToken[]>> {
|
|
319
|
+
try {
|
|
320
|
+
const results = await Promise.all(
|
|
321
|
+
chains.map((chain) => this.getTrendingTokensForChain(chain, options)),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// key output per chain - unwrap CacheWrapper to get IToken[] arrays
|
|
325
|
+
const out: Record<string, IToken[]> = {};
|
|
326
|
+
for (const i in chains) {
|
|
327
|
+
const c = chains[i];
|
|
328
|
+
out[c] = results[i].data; // Extract data from CacheWrapper<IToken[]>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return out;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.runtime.logger?.error(
|
|
334
|
+
`Failed to sync trending tokens: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// FIXME: add chain param
|
|
341
|
+
async getTokenMarketData(tokenAddress: string): Promise<
|
|
342
|
+
| {
|
|
343
|
+
price: number;
|
|
344
|
+
marketCap: number;
|
|
345
|
+
liquidity: number;
|
|
346
|
+
volume24h: number;
|
|
347
|
+
priceHistory: number[];
|
|
348
|
+
}
|
|
349
|
+
| false
|
|
350
|
+
> {
|
|
351
|
+
try {
|
|
352
|
+
if (tokenAddress === "So11111111111111111111111111111111111111111") {
|
|
353
|
+
tokenAddress = "So11111111111111111111111111111111111111112"; // WSOL
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const [response, volResponse, priceHistoryResponse] = await Promise.all([
|
|
357
|
+
fetch(
|
|
358
|
+
`${this.birdeyeUrl("defi/v3/token/market-data")}?address=${tokenAddress}`,
|
|
359
|
+
this.getBirdeyeFetchOptions(),
|
|
360
|
+
),
|
|
361
|
+
fetch(
|
|
362
|
+
`${this.birdeyeUrl("defi/price_volume/single")}?address=${tokenAddress}&type=24h`,
|
|
363
|
+
this.getBirdeyeFetchOptions(),
|
|
364
|
+
),
|
|
365
|
+
fetch(
|
|
366
|
+
`${this.birdeyeUrl("defi/history_price")}?address=${tokenAddress}&address_type=token&type=15m`,
|
|
367
|
+
this.getBirdeyeFetchOptions(),
|
|
368
|
+
),
|
|
369
|
+
]);
|
|
370
|
+
|
|
371
|
+
if (!response.ok || !volResponse.ok || !priceHistoryResponse.ok) {
|
|
372
|
+
throw new Error(`Birdeye API error for token ${tokenAddress}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const [data, volData, priceHistoryData] = await Promise.all([
|
|
376
|
+
response.json(),
|
|
377
|
+
volResponse.json(),
|
|
378
|
+
priceHistoryResponse.json(),
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
if (!data.data) {
|
|
382
|
+
this.runtime.logger.warn(
|
|
383
|
+
`getTokenMarketData - cant save result for ${tokenAddress}: ${JSON.stringify(data)}`,
|
|
384
|
+
);
|
|
385
|
+
//logger.warn('getTokenMarketData - cant save result', data, 'for', tokenAddress);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
price: data.data.price,
|
|
391
|
+
marketCap: data.data.market_cap || 0,
|
|
392
|
+
liquidity: data.data.liquidity || 0,
|
|
393
|
+
volume24h: volData.data.volumeUSD || 0,
|
|
394
|
+
priceHistory: priceHistoryData.data.items.map(
|
|
395
|
+
(item: { value: number }) => item.value,
|
|
396
|
+
),
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.runtime.logger.error(
|
|
400
|
+
`Error fetching token market data: ${error instanceof Error ? error.message : String(error)}`,
|
|
401
|
+
);
|
|
402
|
+
//this.runtime.logger.error({ error },'Error fetching token market data:');
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// we can do singles
|
|
408
|
+
|
|
409
|
+
// Token - Market Data (Multiple) max 20 (BUSINESS $700/mo)
|
|
410
|
+
// https://public-api.birdeye.so/defi/v3/token/market-data/multiple
|
|
411
|
+
// liq,price,supply, circulating,fdv,mcap
|
|
412
|
+
|
|
413
|
+
// Token - Trade Data (Multiple) max 20 (BUSINESS $700/mo)
|
|
414
|
+
// https://public-api.birdeye.so/defi/v3/token/trade-data/multiple
|
|
415
|
+
// has a lot of data
|
|
416
|
+
|
|
417
|
+
/*
|
|
418
|
+
async getTokensTradeData(chain: string, tokenAddresses: string[]): Promise<any> {
|
|
419
|
+
const tokenDb: Record<string, any> = {};
|
|
420
|
+
const chunkArray = (arr: string[], size: number) =>
|
|
421
|
+
arr.map((_, i) => (i % size === 0 ? arr.slice(i, i + size) : null)).filter(Boolean);
|
|
422
|
+
const twenties = chunkArray(tokenAddresses, 20);
|
|
423
|
+
const multipricePs = twenties.map((addresses) => {
|
|
424
|
+
const listStr = addresses.join(',');
|
|
425
|
+
return fetch(
|
|
426
|
+
`${this.birdeyeUrl('defi/v3/token/trade-data/multiple')}`,
|
|
427
|
+
this.getBirdeyeFetchOptions('solana')
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
*/
|
|
432
|
+
|
|
433
|
+
// https://public-api.birdeye.so/defi/token_overview might be a better target
|
|
434
|
+
// what does this provide? 24h volume
|
|
435
|
+
async getTokenTradeData(
|
|
436
|
+
chain: string,
|
|
437
|
+
tokenAddress: string,
|
|
438
|
+
frames = "2h,8h,24h",
|
|
439
|
+
options: GetCacheTimedOptions = {},
|
|
440
|
+
): Promise<unknown> {
|
|
441
|
+
const key = `birdeye_tokenTradeData_${chain}_${tokenAddress}_${frames}`;
|
|
442
|
+
const tsInMs = options.tsInMs ?? CACHE_DEFAULTS.TOKEN_TRADE_DATA_TTL;
|
|
443
|
+
const notOlderThan =
|
|
444
|
+
options.notOlderThan ?? CACHE_DEFAULTS.TOKEN_TRADE_DATA_TTL;
|
|
445
|
+
|
|
446
|
+
// Check cache
|
|
447
|
+
const cached = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
448
|
+
if (cached) {
|
|
449
|
+
return cached;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Fetch fresh data
|
|
453
|
+
try {
|
|
454
|
+
const resp = await fetch(
|
|
455
|
+
`${this.birdeyeUrl("defi/v3/token/trade-data/single")}?address=${tokenAddress}&frames=${frames}`,
|
|
456
|
+
this.getBirdeyeFetchOptions(chain),
|
|
457
|
+
);
|
|
458
|
+
const data = await resp.json();
|
|
459
|
+
if (data) {
|
|
460
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
461
|
+
}
|
|
462
|
+
return data;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
this.runtime.logger?.error(
|
|
465
|
+
`birdeye:getTokenTradeData - ${e instanceof Error ? e.message : String(e)}`,
|
|
466
|
+
);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// [Defi] Price Volume - Multi max 50 (premium $200/mo)
|
|
472
|
+
// https://public-api.birdeye.so/defi/price_volume/multi
|
|
473
|
+
// getting 500s
|
|
474
|
+
/*
|
|
475
|
+
async getTokensPriceVolume(tokenAddresses: string[], type = '24h'): Promise<any> {
|
|
476
|
+
const tokenDb: Record<string, any> = {};
|
|
477
|
+
const chunkArray = (arr: string[], size: number) =>
|
|
478
|
+
arr.map((_, i) => (i % size === 0 ? arr.slice(i, i + size) : null)).filter(Boolean);
|
|
479
|
+
const fities = chunkArray(tokenAddresses, 50);
|
|
480
|
+
this.runtime.logger?.debug(`getTokensPriceVolume - batches: ${fities.length}`);
|
|
481
|
+
|
|
482
|
+
// not sure we want to do this with rate limits...
|
|
483
|
+
const multipricePs = fities.map((addresses) => {
|
|
484
|
+
const listStr = addresses.join(',');
|
|
485
|
+
this.runtime.logger?.debug(`getTokensPriceVolume - batch addresses: ${listStr}`);
|
|
486
|
+
return fetch(
|
|
487
|
+
`${this.birdeyeUrl('defi/price_volume/multi')}?list_address=${listStr}&type=${type}`,
|
|
488
|
+
{...this.getBirdeyeFetchOptions('solana'), method: 'POST' }
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
const multipriceResps = await Promise.all(multipricePs); // wait for the requests to finish
|
|
492
|
+
const multipriceData = await Promise.all(
|
|
493
|
+
multipriceResps.map(async (resp) => {
|
|
494
|
+
if (!resp.ok) {
|
|
495
|
+
const text = await resp.text();
|
|
496
|
+
this.runtime.logger?.error(`API error: ${resp.status} - ${text}`);
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
return resp.json();
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
for (const mpd of multipriceData) {
|
|
504
|
+
this.runtime.logger?.debug('getTokensPriceVolume - response:', mpd);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
*/
|
|
508
|
+
|
|
509
|
+
// [Defi] Price - Multiple max 100 (all)
|
|
510
|
+
// https://public-api.birdeye.so/defi/multi_price
|
|
511
|
+
// Batch CU Cost = N^0.8 × 5 (base cost of a single call) (n_max: 100)
|
|
512
|
+
// FIXME: caching
|
|
513
|
+
async getTokensMarketData(
|
|
514
|
+
chain: string,
|
|
515
|
+
tokenAddresses: string[],
|
|
516
|
+
): Promise<Record<string, IToken | undefined>> {
|
|
517
|
+
const tokenDb: Record<string, IToken | undefined> = {};
|
|
518
|
+
|
|
519
|
+
// Initialize all token addresses as undefined so we know they were checked
|
|
520
|
+
for (const ca of tokenAddresses) {
|
|
521
|
+
tokenDb[ca] = undefined;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
//console.log('beSrv:getTokensMarketData chain', chain, 'tokenAddresses', tokenAddresses)
|
|
525
|
+
|
|
526
|
+
// no real cache because pricing
|
|
527
|
+
// 1-60s cache might have some benefits tho
|
|
528
|
+
// usually called only once every sell signal (1m atm)
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const chunkArray = (arr: string[], size: number) =>
|
|
532
|
+
arr
|
|
533
|
+
.map((_, i) => (i % size === 0 ? arr.slice(i, i + size) : null))
|
|
534
|
+
.filter(Boolean);
|
|
535
|
+
|
|
536
|
+
const hundos = chunkArray(tokenAddresses, 100);
|
|
537
|
+
//console.log('getTokensMarketData hundos', hundos)
|
|
538
|
+
|
|
539
|
+
// Track batches with their addresses for cache management
|
|
540
|
+
const batchesWithAddresses = hundos
|
|
541
|
+
.map((addresses) => {
|
|
542
|
+
if (addresses !== null) {
|
|
543
|
+
const listStr = addresses.join(",");
|
|
544
|
+
return {
|
|
545
|
+
addresses,
|
|
546
|
+
promise: fetch(
|
|
547
|
+
`${this.birdeyeUrl("defi/multi_price")}?list_address=${listStr}&include_liquidity=true`,
|
|
548
|
+
this.getBirdeyeFetchOptions(chain),
|
|
549
|
+
),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
})
|
|
554
|
+
.filter(
|
|
555
|
+
(item): item is { addresses: string[]; promise: Promise<Response> } =>
|
|
556
|
+
item !== undefined,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const multipriceResps = await Promise.all(
|
|
560
|
+
batchesWithAddresses.map((b) => b.promise),
|
|
561
|
+
);
|
|
562
|
+
const multipriceData = await Promise.all(
|
|
563
|
+
multipriceResps.map((resp) => resp.json()),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
//const now = Date.now()
|
|
567
|
+
|
|
568
|
+
for (let i = 0; i < multipriceData.length; i++) {
|
|
569
|
+
const mpd = multipriceData[i];
|
|
570
|
+
const batchAddresses = batchesWithAddresses[i].addresses;
|
|
571
|
+
|
|
572
|
+
// Guard against undefined/null mpd or missing data
|
|
573
|
+
if (!mpd?.data || !mpd.success) {
|
|
574
|
+
this.runtime.logger?.warn(
|
|
575
|
+
`birdeye:getTokensMarketData - batch failed (${batchAddresses.length} addresses), caching all as failed`,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
// Cache all addresses in this failed batch
|
|
579
|
+
for (const ca of batchAddresses) {
|
|
580
|
+
await this.runtime.setCache(`birdeye_tokens_solana_${ca}`, {
|
|
581
|
+
data: undefined,
|
|
582
|
+
setAt: Date.now(),
|
|
583
|
+
ttl: 5 * 60 * 1000, // 5 minutes for failed batch lookups
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Process data from successful batch
|
|
590
|
+
for (const ca of batchAddresses) {
|
|
591
|
+
const t = mpd.data[ca];
|
|
592
|
+
|
|
593
|
+
if (t) {
|
|
594
|
+
/*
|
|
595
|
+
t {
|
|
596
|
+
isScaledUiToken: false,
|
|
597
|
+
value: 0.011726789622156722,
|
|
598
|
+
updateUnixTime: 1751591014,
|
|
599
|
+
updateHumanTime: "2025-07-04T01:03:34",
|
|
600
|
+
priceInNative: 0.00007683147650766234,
|
|
601
|
+
priceChange24h: -12.453478899440487,
|
|
602
|
+
liquidity: 1323844.6216610295,
|
|
603
|
+
}
|
|
604
|
+
*/
|
|
605
|
+
tokenDb[ca] = {
|
|
606
|
+
//provider: 'birdeye',
|
|
607
|
+
//chain: 'solana',
|
|
608
|
+
//address: ca,
|
|
609
|
+
priceUsd: t.value,
|
|
610
|
+
priceSol: t.priceInNative,
|
|
611
|
+
liquidity: t.liquidity,
|
|
612
|
+
priceChange24h: t.priceChange24h,
|
|
613
|
+
//marketcap
|
|
614
|
+
//volume24hUSD
|
|
615
|
+
};
|
|
616
|
+
const test: IToken = tokenDb[ca];
|
|
617
|
+
this.runtime.logger?.debug(
|
|
618
|
+
`birdeye:getTokensMarketData - caching token ${ca}`,
|
|
619
|
+
);
|
|
620
|
+
// Cache successful lookups with full TTL
|
|
621
|
+
await this.runtime.setCache<IToken>(
|
|
622
|
+
`birdeye_tokens_solana_${ca}`,
|
|
623
|
+
test,
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
// Token was in batch but has no valid data (or not in response)
|
|
627
|
+
this.runtime.logger?.warn(
|
|
628
|
+
`${ca} no valid data in response: ${JSON.stringify(t)}`,
|
|
629
|
+
);
|
|
630
|
+
// Cache failed lookups with shorter TTL to avoid re-fetching bad addresses
|
|
631
|
+
await this.runtime.setCache(`birdeye_tokens_solana_${ca}`, {
|
|
632
|
+
data: undefined,
|
|
633
|
+
setAt: Date.now(),
|
|
634
|
+
ttl: 5 * 60 * 1000, // 5 minutes for failed lookups
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return tokenDb;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
this.runtime.logger.error(
|
|
643
|
+
`Error fetching multiple tokens market data: ${error instanceof Error ? error.message : String(error)}`,
|
|
644
|
+
);
|
|
645
|
+
//this.runtime.logger.error({ error }, 'Error fetching multiple tokens market data:', error);
|
|
646
|
+
return tokenDb;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Token - Security (single) all
|
|
651
|
+
// https://public-api.birdeye.so/defi/token_security
|
|
652
|
+
async getTokenSecurityData(
|
|
653
|
+
chain: string,
|
|
654
|
+
tokenAddress: string,
|
|
655
|
+
options: GetCacheTimedOptions = {},
|
|
656
|
+
): Promise<unknown> {
|
|
657
|
+
const key = `birdeye_tokenSecurityData_${chain}_${tokenAddress}`;
|
|
658
|
+
const tsInMs = options.tsInMs ?? CACHE_DEFAULTS.TOKEN_SECURITY_DATA_TTL;
|
|
659
|
+
const notOlderThan =
|
|
660
|
+
options.notOlderThan ?? CACHE_DEFAULTS.TOKEN_SECURITY_DATA_TTL;
|
|
661
|
+
|
|
662
|
+
// Check cache
|
|
663
|
+
const cached = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
664
|
+
if (cached) {
|
|
665
|
+
return cached;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Fetch fresh data
|
|
669
|
+
try {
|
|
670
|
+
const resp = await fetch(
|
|
671
|
+
`${this.birdeyeUrl("defi/token_security")}?address=${tokenAddress}`,
|
|
672
|
+
this.getBirdeyeFetchOptions(chain),
|
|
673
|
+
);
|
|
674
|
+
const data = await resp.json();
|
|
675
|
+
if (data) {
|
|
676
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
677
|
+
}
|
|
678
|
+
return data;
|
|
679
|
+
} catch (e) {
|
|
680
|
+
this.runtime.logger?.error(
|
|
681
|
+
`birdeye:getTokenSecurityData - ${e instanceof Error ? e.message : String(e)}`,
|
|
682
|
+
);
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/*
|
|
688
|
+
async getToken(chain, ca) {
|
|
689
|
+
console.log('birdeye:srv getToken', chain, ca)
|
|
690
|
+
return getTokenMarketData(ca)
|
|
691
|
+
}
|
|
692
|
+
*/
|
|
693
|
+
|
|
694
|
+
// lookup token
|
|
695
|
+
async lookupToken(
|
|
696
|
+
chain: string,
|
|
697
|
+
ca: string,
|
|
698
|
+
options: GetCacheTimedOptions = {},
|
|
699
|
+
) {
|
|
700
|
+
try {
|
|
701
|
+
const key = `birdeye_token_${chain}_${ca}`;
|
|
702
|
+
const tsInMs = options.tsInMs ?? Date.now(); // only syscall if absolutely needed
|
|
703
|
+
const notOlderThan = options.notOlderThan ?? 30 * 1000; // a reasonable length (in ms)
|
|
704
|
+
|
|
705
|
+
// check cache
|
|
706
|
+
const cache = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
707
|
+
if (cache) {
|
|
708
|
+
this.runtime.logger.debug("birdeye:lookupToken - HIT");
|
|
709
|
+
return cache;
|
|
710
|
+
}
|
|
711
|
+
this.runtime.logger.debug("birdeye:lookupToken - MISS");
|
|
712
|
+
|
|
713
|
+
const res = await this.getTokensMarketData(chain, [ca]);
|
|
714
|
+
const data = res[ca];
|
|
715
|
+
if (data) {
|
|
716
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return data;
|
|
720
|
+
} catch (e) {
|
|
721
|
+
this.runtime.logger?.error(
|
|
722
|
+
`birdeyeSvr:lookupToken - err: ${e instanceof Error ? e.message : String(e)}`,
|
|
723
|
+
);
|
|
724
|
+
throw e;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async lookupTokens(
|
|
729
|
+
chainAndAddresses: Array<{ chain: string; address: string }>,
|
|
730
|
+
options: GetCacheTimedOptions = {},
|
|
731
|
+
) {
|
|
732
|
+
try {
|
|
733
|
+
// Lookup all tokens in parallel
|
|
734
|
+
const results = await Promise.all(
|
|
735
|
+
chainAndAddresses.map((cAA) =>
|
|
736
|
+
this.lookupToken(cAA.chain, cAA.address, options),
|
|
737
|
+
),
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
// Transform results into keyed object: key = `${chain}_${address}`
|
|
741
|
+
const keyedResults: Record<string, unknown> = {};
|
|
742
|
+
chainAndAddresses.forEach((cAA, index) => {
|
|
743
|
+
const key = `${cAA.chain}_${cAA.address}`;
|
|
744
|
+
keyedResults[key] = results[index];
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
return keyedResults;
|
|
748
|
+
} catch (e) {
|
|
749
|
+
this.runtime.logger?.error(
|
|
750
|
+
`birdeyeSvr:lookupTokens - err: ${e instanceof Error ? e.message : String(e)}`,
|
|
751
|
+
);
|
|
752
|
+
throw e;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async fetchSearchTokenMarketDataChain(
|
|
757
|
+
chain: string,
|
|
758
|
+
_params: unknown,
|
|
759
|
+
_options: GetCacheTimedOptions = {},
|
|
760
|
+
) {
|
|
761
|
+
const birdeyeFetchOptions: RequestInit = this.getBirdeyeFetchOptions(chain);
|
|
762
|
+
const res = await fetch(
|
|
763
|
+
`${this.birdeyeUrl("defi/v3/search")}`,
|
|
764
|
+
birdeyeFetchOptions,
|
|
765
|
+
);
|
|
766
|
+
const resp = await res.json();
|
|
767
|
+
this.runtime.logger.log("resp", resp);
|
|
768
|
+
return resp;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async lookupSymbolAllChains(
|
|
772
|
+
symbol: string,
|
|
773
|
+
options: GetCacheTimedOptions = {},
|
|
774
|
+
) {
|
|
775
|
+
// set up cache
|
|
776
|
+
const key = `birdeye_symbol_${symbol}`;
|
|
777
|
+
const tsInMs = options.tsInMs ?? Date.now();
|
|
778
|
+
const notOlderThan = options.notOlderThan ?? 30 * 1000;
|
|
779
|
+
|
|
780
|
+
// check cache
|
|
781
|
+
const cache = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
782
|
+
if (cache) {
|
|
783
|
+
return cache;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const birdeyeFetchOptions: RequestInit =
|
|
787
|
+
this.getBirdeyeFetchOptions("solana");
|
|
788
|
+
const res = await fetch(
|
|
789
|
+
`${this.birdeyeUrl("defi/v3/search")}?chain=all&target=token&keyword=${encodeURIComponent(symbol)}`,
|
|
790
|
+
birdeyeFetchOptions,
|
|
791
|
+
);
|
|
792
|
+
const resp = await res.json();
|
|
793
|
+
const data = resp.data.items;
|
|
794
|
+
if (data) {
|
|
795
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
796
|
+
}
|
|
797
|
+
return data;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async fetchWalletTokenList(
|
|
801
|
+
chain: BirdeyeSupportedChain,
|
|
802
|
+
publicKey: string,
|
|
803
|
+
options: GetCacheTimedOptions = {},
|
|
804
|
+
) {
|
|
805
|
+
// Get entire portfolio
|
|
806
|
+
// set up cache
|
|
807
|
+
const key = `birdeye_walletTokenList_${chain}_${publicKey}`;
|
|
808
|
+
const tsInMs = options.tsInMs ?? Date.now();
|
|
809
|
+
const notOlderThan = options.notOlderThan ?? 30 * 1000;
|
|
810
|
+
|
|
811
|
+
// check cache
|
|
812
|
+
const cache = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
813
|
+
if (cache) {
|
|
814
|
+
return cache;
|
|
815
|
+
}
|
|
816
|
+
// get data
|
|
817
|
+
const birdeyeFetchOptions: RequestInit = this.getBirdeyeFetchOptions(chain);
|
|
818
|
+
const res = await fetch(
|
|
819
|
+
`${this.birdeyeUrl("v1/wallet/token_list")}?wallet=${publicKey}`,
|
|
820
|
+
birdeyeFetchOptions,
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
const resp = await res.json();
|
|
824
|
+
const data = resp?.data;
|
|
825
|
+
if (data) {
|
|
826
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
827
|
+
}
|
|
828
|
+
return data;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async fetchWalletTxList(
|
|
832
|
+
chain: BirdeyeSupportedChain,
|
|
833
|
+
publicKey: string,
|
|
834
|
+
options: GetCacheTimedOptions = {},
|
|
835
|
+
) {
|
|
836
|
+
// set up cache
|
|
837
|
+
const key = `birdeye_walletTxList_${chain}_${publicKey}`;
|
|
838
|
+
const tsInMs = options.tsInMs ?? Date.now();
|
|
839
|
+
const notOlderThan = options.notOlderThan ?? 30 * 1000;
|
|
840
|
+
|
|
841
|
+
// check cache
|
|
842
|
+
const cache = await this.getCacheTimed<unknown>(key, { notOlderThan });
|
|
843
|
+
if (cache) {
|
|
844
|
+
return cache;
|
|
845
|
+
}
|
|
846
|
+
// get data
|
|
847
|
+
const birdeyeFetchOptions: RequestInit = this.getBirdeyeFetchOptions(chain);
|
|
848
|
+
const res = await fetch(
|
|
849
|
+
`${this.birdeyeUrl("v1/wallet/tx_list")}?wallet=${publicKey}&limit=100`,
|
|
850
|
+
birdeyeFetchOptions,
|
|
851
|
+
);
|
|
852
|
+
const resp = await res.json();
|
|
853
|
+
const data = resp?.data?.[chain] || [];
|
|
854
|
+
if (data) {
|
|
855
|
+
this.setCacheTimed(key, data, tsInMs);
|
|
856
|
+
}
|
|
857
|
+
return data;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
static async start(runtime: IAgentRuntime): Promise<BirdeyeService> {
|
|
861
|
+
runtime.logger.log("Initializing Birdeye Service");
|
|
862
|
+
const birdEyeService = new BirdeyeService(runtime);
|
|
863
|
+
|
|
864
|
+
// Clean any stale recurring birdeye tasks left over from previous runs.
|
|
865
|
+
runtime.initPromise
|
|
866
|
+
.then(async () => {
|
|
867
|
+
const tasks = await runtime.getTasks({
|
|
868
|
+
tags: ["queue", "repeat", "plugin_birdeye"],
|
|
869
|
+
agentIds: [runtime.agentId],
|
|
870
|
+
});
|
|
871
|
+
for (const task of tasks) {
|
|
872
|
+
if (task.id) {
|
|
873
|
+
await runtime.deleteTask(task.id);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Register the BIRDEYE_SYNC_WALLET task worker + recurring task
|
|
878
|
+
// when the agent owns a tracked wallet address.
|
|
879
|
+
const walletAddr = runtime.getSetting("BIRDEYE_WALLET_ADDR");
|
|
880
|
+
if (walletAddr) {
|
|
881
|
+
const birdeye = new Birdeye(runtime);
|
|
882
|
+
runtime.registerTaskWorker({
|
|
883
|
+
name: "BIRDEYE_SYNC_WALLET",
|
|
884
|
+
validate: async () => true,
|
|
885
|
+
execute: async (rt) => {
|
|
886
|
+
try {
|
|
887
|
+
await birdeye.syncWallet();
|
|
888
|
+
} catch (error) {
|
|
889
|
+
rt.logger.error(
|
|
890
|
+
`Failed to sync trending tokens: ${error instanceof Error ? error.message : String(error)}`,
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
return undefined;
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
await runtime.createTask({
|
|
898
|
+
name: "BIRDEYE_SYNC_WALLET",
|
|
899
|
+
description: "Sync wallet from Birdeye",
|
|
900
|
+
worldId: runtime.agentId,
|
|
901
|
+
metadata: {
|
|
902
|
+
createdAt: new Date().toISOString(),
|
|
903
|
+
updatedAt: Date.now(),
|
|
904
|
+
updateInterval: 1000 * 60 * 5, // 5 minutes
|
|
905
|
+
},
|
|
906
|
+
tags: ["queue", "repeat", "plugin_birdeye", "immediate"],
|
|
907
|
+
});
|
|
908
|
+
runtime.logger.log("birdeye init - tasks registered");
|
|
909
|
+
}
|
|
910
|
+
})
|
|
911
|
+
.catch((error) => {
|
|
912
|
+
runtime.logger.error(
|
|
913
|
+
`birdeye task setup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
914
|
+
);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Register Birdeye as a data provider with INTEL_DATAPROVIDER once
|
|
918
|
+
// Birdeye is loaded. INTEL_DATAPROVIDER is optional — probe it
|
|
919
|
+
// synchronously and skip registration if it's not present.
|
|
920
|
+
runtime
|
|
921
|
+
.getServiceLoadPromise(BIRDEYE_SERVICE_NAME as ServiceTypeName)
|
|
922
|
+
.then(() => {
|
|
923
|
+
const infoService = runtime.getService("INTEL_DATAPROVIDER") as
|
|
924
|
+
| { registerDataProvder?: (provider: unknown) => void }
|
|
925
|
+
| undefined;
|
|
926
|
+
|
|
927
|
+
if (
|
|
928
|
+
!infoService ||
|
|
929
|
+
typeof infoService.registerDataProvder !== "function"
|
|
930
|
+
) {
|
|
931
|
+
runtime.logger?.debug(
|
|
932
|
+
"INTEL_DATAPROVIDER service not available, skipping Birdeye data provider registration",
|
|
933
|
+
);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
infoService.registerDataProvder({
|
|
938
|
+
name: "Birdeye",
|
|
939
|
+
trendingService: BIRDEYE_SERVICE_NAME,
|
|
940
|
+
lookupService: BIRDEYE_SERVICE_NAME,
|
|
941
|
+
});
|
|
942
|
+
runtime.logger?.log(
|
|
943
|
+
"Birdeye data provider registered with INTEL_DATAPROVIDER",
|
|
944
|
+
);
|
|
945
|
+
})
|
|
946
|
+
.catch((e) => {
|
|
947
|
+
runtime.logger?.debug(
|
|
948
|
+
`Birdeye service load failed; skipping data provider registration: ${e instanceof Error ? e.message : String(e)}`,
|
|
949
|
+
);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
runtime.logger.log("Birdeye service initialized");
|
|
953
|
+
return birdEyeService;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
static async stop(runtime: IAgentRuntime) {
|
|
957
|
+
const service = runtime.getService(BIRDEYE_SERVICE_NAME);
|
|
958
|
+
if (!service) {
|
|
959
|
+
runtime.logger.error("Birdeye not found");
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
await service.stop();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async stop(): Promise<void> {
|
|
966
|
+
this.runtime.logger?.log("BirdEye service shutdown");
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async getCacheTimed<T>(key: string, options: GetCacheTimedOptions = {}) {
|
|
970
|
+
const wrapper = await this.runtime.getCache<CacheWrapper<T>>(key);
|
|
971
|
+
if (!wrapper) return false;
|
|
972
|
+
if (options.notOlderThan) {
|
|
973
|
+
const now = options.tsInMs ?? Date.now();
|
|
974
|
+
const diff = now - wrapper.setAt;
|
|
975
|
+
//console.log('checking notOlderThan', diff + 'ms', 'setAt', wrapper.setAt, 'asking', options.notOlderThan)
|
|
976
|
+
if (diff > options.notOlderThan) {
|
|
977
|
+
// no data
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
// return data
|
|
982
|
+
return wrapper.data;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async setCacheTimed<T>(key: string, val: T, tsInMs = 0) {
|
|
986
|
+
if (tsInMs === 0) tsInMs = Date.now();
|
|
987
|
+
return this.runtime.setCache<CacheWrapper<T>>(key, {
|
|
988
|
+
setAt: tsInMs,
|
|
989
|
+
data: val,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|