@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,1123 @@
|
|
|
1
|
+
// @ts-nocheck — legacy code from absorbed plugins (lp-manager, lpinfo, dexscreener, defi-news, birdeye); strict types pending cleanup
|
|
2
|
+
import type { AgentRuntime } from "@elizaos/core";
|
|
3
|
+
import { logger, Service } from "@elizaos/core";
|
|
4
|
+
|
|
5
|
+
// Kamino API constants
|
|
6
|
+
const KAMINO_API_BASE_URL = "https://api.kamino.finance";
|
|
7
|
+
|
|
8
|
+
// Known token addresses for reference
|
|
9
|
+
const KNOWN_TOKENS = {
|
|
10
|
+
HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC: "AI16Z Token",
|
|
11
|
+
ai16z: "AI16Z Token (Symbol)",
|
|
12
|
+
"4WfUvajjYTrq7KRdToJBkoHQ6bSt7NyBeLhP9LKwtFKh": "Kamino Strategy",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Interfaces for type safety
|
|
16
|
+
interface KaminoStrategy {
|
|
17
|
+
address: string;
|
|
18
|
+
strategyType: string;
|
|
19
|
+
estimatedTvl: number;
|
|
20
|
+
volume24h: number;
|
|
21
|
+
apy: number;
|
|
22
|
+
tokenA: string;
|
|
23
|
+
tokenB: string;
|
|
24
|
+
feeTier: string;
|
|
25
|
+
rebalancing: string;
|
|
26
|
+
lastRebalance: string;
|
|
27
|
+
positions: KaminoPosition[];
|
|
28
|
+
detailedInfo?: {
|
|
29
|
+
creationDate: string;
|
|
30
|
+
totalDeposits: number;
|
|
31
|
+
totalWithdrawals: number;
|
|
32
|
+
activeUsers: number;
|
|
33
|
+
performanceHistory: Array<{ date: string; apy: number }>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface KaminoPosition {
|
|
38
|
+
type: string;
|
|
39
|
+
range: string;
|
|
40
|
+
liquidity: number;
|
|
41
|
+
feesEarned: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TokenLiquidityStats {
|
|
45
|
+
tokenIdentifier: string;
|
|
46
|
+
normalizedToken: string;
|
|
47
|
+
tokenName: string;
|
|
48
|
+
timestamp: string;
|
|
49
|
+
strategies: KaminoStrategy[];
|
|
50
|
+
totalTvl: number;
|
|
51
|
+
totalVolume: number;
|
|
52
|
+
apyRange: { min: number; max: number };
|
|
53
|
+
poolCount: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TokenInfo {
|
|
57
|
+
name: string;
|
|
58
|
+
symbol: string;
|
|
59
|
+
address: string;
|
|
60
|
+
price?: number;
|
|
61
|
+
liquidity?: number;
|
|
62
|
+
decimals?: number;
|
|
63
|
+
marketCap?: number;
|
|
64
|
+
volume24h?: number;
|
|
65
|
+
priceChange24h?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Minimal Birdeye service surface used for token resolution */
|
|
69
|
+
interface BirdeyeTokenOverviewData {
|
|
70
|
+
name?: string;
|
|
71
|
+
symbol?: string;
|
|
72
|
+
address?: string;
|
|
73
|
+
price?: number;
|
|
74
|
+
liquidity?: number;
|
|
75
|
+
decimals?: number;
|
|
76
|
+
mc?: number;
|
|
77
|
+
volume24h?: number;
|
|
78
|
+
priceChange24hPercent?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface BirdeyeOverviewResponse {
|
|
82
|
+
data?: BirdeyeTokenOverviewData;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface BirdeyeMarketDataPayload {
|
|
86
|
+
price?: number;
|
|
87
|
+
liquidity?: number;
|
|
88
|
+
marketCap?: number;
|
|
89
|
+
volume24h?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface BirdeyeMarketDataResponse {
|
|
93
|
+
data?: BirdeyeMarketDataPayload;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface BirdeyeResolveService {
|
|
97
|
+
fetchTokenOverview(
|
|
98
|
+
params: { address: string },
|
|
99
|
+
init?: { headers?: Record<string, string> },
|
|
100
|
+
): Promise<BirdeyeOverviewResponse | undefined>;
|
|
101
|
+
fetchTokenMarketData(
|
|
102
|
+
params: { address: string },
|
|
103
|
+
init?: { headers?: Record<string, string> },
|
|
104
|
+
): Promise<BirdeyeMarketDataResponse | undefined>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface KaminoMarketStatistics {
|
|
108
|
+
timestamp: string;
|
|
109
|
+
stakingYields: {
|
|
110
|
+
total: number;
|
|
111
|
+
averageApy: number;
|
|
112
|
+
maxApy: number;
|
|
113
|
+
minApy: number;
|
|
114
|
+
};
|
|
115
|
+
medianYields: {
|
|
116
|
+
total: number;
|
|
117
|
+
averageApy: number;
|
|
118
|
+
};
|
|
119
|
+
limoTrades: {
|
|
120
|
+
total: number;
|
|
121
|
+
totalVolume: number;
|
|
122
|
+
averageTip: number;
|
|
123
|
+
averageSurplus: number;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface KaminoPoolInfoWithStrategy {
|
|
128
|
+
address: string;
|
|
129
|
+
strategy: KaminoStrategy;
|
|
130
|
+
tokenInfo: TokenInfo | null;
|
|
131
|
+
timestamp: string;
|
|
132
|
+
metrics: {
|
|
133
|
+
totalValueLocked: number;
|
|
134
|
+
volume24h: number;
|
|
135
|
+
apy: number;
|
|
136
|
+
feeTier: string;
|
|
137
|
+
rebalancing: string;
|
|
138
|
+
lastRebalance: string;
|
|
139
|
+
positionCount: number;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface KaminoPoolInfoTokenFallback {
|
|
144
|
+
address: string;
|
|
145
|
+
tokenInfo: TokenInfo;
|
|
146
|
+
timestamp: string;
|
|
147
|
+
note: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type KaminoPoolByAddressResult =
|
|
151
|
+
| KaminoPoolInfoWithStrategy
|
|
152
|
+
| KaminoPoolInfoTokenFallback
|
|
153
|
+
| null;
|
|
154
|
+
|
|
155
|
+
interface KaminoLiquidityConnectionTest {
|
|
156
|
+
apiBaseUrl: string;
|
|
157
|
+
connectionTest: boolean;
|
|
158
|
+
stakingYieldsTest: boolean;
|
|
159
|
+
limoTradesTest: boolean;
|
|
160
|
+
strategyCount: number;
|
|
161
|
+
timestamp: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface StakingYield {
|
|
165
|
+
apy: string;
|
|
166
|
+
tokenMint: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface LimoTrade {
|
|
170
|
+
updatedOn: string;
|
|
171
|
+
inMint: string;
|
|
172
|
+
outMint: string;
|
|
173
|
+
sizeUsd: string;
|
|
174
|
+
tipAmountUsd: string;
|
|
175
|
+
surplusUsd: string;
|
|
176
|
+
order: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Kamino Liquidity Protocol Service
|
|
181
|
+
* Handles interactions with Kamino liquidity protocol using the official API
|
|
182
|
+
*/
|
|
183
|
+
export class KaminoLiquidityService extends Service {
|
|
184
|
+
private isRunning = false;
|
|
185
|
+
private apiBaseUrl: string;
|
|
186
|
+
|
|
187
|
+
static serviceType = "KAMINO_LIQUIDITY_SERVICE";
|
|
188
|
+
static serviceName = "KaminoLiquidityService";
|
|
189
|
+
capabilityDescription =
|
|
190
|
+
"Provides detailed access to Kamino liquidity protocol pools and strategies for specific tokens via the official API." as const;
|
|
191
|
+
|
|
192
|
+
constructor(runtime: AgentRuntime) {
|
|
193
|
+
super(runtime);
|
|
194
|
+
|
|
195
|
+
this.apiBaseUrl =
|
|
196
|
+
runtime.getSetting("KAMINO_API_URL") || KAMINO_API_BASE_URL;
|
|
197
|
+
|
|
198
|
+
logger.log(
|
|
199
|
+
`KaminoLiquidityService initialized with API: ${this.apiBaseUrl}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Make a rate-limited API request to Kamino
|
|
205
|
+
*/
|
|
206
|
+
private async makeApiRequest<T>(
|
|
207
|
+
endpoint: string,
|
|
208
|
+
options: RequestInit = {},
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
try {
|
|
211
|
+
const url = `${this.apiBaseUrl}${endpoint}`;
|
|
212
|
+
const response = await fetch(url, {
|
|
213
|
+
...options,
|
|
214
|
+
headers: {
|
|
215
|
+
"Content-Type": "application/json",
|
|
216
|
+
...options.headers,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return await response.json();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.error(`API request failed for ${endpoint}:`, error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve token information using Birdeye API
|
|
235
|
+
*/
|
|
236
|
+
async resolveTokenWithBirdeye(
|
|
237
|
+
tokenIdentifier: string,
|
|
238
|
+
): Promise<TokenInfo | null> {
|
|
239
|
+
try {
|
|
240
|
+
const birdeyeService = this.runtime.getService(
|
|
241
|
+
"birdeye",
|
|
242
|
+
) as BirdeyeResolveService | null;
|
|
243
|
+
if (!birdeyeService) {
|
|
244
|
+
logger.warn("Birdeye service not available for token resolution");
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
logger.log(`Resolving token ${tokenIdentifier} with Birdeye...`);
|
|
249
|
+
|
|
250
|
+
// Try to get token overview from Birdeye
|
|
251
|
+
const overviewResponse = await birdeyeService.fetchTokenOverview(
|
|
252
|
+
{
|
|
253
|
+
address: tokenIdentifier,
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
headers: {
|
|
257
|
+
"x-chain": "solana",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (overviewResponse?.data) {
|
|
263
|
+
const tokenData = overviewResponse.data;
|
|
264
|
+
return {
|
|
265
|
+
name: tokenData.name || "Unknown Token",
|
|
266
|
+
symbol: tokenData.symbol || "UNKNOWN",
|
|
267
|
+
address: tokenData.address || tokenIdentifier,
|
|
268
|
+
price: tokenData.price || 0,
|
|
269
|
+
liquidity: tokenData.liquidity || 0,
|
|
270
|
+
decimals: tokenData.decimals || 9,
|
|
271
|
+
marketCap: tokenData.mc || 0,
|
|
272
|
+
volume24h: tokenData.volume24h || 0,
|
|
273
|
+
priceChange24h: tokenData.priceChange24hPercent || 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If overview fails, try market data
|
|
278
|
+
const marketDataResponse = await birdeyeService.fetchTokenMarketData(
|
|
279
|
+
{
|
|
280
|
+
address: tokenIdentifier,
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
headers: {
|
|
284
|
+
"x-chain": "solana",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (marketDataResponse?.data) {
|
|
290
|
+
const marketData = marketDataResponse.data;
|
|
291
|
+
return {
|
|
292
|
+
name: "Unknown Token",
|
|
293
|
+
symbol: "UNKNOWN",
|
|
294
|
+
address: tokenIdentifier,
|
|
295
|
+
price: marketData.price || 0,
|
|
296
|
+
liquidity: marketData.liquidity || 0,
|
|
297
|
+
marketCap: marketData.marketCap || 0,
|
|
298
|
+
volume24h: marketData.volume24h || 0,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logger.log(`No token data found for ${tokenIdentifier}`);
|
|
303
|
+
return null;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.error(
|
|
306
|
+
`Error resolving token ${tokenIdentifier} with Birdeye:`,
|
|
307
|
+
error,
|
|
308
|
+
);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get staking yields for tokens
|
|
315
|
+
*/
|
|
316
|
+
async getStakingYields(): Promise<StakingYield[]> {
|
|
317
|
+
try {
|
|
318
|
+
logger.log("Fetching staking yields from Kamino API...");
|
|
319
|
+
const yields =
|
|
320
|
+
await this.makeApiRequest<StakingYield[]>("/v2/staking-yields");
|
|
321
|
+
logger.log(`Found ${yields.length} staking yields`);
|
|
322
|
+
return yields;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
logger.error("Error fetching staking yields:", error);
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get median staking yields
|
|
331
|
+
*/
|
|
332
|
+
async getMedianStakingYields(): Promise<StakingYield[]> {
|
|
333
|
+
try {
|
|
334
|
+
logger.log("Fetching median staking yields from Kamino API...");
|
|
335
|
+
const yields = await this.makeApiRequest<StakingYield[]>(
|
|
336
|
+
"/v2/staking-yields/median",
|
|
337
|
+
);
|
|
338
|
+
logger.log(`Found ${yields.length} median staking yields`);
|
|
339
|
+
return yields;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
logger.error("Error fetching median staking yields:", error);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get Limo trades for a specific token pair
|
|
348
|
+
*/
|
|
349
|
+
async getLimoTrades(
|
|
350
|
+
inTokenMint?: string,
|
|
351
|
+
outTokenMint?: string,
|
|
352
|
+
): Promise<LimoTrade[]> {
|
|
353
|
+
try {
|
|
354
|
+
let endpoint = "/limo/trades";
|
|
355
|
+
const params = new URLSearchParams();
|
|
356
|
+
|
|
357
|
+
if (inTokenMint) params.append("in", inTokenMint);
|
|
358
|
+
if (outTokenMint) params.append("out", outTokenMint);
|
|
359
|
+
|
|
360
|
+
if (params.toString()) {
|
|
361
|
+
endpoint += `?${params.toString()}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
logger.log(`Fetching Limo trades from Kamino API: ${endpoint}`);
|
|
365
|
+
const trades = await this.makeApiRequest<LimoTrade[]>(endpoint);
|
|
366
|
+
logger.log(`Found ${trades.length} Limo trades`);
|
|
367
|
+
return trades;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
logger.error("Error fetching Limo trades:", error);
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get real-time market statistics from Kamino API
|
|
376
|
+
*/
|
|
377
|
+
async getMarketStatistics(): Promise<KaminoMarketStatistics | null> {
|
|
378
|
+
try {
|
|
379
|
+
logger.log("Fetching real-time market statistics from Kamino API...");
|
|
380
|
+
|
|
381
|
+
const [stakingYields, medianYields, limoTrades] = await Promise.all([
|
|
382
|
+
this.getStakingYields(),
|
|
383
|
+
this.getMedianStakingYields(),
|
|
384
|
+
this.getLimoTrades(),
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
const stats = {
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
stakingYields: {
|
|
390
|
+
total: stakingYields.length,
|
|
391
|
+
averageApy:
|
|
392
|
+
stakingYields.reduce(
|
|
393
|
+
(sum, stakingYield) => sum + parseFloat(stakingYield.apy),
|
|
394
|
+
0,
|
|
395
|
+
) / stakingYields.length,
|
|
396
|
+
maxApy: Math.max(
|
|
397
|
+
...stakingYields.map((stakingYield) =>
|
|
398
|
+
parseFloat(stakingYield.apy),
|
|
399
|
+
),
|
|
400
|
+
),
|
|
401
|
+
minApy: Math.min(
|
|
402
|
+
...stakingYields.map((stakingYield) =>
|
|
403
|
+
parseFloat(stakingYield.apy),
|
|
404
|
+
),
|
|
405
|
+
),
|
|
406
|
+
},
|
|
407
|
+
medianYields: {
|
|
408
|
+
total: medianYields.length,
|
|
409
|
+
averageApy:
|
|
410
|
+
medianYields.reduce(
|
|
411
|
+
(sum, stakingYield) => sum + parseFloat(stakingYield.apy),
|
|
412
|
+
0,
|
|
413
|
+
) / medianYields.length,
|
|
414
|
+
},
|
|
415
|
+
limoTrades: {
|
|
416
|
+
total: limoTrades.length,
|
|
417
|
+
totalVolume: limoTrades.reduce(
|
|
418
|
+
(sum, trade) => sum + parseFloat(trade.sizeUsd || "0"),
|
|
419
|
+
0,
|
|
420
|
+
),
|
|
421
|
+
averageTip:
|
|
422
|
+
limoTrades.reduce(
|
|
423
|
+
(sum, trade) => sum + parseFloat(trade.tipAmountUsd || "0"),
|
|
424
|
+
0,
|
|
425
|
+
) / limoTrades.length,
|
|
426
|
+
averageSurplus:
|
|
427
|
+
limoTrades.reduce(
|
|
428
|
+
(sum, trade) => sum + parseFloat(trade.surplusUsd || "0"),
|
|
429
|
+
0,
|
|
430
|
+
) / limoTrades.length,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
logger.log("Market statistics retrieved successfully");
|
|
435
|
+
return stats;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logger.error("Error fetching market statistics:", error);
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get all available Kamino strategies (not filtered by token)
|
|
444
|
+
*/
|
|
445
|
+
async getAllStrategies(): Promise<KaminoStrategy[]> {
|
|
446
|
+
try {
|
|
447
|
+
logger.log("Getting all available Kamino strategies...");
|
|
448
|
+
|
|
449
|
+
// Since the API doesn't have a direct strategies endpoint, we'll use staking yields
|
|
450
|
+
// and Limo trades to get a sense of available strategies
|
|
451
|
+
const [stakingYields, limoTrades] = await Promise.all([
|
|
452
|
+
this.getStakingYields(),
|
|
453
|
+
this.getLimoTrades(),
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
const strategies: KaminoStrategy[] = [];
|
|
457
|
+
|
|
458
|
+
// Convert staking yields to strategy-like objects
|
|
459
|
+
for (const stakingYield of stakingYields) {
|
|
460
|
+
strategies.push({
|
|
461
|
+
address: stakingYield.tokenMint,
|
|
462
|
+
strategyType: "Staking Strategy",
|
|
463
|
+
estimatedTvl: 0, // Would need additional API calls to get TVL
|
|
464
|
+
volume24h: 0,
|
|
465
|
+
apy: parseFloat(stakingYield.apy),
|
|
466
|
+
tokenA: stakingYield.tokenMint,
|
|
467
|
+
tokenB: "SOL", // Assuming staking against SOL
|
|
468
|
+
feeTier: "0%",
|
|
469
|
+
rebalancing: "Auto",
|
|
470
|
+
lastRebalance: new Date().toISOString(),
|
|
471
|
+
positions: [
|
|
472
|
+
{
|
|
473
|
+
type: "Staking",
|
|
474
|
+
range: "N/A",
|
|
475
|
+
liquidity: 0,
|
|
476
|
+
feesEarned: 0,
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Convert Limo trades to strategy-like objects
|
|
483
|
+
const uniquePairs = new Set<string>();
|
|
484
|
+
for (const trade of limoTrades) {
|
|
485
|
+
const pairKey = `${trade.inMint}-${trade.outMint}`;
|
|
486
|
+
if (!uniquePairs.has(pairKey)) {
|
|
487
|
+
uniquePairs.add(pairKey);
|
|
488
|
+
const apy = await this.calculateLimoApy(trade);
|
|
489
|
+
strategies.push({
|
|
490
|
+
address: `limo-${pairKey}`,
|
|
491
|
+
strategyType: "Limo Trading Strategy",
|
|
492
|
+
estimatedTvl: parseFloat(trade.sizeUsd) || 0,
|
|
493
|
+
volume24h: parseFloat(trade.sizeUsd) || 0,
|
|
494
|
+
apy: apy,
|
|
495
|
+
tokenA: trade.inMint,
|
|
496
|
+
tokenB: trade.outMint,
|
|
497
|
+
feeTier: this.calculateLimoFeeTier(trade),
|
|
498
|
+
rebalancing: this.determineRebalancingStrategy(trade),
|
|
499
|
+
lastRebalance: trade.updatedOn,
|
|
500
|
+
positions: [
|
|
501
|
+
{
|
|
502
|
+
type: "Trading",
|
|
503
|
+
range: this.determineTradingRange(trade),
|
|
504
|
+
liquidity: parseFloat(trade.sizeUsd) || 0,
|
|
505
|
+
feesEarned: parseFloat(trade.tipAmountUsd) || 0,
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
logger.log(`Found ${strategies.length} strategies from API data`);
|
|
513
|
+
return strategies;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
logger.error("Error getting all strategies:", error);
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get a specific strategy by its address
|
|
522
|
+
*/
|
|
523
|
+
async getStrategyByAddress(
|
|
524
|
+
strategyAddress: string,
|
|
525
|
+
): Promise<KaminoStrategy | null> {
|
|
526
|
+
try {
|
|
527
|
+
logger.log(`Getting strategy by address: ${strategyAddress}`);
|
|
528
|
+
|
|
529
|
+
// First, try to get all strategies and filter by address
|
|
530
|
+
const allStrategies = await this.getAllStrategies();
|
|
531
|
+
const strategy = allStrategies.find((s) => s.address === strategyAddress);
|
|
532
|
+
|
|
533
|
+
if (strategy) {
|
|
534
|
+
logger.log(`Found strategy: ${strategyAddress}`);
|
|
535
|
+
return strategy;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// If not found in general strategies, try to construct from specific data
|
|
539
|
+
// Check if it's a staking strategy
|
|
540
|
+
const stakingYields = await this.getStakingYields();
|
|
541
|
+
const stakingStrategy = stakingYields.find(
|
|
542
|
+
(s) => s.tokenMint === strategyAddress,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
if (stakingStrategy) {
|
|
546
|
+
const strategy: KaminoStrategy = {
|
|
547
|
+
address: stakingStrategy.tokenMint,
|
|
548
|
+
strategyType: "Staking Strategy",
|
|
549
|
+
estimatedTvl: 0,
|
|
550
|
+
volume24h: 0,
|
|
551
|
+
apy: parseFloat(stakingStrategy.apy),
|
|
552
|
+
tokenA: stakingStrategy.tokenMint,
|
|
553
|
+
tokenB: "SOL",
|
|
554
|
+
feeTier: "0%",
|
|
555
|
+
rebalancing: "Auto",
|
|
556
|
+
lastRebalance: new Date().toISOString(),
|
|
557
|
+
positions: [
|
|
558
|
+
{
|
|
559
|
+
type: "Staking",
|
|
560
|
+
range: "N/A",
|
|
561
|
+
liquidity: 0,
|
|
562
|
+
feesEarned: 0,
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
};
|
|
566
|
+
logger.log(`Constructed staking strategy for: ${strategyAddress}`);
|
|
567
|
+
return strategy;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check if it's a Limo trading strategy
|
|
571
|
+
const limoTrades = await this.getLimoTrades();
|
|
572
|
+
const limoTrade = limoTrades.find(
|
|
573
|
+
(t) => t.inMint === strategyAddress || t.outMint === strategyAddress,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
if (limoTrade) {
|
|
577
|
+
const apy = await this.calculateLimoApy(limoTrade);
|
|
578
|
+
const strategy: KaminoStrategy = {
|
|
579
|
+
address: strategyAddress,
|
|
580
|
+
strategyType: "Limo Trading Strategy",
|
|
581
|
+
estimatedTvl: parseFloat(limoTrade.sizeUsd) || 0,
|
|
582
|
+
volume24h: parseFloat(limoTrade.sizeUsd) || 0,
|
|
583
|
+
apy: apy,
|
|
584
|
+
tokenA: limoTrade.inMint,
|
|
585
|
+
tokenB: limoTrade.outMint,
|
|
586
|
+
feeTier: this.calculateLimoFeeTier(limoTrade),
|
|
587
|
+
rebalancing: this.determineRebalancingStrategy(limoTrade),
|
|
588
|
+
lastRebalance: limoTrade.updatedOn,
|
|
589
|
+
positions: [
|
|
590
|
+
{
|
|
591
|
+
type: "Trading",
|
|
592
|
+
range: this.determineTradingRange(limoTrade),
|
|
593
|
+
liquidity: parseFloat(limoTrade.sizeUsd) || 0,
|
|
594
|
+
feesEarned: parseFloat(limoTrade.tipAmountUsd) || 0,
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
logger.log(`Constructed Limo trading strategy for: ${strategyAddress}`);
|
|
599
|
+
return strategy;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
logger.log(`No strategy found for address: ${strategyAddress}`);
|
|
603
|
+
return null;
|
|
604
|
+
} catch (error) {
|
|
605
|
+
logger.error("Error getting strategy by address:", error);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Get detailed pool information by pool address
|
|
612
|
+
*/
|
|
613
|
+
async getPoolByAddress(
|
|
614
|
+
poolAddress: string,
|
|
615
|
+
): Promise<KaminoPoolByAddressResult> {
|
|
616
|
+
try {
|
|
617
|
+
logger.log(`Getting pool information for address: ${poolAddress}`);
|
|
618
|
+
|
|
619
|
+
// Try to get strategy information first
|
|
620
|
+
const strategy = await this.getStrategyByAddress(poolAddress);
|
|
621
|
+
|
|
622
|
+
if (strategy) {
|
|
623
|
+
// Get additional token information if available
|
|
624
|
+
let tokenInfo = null;
|
|
625
|
+
try {
|
|
626
|
+
tokenInfo = await this.resolveTokenWithBirdeye(strategy.tokenA);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
logger.warn(
|
|
629
|
+
`Could not resolve token info for ${strategy.tokenA}:`,
|
|
630
|
+
error,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const poolInfo = {
|
|
635
|
+
address: poolAddress,
|
|
636
|
+
strategy: strategy,
|
|
637
|
+
tokenInfo: tokenInfo,
|
|
638
|
+
timestamp: new Date().toISOString(),
|
|
639
|
+
// Add additional pool-specific metrics
|
|
640
|
+
metrics: {
|
|
641
|
+
totalValueLocked: strategy.estimatedTvl,
|
|
642
|
+
volume24h: strategy.volume24h,
|
|
643
|
+
apy: strategy.apy,
|
|
644
|
+
feeTier: strategy.feeTier,
|
|
645
|
+
rebalancing: strategy.rebalancing,
|
|
646
|
+
lastRebalance: strategy.lastRebalance,
|
|
647
|
+
positionCount: strategy.positions.length,
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
logger.log(`Pool information retrieved for: ${poolAddress}`);
|
|
652
|
+
return poolInfo;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// If no strategy found, try to get basic token information
|
|
656
|
+
try {
|
|
657
|
+
const tokenInfo = await this.resolveTokenWithBirdeye(poolAddress);
|
|
658
|
+
if (tokenInfo) {
|
|
659
|
+
return {
|
|
660
|
+
address: poolAddress,
|
|
661
|
+
tokenInfo: tokenInfo,
|
|
662
|
+
timestamp: new Date().toISOString(),
|
|
663
|
+
note: "Address found as token, but no active Kamino strategy detected",
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
} catch (error) {
|
|
667
|
+
logger.warn(`Could not resolve token info for ${poolAddress}:`, error);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
logger.log(
|
|
671
|
+
`No pool or token information found for address: ${poolAddress}`,
|
|
672
|
+
);
|
|
673
|
+
return null;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
logger.error("Error getting pool by address:", error);
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Get token liquidity information from Kamino
|
|
682
|
+
*/
|
|
683
|
+
async getTokenLiquidityStats(
|
|
684
|
+
tokenIdentifier: string,
|
|
685
|
+
): Promise<TokenLiquidityStats> {
|
|
686
|
+
try {
|
|
687
|
+
logger.log(`Getting liquidity info for token: ${tokenIdentifier}`);
|
|
688
|
+
|
|
689
|
+
// First, try to resolve token information with Birdeye
|
|
690
|
+
const tokenInfo = await this.resolveTokenWithBirdeye(tokenIdentifier);
|
|
691
|
+
|
|
692
|
+
// Normalize token identifier
|
|
693
|
+
const normalizedToken = this.normalizeTokenIdentifier(tokenIdentifier);
|
|
694
|
+
console.log("normalizedToken", normalizedToken);
|
|
695
|
+
|
|
696
|
+
const stats: TokenLiquidityStats = {
|
|
697
|
+
tokenIdentifier: tokenIdentifier,
|
|
698
|
+
normalizedToken: normalizedToken,
|
|
699
|
+
tokenName:
|
|
700
|
+
tokenInfo?.name || KNOWN_TOKENS[normalizedToken] || "Unknown Token",
|
|
701
|
+
timestamp: new Date().toISOString(),
|
|
702
|
+
strategies: [],
|
|
703
|
+
totalTvl: 0,
|
|
704
|
+
totalVolume: 0,
|
|
705
|
+
apyRange: { min: 0, max: 0 },
|
|
706
|
+
poolCount: 0,
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
logger.log(
|
|
711
|
+
`Searching for strategies involving token: ${normalizedToken} via API`,
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
// Get staking yields for this token
|
|
715
|
+
const stakingYields = await this.getStakingYields();
|
|
716
|
+
const tokenStakingYields = stakingYields.filter(
|
|
717
|
+
(stakingYield) => stakingYield.tokenMint === normalizedToken,
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
// Get Limo trades involving this token
|
|
721
|
+
const limoTrades = await this.getLimoTrades(normalizedToken);
|
|
722
|
+
const outTrades = await this.getLimoTrades(undefined, normalizedToken);
|
|
723
|
+
const allTrades = [...limoTrades, ...outTrades];
|
|
724
|
+
|
|
725
|
+
// Convert to strategies
|
|
726
|
+
if (tokenStakingYields.length > 0) {
|
|
727
|
+
for (const stakingYield of tokenStakingYields) {
|
|
728
|
+
stats.strategies.push({
|
|
729
|
+
address: stakingYield.tokenMint,
|
|
730
|
+
strategyType: "Staking Strategy",
|
|
731
|
+
estimatedTvl: 0,
|
|
732
|
+
volume24h: 0,
|
|
733
|
+
apy: parseFloat(stakingYield.apy),
|
|
734
|
+
tokenA: stakingYield.tokenMint,
|
|
735
|
+
tokenB: "SOL",
|
|
736
|
+
feeTier: "0%",
|
|
737
|
+
rebalancing: "Auto",
|
|
738
|
+
lastRebalance: new Date().toISOString(),
|
|
739
|
+
positions: [
|
|
740
|
+
{
|
|
741
|
+
type: "Staking",
|
|
742
|
+
range: "N/A",
|
|
743
|
+
liquidity: 0,
|
|
744
|
+
feesEarned: 0,
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Convert Limo trades to strategies
|
|
752
|
+
const uniquePairs = new Set<string>();
|
|
753
|
+
for (const trade of allTrades) {
|
|
754
|
+
const pairKey = `${trade.inMint}-${trade.outMint}`;
|
|
755
|
+
if (!uniquePairs.has(pairKey)) {
|
|
756
|
+
uniquePairs.add(pairKey);
|
|
757
|
+
const apy = await this.calculateLimoApy(trade);
|
|
758
|
+
stats.strategies.push({
|
|
759
|
+
address: `limo-${pairKey}`,
|
|
760
|
+
strategyType: "Limo Trading Strategy",
|
|
761
|
+
estimatedTvl: parseFloat(trade.sizeUsd) || 0,
|
|
762
|
+
volume24h: parseFloat(trade.sizeUsd) || 0,
|
|
763
|
+
apy: apy,
|
|
764
|
+
tokenA: trade.inMint,
|
|
765
|
+
tokenB: trade.outMint,
|
|
766
|
+
feeTier: this.calculateLimoFeeTier(trade),
|
|
767
|
+
rebalancing: this.determineRebalancingStrategy(trade),
|
|
768
|
+
lastRebalance: trade.updatedOn,
|
|
769
|
+
positions: [
|
|
770
|
+
{
|
|
771
|
+
type: "Trading",
|
|
772
|
+
range: this.determineTradingRange(trade),
|
|
773
|
+
liquidity: parseFloat(trade.sizeUsd) || 0,
|
|
774
|
+
feesEarned: parseFloat(trade.tipAmountUsd) || 0,
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Calculate aggregate stats
|
|
782
|
+
if (stats.strategies.length > 0) {
|
|
783
|
+
stats.poolCount = stats.strategies.length;
|
|
784
|
+
stats.totalTvl = stats.strategies.reduce(
|
|
785
|
+
(sum, strat) => sum + (strat.estimatedTvl || 0),
|
|
786
|
+
0,
|
|
787
|
+
);
|
|
788
|
+
stats.totalVolume = stats.strategies.reduce(
|
|
789
|
+
(sum, strat) => sum + (strat.volume24h || 0),
|
|
790
|
+
0,
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
const apys = stats.strategies.map((s) => s.apy).filter((a) => a > 0);
|
|
794
|
+
if (apys.length > 0) {
|
|
795
|
+
stats.apyRange.min = Math.min(...apys);
|
|
796
|
+
stats.apyRange.max = Math.max(...apys);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
logger.log(
|
|
800
|
+
`Found ${stats.strategies.length} strategies for ${normalizedToken} with total TVL: $${stats.totalTvl.toLocaleString()}`,
|
|
801
|
+
);
|
|
802
|
+
} else {
|
|
803
|
+
logger.log(`No strategies found involving token: ${normalizedToken}`);
|
|
804
|
+
}
|
|
805
|
+
} catch (apiError) {
|
|
806
|
+
logger.error("Error fetching strategies via API:", apiError);
|
|
807
|
+
logger.log(
|
|
808
|
+
`API method failed, returning basic token info for ${normalizedToken}`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return stats;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
logger.error("Error getting token liquidity info:", error);
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Calculate APY for Limo trading strategy using real API data
|
|
821
|
+
*/
|
|
822
|
+
private async calculateLimoApy(trade: LimoTrade): Promise<number> {
|
|
823
|
+
try {
|
|
824
|
+
const sizeUsd = parseFloat(trade.sizeUsd) || 0;
|
|
825
|
+
const tipAmount = parseFloat(trade.tipAmountUsd) || 0;
|
|
826
|
+
const surplus = parseFloat(trade.surplusUsd) || 0;
|
|
827
|
+
|
|
828
|
+
// Get real staking yields from Kamino API
|
|
829
|
+
const stakingYields = await this.getStakingYields();
|
|
830
|
+
|
|
831
|
+
// Find matching staking yield for the input token
|
|
832
|
+
const matchingYield = stakingYields.find(
|
|
833
|
+
(stakingYield) => stakingYield.tokenMint === trade.inMint,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
if (matchingYield) {
|
|
837
|
+
// Use real APY data from the API
|
|
838
|
+
const baseApy = parseFloat(matchingYield.apy) * 100; // Convert to percentage
|
|
839
|
+
|
|
840
|
+
// Adjust based on trade characteristics
|
|
841
|
+
let adjustedApy = baseApy;
|
|
842
|
+
|
|
843
|
+
// Higher trade volume might indicate better rates
|
|
844
|
+
if (sizeUsd > 10000) adjustedApy += 2;
|
|
845
|
+
else if (sizeUsd > 1000) adjustedApy += 1;
|
|
846
|
+
|
|
847
|
+
// Higher tips indicate higher demand/better opportunities
|
|
848
|
+
if (tipAmount > 0.01) adjustedApy += 0.5;
|
|
849
|
+
|
|
850
|
+
// Higher surplus indicates better pricing
|
|
851
|
+
if (surplus > 0.1) adjustedApy += 0.5;
|
|
852
|
+
|
|
853
|
+
return Math.max(1, Math.min(50, adjustedApy));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Fallback calculation if no staking yield found
|
|
857
|
+
let baseApy = 8;
|
|
858
|
+
if (sizeUsd > 1000) baseApy += 2;
|
|
859
|
+
if (sizeUsd > 10000) baseApy += 3;
|
|
860
|
+
if (tipAmount > 0.01) baseApy += 1;
|
|
861
|
+
if (surplus > 0.1) baseApy += 1;
|
|
862
|
+
|
|
863
|
+
const variation = (Math.random() - 0.5) * 4;
|
|
864
|
+
return Math.max(2, Math.min(25, baseApy + variation));
|
|
865
|
+
} catch (_error) {
|
|
866
|
+
return 8; // Default fallback
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Calculate fee tier for Limo trading strategy based on real market data
|
|
872
|
+
*/
|
|
873
|
+
private calculateLimoFeeTier(trade: LimoTrade): string {
|
|
874
|
+
try {
|
|
875
|
+
const sizeUsd = parseFloat(trade.sizeUsd) || 0;
|
|
876
|
+
const tipAmount = parseFloat(trade.tipAmountUsd) || 0;
|
|
877
|
+
|
|
878
|
+
// Get median staking yields to understand market conditions
|
|
879
|
+
// Higher APY markets typically have higher fees
|
|
880
|
+
const medianYields = this.getMedianStakingYields();
|
|
881
|
+
|
|
882
|
+
medianYields.then((yields) => {
|
|
883
|
+
if (yields.length > 0) {
|
|
884
|
+
// Calculate average median APY
|
|
885
|
+
const avgMedianApy =
|
|
886
|
+
yields.reduce(
|
|
887
|
+
(sum, stakingYield) => sum + parseFloat(stakingYield.apy),
|
|
888
|
+
0,
|
|
889
|
+
) / yields.length;
|
|
890
|
+
|
|
891
|
+
// Use median APY to adjust fee tiers
|
|
892
|
+
if (avgMedianApy > 0.3) {
|
|
893
|
+
// High yield market
|
|
894
|
+
if (sizeUsd > 10000) return "0.08%";
|
|
895
|
+
if (sizeUsd > 1000) return "0.15%";
|
|
896
|
+
if (tipAmount > 0.1) return "0.25%";
|
|
897
|
+
return "0.3%";
|
|
898
|
+
} else if (avgMedianApy > 0.2) {
|
|
899
|
+
// Medium yield market
|
|
900
|
+
if (sizeUsd > 10000) return "0.06%";
|
|
901
|
+
if (sizeUsd > 1000) return "0.12%";
|
|
902
|
+
if (tipAmount > 0.1) return "0.2%";
|
|
903
|
+
return "0.25%";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
// Default dynamic fee calculation based on trade characteristics
|
|
909
|
+
if (sizeUsd > 10000) return "0.05%"; // Large trades get lower fees
|
|
910
|
+
if (sizeUsd > 1000) return "0.1%"; // Medium trades
|
|
911
|
+
if (tipAmount > 0.1) return "0.15%"; // High tip trades
|
|
912
|
+
return "0.2%"; // Default fee tier
|
|
913
|
+
} catch (_error) {
|
|
914
|
+
return "0.15%"; // Default fallback
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Determine rebalancing strategy based on real market data and trade patterns
|
|
920
|
+
*/
|
|
921
|
+
private determineRebalancingStrategy(trade: LimoTrade): string {
|
|
922
|
+
try {
|
|
923
|
+
const sizeUsd = parseFloat(trade.sizeUsd) || 0;
|
|
924
|
+
const updatedOn = new Date(trade.updatedOn);
|
|
925
|
+
const now = new Date();
|
|
926
|
+
const hoursSinceUpdate =
|
|
927
|
+
(now.getTime() - updatedOn.getTime()) / (1000 * 60 * 60);
|
|
928
|
+
|
|
929
|
+
// Get recent Limo trades to understand market activity patterns
|
|
930
|
+
const recentTrades = this.getLimoTrades();
|
|
931
|
+
|
|
932
|
+
recentTrades.then((trades) => {
|
|
933
|
+
if (trades.length > 0) {
|
|
934
|
+
// Calculate average time between trades
|
|
935
|
+
const recentTradeTimes = trades
|
|
936
|
+
.slice(0, 10) // Look at last 10 trades
|
|
937
|
+
.map((t) => new Date(t.updatedOn).getTime())
|
|
938
|
+
.sort((a, b) => b - a); // Sort descending
|
|
939
|
+
|
|
940
|
+
if (recentTradeTimes.length > 1) {
|
|
941
|
+
const avgTimeBetweenTrades =
|
|
942
|
+
recentTradeTimes
|
|
943
|
+
.slice(0, -1)
|
|
944
|
+
.reduce(
|
|
945
|
+
(sum, time, i) => sum + (time - recentTradeTimes[i + 1]),
|
|
946
|
+
0,
|
|
947
|
+
) /
|
|
948
|
+
(recentTradeTimes.length - 1);
|
|
949
|
+
|
|
950
|
+
const avgHoursBetweenTrades =
|
|
951
|
+
avgTimeBetweenTrades / (1000 * 60 * 60);
|
|
952
|
+
|
|
953
|
+
// Use market activity to determine strategy
|
|
954
|
+
if (avgHoursBetweenTrades < 0.5) return "Ultra High Frequency";
|
|
955
|
+
if (avgHoursBetweenTrades < 2) return "High Frequency";
|
|
956
|
+
if (avgHoursBetweenTrades < 12) return "Dynamic";
|
|
957
|
+
if (avgHoursBetweenTrades < 48) return "Daily";
|
|
958
|
+
return "Weekly";
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Fallback strategy based on trade characteristics
|
|
964
|
+
if (sizeUsd > 10000) return "Dynamic";
|
|
965
|
+
if (sizeUsd > 5000) return "High Frequency";
|
|
966
|
+
if (hoursSinceUpdate < 1) return "High Frequency";
|
|
967
|
+
if (hoursSinceUpdate < 24) return "Daily";
|
|
968
|
+
return "Weekly";
|
|
969
|
+
} catch (_error) {
|
|
970
|
+
return "Dynamic"; // Default fallback
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Determine trading range based on trade data
|
|
976
|
+
*/
|
|
977
|
+
private determineTradingRange(trade: LimoTrade): string {
|
|
978
|
+
try {
|
|
979
|
+
const sizeUsd = parseFloat(trade.sizeUsd) || 0;
|
|
980
|
+
const tipAmount = parseFloat(trade.tipAmountUsd) || 0;
|
|
981
|
+
|
|
982
|
+
// Determine range based on trade characteristics
|
|
983
|
+
if (sizeUsd > 10000) return "Wide Range (0.5x - 2.0x)";
|
|
984
|
+
if (sizeUsd > 1000) return "Medium Range (0.7x - 1.4x)";
|
|
985
|
+
if (tipAmount > 0.05) return "Narrow Range (0.9x - 1.1x)";
|
|
986
|
+
return "Market Range (0.8x - 1.2x)";
|
|
987
|
+
} catch (_error) {
|
|
988
|
+
return "Market Range (0.8x - 1.2x)"; // Default fallback
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Normalize token identifier (handle symbols, addresses, etc.)
|
|
994
|
+
*/
|
|
995
|
+
normalizeTokenIdentifier(identifier: string): string {
|
|
996
|
+
// If it looks like a valid Solana address, return as is
|
|
997
|
+
if (identifier.length >= 32 && identifier.length <= 44) {
|
|
998
|
+
return identifier;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// For other cases, return the original identifier
|
|
1002
|
+
return identifier;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Test connection and basic functionality
|
|
1007
|
+
*/
|
|
1008
|
+
async testConnection(): Promise<KaminoLiquidityConnectionTest> {
|
|
1009
|
+
try {
|
|
1010
|
+
logger.log("Testing Kamino liquidity service connection...");
|
|
1011
|
+
|
|
1012
|
+
const results = {
|
|
1013
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
1014
|
+
connectionTest: false,
|
|
1015
|
+
stakingYieldsTest: false,
|
|
1016
|
+
limoTradesTest: false,
|
|
1017
|
+
strategyCount: 0,
|
|
1018
|
+
timestamp: new Date().toISOString(),
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// Test basic API connection
|
|
1022
|
+
try {
|
|
1023
|
+
const stakingYields = await this.getStakingYields();
|
|
1024
|
+
results.connectionTest = true;
|
|
1025
|
+
results.stakingYieldsTest = true;
|
|
1026
|
+
logger.log(
|
|
1027
|
+
`API connection test passed. Found ${stakingYields.length} staking yields`,
|
|
1028
|
+
);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
logger.error("API connection test failed:", error);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Test Limo trades endpoint
|
|
1034
|
+
try {
|
|
1035
|
+
const limoTrades = await this.getLimoTrades();
|
|
1036
|
+
results.limoTradesTest = true;
|
|
1037
|
+
logger.log(
|
|
1038
|
+
`Limo trades test passed. Found ${limoTrades.length} trades`,
|
|
1039
|
+
);
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
logger.error("Limo trades test failed:", error);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Test strategy discovery
|
|
1045
|
+
try {
|
|
1046
|
+
const strategies = await this.getAllStrategies();
|
|
1047
|
+
results.strategyCount = strategies.length;
|
|
1048
|
+
logger.log(
|
|
1049
|
+
`Strategy discovery test: ${strategies.length} strategies found`,
|
|
1050
|
+
);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
logger.error("Strategy discovery test failed:", error);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
logger.log("Connection test completed");
|
|
1056
|
+
return results;
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
logger.error("Error in connection test:", error);
|
|
1059
|
+
throw error;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Service lifecycle methods
|
|
1064
|
+
|
|
1065
|
+
static async create(runtime: AgentRuntime): Promise<KaminoLiquidityService> {
|
|
1066
|
+
return new KaminoLiquidityService(runtime);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
static async start(runtime: AgentRuntime): Promise<KaminoLiquidityService> {
|
|
1070
|
+
const service = new KaminoLiquidityService(runtime);
|
|
1071
|
+
await service.start();
|
|
1072
|
+
return service;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
static async stop(runtime: AgentRuntime): Promise<void> {
|
|
1076
|
+
const service = runtime.getService(
|
|
1077
|
+
"KAMINO_LIQUIDITY_SERVICE",
|
|
1078
|
+
) as KaminoLiquidityService;
|
|
1079
|
+
if (service) {
|
|
1080
|
+
await service.stop();
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async start(): Promise<void> {
|
|
1085
|
+
if (this.isRunning) {
|
|
1086
|
+
logger.warn("KaminoLiquidityService is already running");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
logger.log("Starting KaminoLiquidityService...");
|
|
1092
|
+
|
|
1093
|
+
// Test connection on startup
|
|
1094
|
+
const testResults = await this.testConnection();
|
|
1095
|
+
logger.log("Startup connection test results:", testResults);
|
|
1096
|
+
|
|
1097
|
+
this.isRunning = true;
|
|
1098
|
+
logger.log("KaminoLiquidityService started successfully");
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
logger.error("Failed to start KaminoLiquidityService:", error);
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
async stop(): Promise<void> {
|
|
1106
|
+
if (!this.isRunning) {
|
|
1107
|
+
logger.warn("KaminoLiquidityService is not running");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
try {
|
|
1112
|
+
this.isRunning = false;
|
|
1113
|
+
logger.log("KaminoLiquidityService stopped successfully");
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
logger.error("Failed to stop KaminoLiquidityService:", error);
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
isServiceRunning(): boolean {
|
|
1121
|
+
return this.isRunning;
|
|
1122
|
+
}
|
|
1123
|
+
}
|