amped-defi 1.0.0
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/README.md +757 -0
- package/dist/__mocks__/@sodax/sdk.d.ts +24 -0
- package/dist/__mocks__/@sodax/sdk.d.ts.map +1 -0
- package/dist/__mocks__/@sodax/sdk.js +24 -0
- package/dist/__mocks__/@sodax/sdk.js.map +1 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +32 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/policyEngine.d.ts +119 -0
- package/dist/policy/policyEngine.d.ts.map +1 -0
- package/dist/policy/policyEngine.js +322 -0
- package/dist/policy/policyEngine.js.map +1 -0
- package/dist/providers/spokeProviderFactory.d.ts +38 -0
- package/dist/providers/spokeProviderFactory.d.ts.map +1 -0
- package/dist/providers/spokeProviderFactory.js +212 -0
- package/dist/providers/spokeProviderFactory.js.map +1 -0
- package/dist/sodax/client.d.ts +34 -0
- package/dist/sodax/client.d.ts.map +1 -0
- package/dist/sodax/client.js +99 -0
- package/dist/sodax/client.js.map +1 -0
- package/dist/tools/bridge.d.ts +105 -0
- package/dist/tools/bridge.d.ts.map +1 -0
- package/dist/tools/bridge.js +334 -0
- package/dist/tools/bridge.js.map +1 -0
- package/dist/tools/discovery.d.ts +141 -0
- package/dist/tools/discovery.d.ts.map +1 -0
- package/dist/tools/discovery.js +777 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/moneyMarket.d.ts +227 -0
- package/dist/tools/moneyMarket.d.ts.map +1 -0
- package/dist/tools/moneyMarket.js +867 -0
- package/dist/tools/moneyMarket.js.map +1 -0
- package/dist/tools/portfolio.d.ts +43 -0
- package/dist/tools/portfolio.d.ts.map +1 -0
- package/dist/tools/portfolio.js +538 -0
- package/dist/tools/portfolio.js.map +1 -0
- package/dist/tools/swap.d.ts +71 -0
- package/dist/tools/swap.d.ts.map +1 -0
- package/dist/tools/swap.js +762 -0
- package/dist/tools/swap.js.map +1 -0
- package/dist/tools/walletManagement.d.ts +80 -0
- package/dist/tools/walletManagement.d.ts.map +1 -0
- package/dist/tools/walletManagement.js +289 -0
- package/dist/tools/walletManagement.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errorUtils.d.ts +2 -0
- package/dist/utils/errorUtils.d.ts.map +1 -0
- package/dist/utils/errorUtils.js +19 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/dist/utils/errors.d.ts +144 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +310 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/positionAggregator.d.ts +122 -0
- package/dist/utils/positionAggregator.d.ts.map +1 -0
- package/dist/utils/positionAggregator.js +377 -0
- package/dist/utils/positionAggregator.js.map +1 -0
- package/dist/utils/priceService.d.ts +45 -0
- package/dist/utils/priceService.d.ts.map +1 -0
- package/dist/utils/priceService.js +108 -0
- package/dist/utils/priceService.js.map +1 -0
- package/dist/utils/sodaxApi.d.ts +92 -0
- package/dist/utils/sodaxApi.d.ts.map +1 -0
- package/dist/utils/sodaxApi.js +143 -0
- package/dist/utils/sodaxApi.js.map +1 -0
- package/dist/utils/tokenResolver.d.ts +54 -0
- package/dist/utils/tokenResolver.d.ts.map +1 -0
- package/dist/utils/tokenResolver.js +252 -0
- package/dist/utils/tokenResolver.js.map +1 -0
- package/dist/wallet/backendConfig.d.ts +37 -0
- package/dist/wallet/backendConfig.d.ts.map +1 -0
- package/dist/wallet/backendConfig.js +125 -0
- package/dist/wallet/backendConfig.js.map +1 -0
- package/dist/wallet/backends/BankrBackend.d.ts +73 -0
- package/dist/wallet/backends/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/backends/BankrBackend.js +315 -0
- package/dist/wallet/backends/BankrBackend.js.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts +75 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.js +243 -0
- package/dist/wallet/backends/BankrWalletProvider.js.map +1 -0
- package/dist/wallet/backends/EnvBackend.d.ts +50 -0
- package/dist/wallet/backends/EnvBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EnvBackend.js +114 -0
- package/dist/wallet/backends/EnvBackend.js.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts +40 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js +81 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js.map +1 -0
- package/dist/wallet/backends/index.d.ts +10 -0
- package/dist/wallet/backends/index.d.ts.map +1 -0
- package/dist/wallet/backends/index.js +10 -0
- package/dist/wallet/backends/index.js.map +1 -0
- package/dist/wallet/index.d.ts +9 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +12 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts +107 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.js +208 -0
- package/dist/wallet/providers/AmpedWalletProvider.js.map +1 -0
- package/dist/wallet/providers/BankrBackend.d.ts +105 -0
- package/dist/wallet/providers/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/providers/BankrBackend.js +327 -0
- package/dist/wallet/providers/BankrBackend.js.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts +62 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.js +152 -0
- package/dist/wallet/providers/LocalKeyBackend.js.map +1 -0
- package/dist/wallet/providers/chainConfig.d.ts +209 -0
- package/dist/wallet/providers/chainConfig.d.ts.map +1 -0
- package/dist/wallet/providers/chainConfig.js +175 -0
- package/dist/wallet/providers/chainConfig.js.map +1 -0
- package/dist/wallet/providers/index.d.ts +30 -0
- package/dist/wallet/providers/index.d.ts.map +1 -0
- package/dist/wallet/providers/index.js +32 -0
- package/dist/wallet/providers/index.js.map +1 -0
- package/dist/wallet/providers/types.d.ts +156 -0
- package/dist/wallet/providers/types.d.ts.map +1 -0
- package/dist/wallet/providers/types.js +11 -0
- package/dist/wallet/providers/types.js.map +1 -0
- package/dist/wallet/skillWalletAdapter.d.ts +96 -0
- package/dist/wallet/skillWalletAdapter.d.ts.map +1 -0
- package/dist/wallet/skillWalletAdapter.js +280 -0
- package/dist/wallet/skillWalletAdapter.js.map +1 -0
- package/dist/wallet/types.d.ts +134 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/types.js +138 -0
- package/dist/wallet/types.js.map +1 -0
- package/dist/wallet/walletManager.d.ts +111 -0
- package/dist/wallet/walletManager.d.ts.map +1 -0
- package/dist/wallet/walletManager.js +476 -0
- package/dist/wallet/walletManager.js.map +1 -0
- package/dist/wallet/walletRegistry.d.ts +95 -0
- package/dist/wallet/walletRegistry.d.ts.map +1 -0
- package/dist/wallet/walletRegistry.js +184 -0
- package/dist/wallet/walletRegistry.js.map +1 -0
- package/index.js +2 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +69 -0
- package/src/__mocks__/@sodax/sdk.ts +28 -0
- package/src/__tests__/errors.test.ts +238 -0
- package/src/__tests__/policyEngine.test.ts +354 -0
- package/src/__tests__/positionAggregator.test.ts +271 -0
- package/src/__tests__/setup.ts +35 -0
- package/src/__tests__/sodaxApi.test.ts +203 -0
- package/src/__tests__/walletRegistry.test.ts +155 -0
- package/src/index.ts +376 -0
- package/src/policy/policyEngine.ts +389 -0
- package/src/providers/spokeProviderFactory.ts +283 -0
- package/src/sodax/client.ts +113 -0
- package/src/tools/bridge.ts +425 -0
- package/src/tools/discovery.ts +989 -0
- package/src/tools/moneyMarket.ts +1265 -0
- package/src/tools/portfolio.ts +697 -0
- package/src/tools/swap.ts +926 -0
- package/src/tools/walletManagement.ts +359 -0
- package/src/types.ts +228 -0
- package/src/utils/errorUtils.ts +16 -0
- package/src/utils/errors.ts +396 -0
- package/src/utils/positionAggregator.ts +559 -0
- package/src/utils/priceService.ts +153 -0
- package/src/utils/sodaxApi.ts +261 -0
- package/src/utils/tokenResolver.ts +286 -0
- package/src/wallet/backendConfig.ts +151 -0
- package/src/wallet/backends/BankrBackend.ts +399 -0
- package/src/wallet/backends/BankrWalletProvider.ts +329 -0
- package/src/wallet/backends/EnvBackend.ts +149 -0
- package/src/wallet/backends/EvmWalletSkillBackend.ts +110 -0
- package/src/wallet/backends/index.ts +10 -0
- package/src/wallet/index.ts +14 -0
- package/src/wallet/providers/AmpedWalletProvider.ts +267 -0
- package/src/wallet/providers/BankrBackend.ts +407 -0
- package/src/wallet/providers/LocalKeyBackend.ts +184 -0
- package/src/wallet/providers/chainConfig.ts +194 -0
- package/src/wallet/providers/index.ts +62 -0
- package/src/wallet/providers/types.ts +186 -0
- package/src/wallet/skillWalletAdapter.ts +335 -0
- package/src/wallet/types.ts +248 -0
- package/src/wallet/walletManager.ts +561 -0
- package/src/wallet/walletRegistry.ts +216 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery/Read Tools for Amped DeFi Plugin
|
|
3
|
+
*
|
|
4
|
+
* These tools provide read-only access to:
|
|
5
|
+
* - Supported chains and tokens
|
|
6
|
+
* - Wallet address resolution
|
|
7
|
+
* - Money market positions and reserves
|
|
8
|
+
*
|
|
9
|
+
* @module tools/discovery
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Type, Static } from '@sinclair/typebox';
|
|
13
|
+
import { getSodaxClient } from '../sodax/client';
|
|
14
|
+
import { toSodaxChainId } from '../wallet/types';
|
|
15
|
+
import { getSpokeProvider } from '../providers/spokeProviderFactory';
|
|
16
|
+
import { getWalletManager, type IWalletBackend, type WalletInfo } from '../wallet';
|
|
17
|
+
import {
|
|
18
|
+
aggregateCrossChainPositions,
|
|
19
|
+
formatHealthFactor,
|
|
20
|
+
getHealthFactorStatus,
|
|
21
|
+
getPositionRecommendation
|
|
22
|
+
} from '../utils/positionAggregator';
|
|
23
|
+
import { getSodaxApiClient } from '../utils/sodaxApi';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// TypeBox Schemas
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Schema for amped_supported_chains - no parameters required
|
|
31
|
+
*/
|
|
32
|
+
const SupportedChainsSchema = Type.Object({});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Schema for amped_supported_tokens
|
|
36
|
+
*/
|
|
37
|
+
const SupportedTokensSchema = Type.Object({
|
|
38
|
+
module: Type.Union([
|
|
39
|
+
Type.Literal('swaps'),
|
|
40
|
+
Type.Literal('bridge'),
|
|
41
|
+
Type.Literal('moneyMarket'),
|
|
42
|
+
]),
|
|
43
|
+
chainId: Type.String({
|
|
44
|
+
description: 'Spoke chain ID (e.g., "ethereum", "arbitrum", "sonic")',
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Schema for amped_wallet_address
|
|
50
|
+
*/
|
|
51
|
+
const WalletAddressSchema = Type.Object({
|
|
52
|
+
walletId: Type.String({
|
|
53
|
+
description: 'Unique identifier for the wallet',
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schema for amped_money_market_positions
|
|
59
|
+
*/
|
|
60
|
+
const MoneyMarketPositionsSchema = Type.Object({
|
|
61
|
+
walletId: Type.String({
|
|
62
|
+
description: 'Unique identifier for the wallet',
|
|
63
|
+
}),
|
|
64
|
+
chainId: Type.String({
|
|
65
|
+
description: 'Spoke chain ID to query positions on',
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Schema for amped_money_market_reserves
|
|
71
|
+
*/
|
|
72
|
+
const MoneyMarketReservesSchema = Type.Object({
|
|
73
|
+
chainId: Type.Optional(
|
|
74
|
+
Type.String({
|
|
75
|
+
description:
|
|
76
|
+
'Optional chain ID. Money market is hub-centric, so this filters results for a specific spoke chain if needed',
|
|
77
|
+
})
|
|
78
|
+
),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Schema for amped_cross_chain_positions
|
|
83
|
+
* Get aggregated positions view across all chains
|
|
84
|
+
*/
|
|
85
|
+
const CrossChainPositionsSchema = Type.Object({
|
|
86
|
+
walletId: Type.String({
|
|
87
|
+
description: 'Unique identifier for the wallet',
|
|
88
|
+
}),
|
|
89
|
+
chainIds: Type.Optional(
|
|
90
|
+
Type.Array(Type.String(), {
|
|
91
|
+
description: 'Optional array of specific chain IDs to query (defaults to all supported chains)',
|
|
92
|
+
})
|
|
93
|
+
),
|
|
94
|
+
includeZeroBalances: Type.Optional(
|
|
95
|
+
Type.Boolean({
|
|
96
|
+
description: 'Include positions with zero balance',
|
|
97
|
+
default: false,
|
|
98
|
+
})
|
|
99
|
+
),
|
|
100
|
+
minUsdValue: Type.Optional(
|
|
101
|
+
Type.Number({
|
|
102
|
+
description: 'Minimum USD value threshold for including positions',
|
|
103
|
+
default: 0.01,
|
|
104
|
+
})
|
|
105
|
+
),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Schema for amped_user_intents
|
|
110
|
+
* Query user intent history from SODAX API
|
|
111
|
+
*/
|
|
112
|
+
const UserIntentsSchema = Type.Object({
|
|
113
|
+
walletId: Type.String({
|
|
114
|
+
description: 'Unique identifier for the wallet',
|
|
115
|
+
}),
|
|
116
|
+
status: Type.Optional(
|
|
117
|
+
Type.Union([
|
|
118
|
+
Type.Literal('all', { description: 'All intents (open and closed)' }),
|
|
119
|
+
Type.Literal('open', { description: 'Only open/pending intents' }),
|
|
120
|
+
Type.Literal('closed', { description: 'Only filled/cancelled/expired intents' }),
|
|
121
|
+
], {
|
|
122
|
+
description: 'Filter by intent status',
|
|
123
|
+
default: 'all',
|
|
124
|
+
})
|
|
125
|
+
),
|
|
126
|
+
limit: Type.Optional(
|
|
127
|
+
Type.Number({
|
|
128
|
+
description: 'Number of items to return (default: 50, max: 100)',
|
|
129
|
+
default: 50,
|
|
130
|
+
minimum: 1,
|
|
131
|
+
maximum: 100,
|
|
132
|
+
})
|
|
133
|
+
),
|
|
134
|
+
offset: Type.Optional(
|
|
135
|
+
Type.Number({
|
|
136
|
+
description: 'Number of items to skip (for pagination)',
|
|
137
|
+
default: 0,
|
|
138
|
+
minimum: 0,
|
|
139
|
+
})
|
|
140
|
+
),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Schema for amped_list_wallets - List all configured wallets
|
|
145
|
+
*/
|
|
146
|
+
const ListWalletsSchema = Type.Object({});
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Type Definitions
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
type SupportedChainsParams = Static<typeof SupportedChainsSchema>;
|
|
153
|
+
type SupportedTokensParams = Static<typeof SupportedTokensSchema>;
|
|
154
|
+
type WalletAddressParams = Static<typeof WalletAddressSchema>;
|
|
155
|
+
type MoneyMarketPositionsParams = Static<typeof MoneyMarketPositionsSchema>;
|
|
156
|
+
type ListWalletsParams = Static<typeof ListWalletsSchema>;
|
|
157
|
+
type MoneyMarketReservesParams = Static<typeof MoneyMarketReservesSchema>;
|
|
158
|
+
type CrossChainPositionsParams = Static<typeof CrossChainPositionsSchema>;
|
|
159
|
+
type UserIntentsParams = Static<typeof UserIntentsSchema>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* AgentTools interface for registering tools with the OpenClaw framework
|
|
163
|
+
*/
|
|
164
|
+
interface AgentTools {
|
|
165
|
+
register(tool: {
|
|
166
|
+
name: string;
|
|
167
|
+
summary: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
schema: unknown;
|
|
170
|
+
handler: (params: unknown) => Promise<unknown>;
|
|
171
|
+
}): void;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Helper to wrap typed handlers for AgentTools registration
|
|
175
|
+
function wrapHandler<T>(handler: (params: T) => Promise<unknown>): (params: unknown) => Promise<unknown> {
|
|
176
|
+
return (params: unknown) => handler(params as T);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Tool Implementations
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get supported spoke chains from SODAX configuration
|
|
185
|
+
*/
|
|
186
|
+
async function handleSupportedChains(
|
|
187
|
+
_params: SupportedChainsParams
|
|
188
|
+
): Promise<unknown> {
|
|
189
|
+
const sodax = getSodaxClient();
|
|
190
|
+
const chains = sodax.config.getSupportedSpokeChains();
|
|
191
|
+
|
|
192
|
+
// SDK may return chain IDs as strings or chain objects
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
chains: chains.map((chain: any) => {
|
|
196
|
+
// Handle both string IDs and chain objects
|
|
197
|
+
if (typeof chain === 'string') {
|
|
198
|
+
return {
|
|
199
|
+
id: chain,
|
|
200
|
+
name: chain,
|
|
201
|
+
type: 'evm',
|
|
202
|
+
isHub: chain === 'sonic',
|
|
203
|
+
nativeCurrency: undefined,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
id: chain.id || chain,
|
|
208
|
+
name: chain.name || chain.id || chain,
|
|
209
|
+
type: chain.type || 'evm',
|
|
210
|
+
isHub: (chain.id || chain) === 'sonic',
|
|
211
|
+
nativeCurrency: chain.nativeCurrency,
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get supported tokens for a specific module and chain
|
|
219
|
+
*/
|
|
220
|
+
async function handleSupportedTokens(
|
|
221
|
+
params: SupportedTokensParams
|
|
222
|
+
): Promise<unknown> {
|
|
223
|
+
const sodax = getSodaxClient();
|
|
224
|
+
const { module, chainId: rawChainId } = params;
|
|
225
|
+
const chainId = toSodaxChainId(rawChainId);
|
|
226
|
+
|
|
227
|
+
let tokens: Array<{
|
|
228
|
+
address: string;
|
|
229
|
+
symbol: string;
|
|
230
|
+
name: string;
|
|
231
|
+
decimals: number;
|
|
232
|
+
logoURI?: string;
|
|
233
|
+
}> = [];
|
|
234
|
+
|
|
235
|
+
// Helper to normalize token data
|
|
236
|
+
const normalizeToken = (token: any) => ({
|
|
237
|
+
address: token.address || '',
|
|
238
|
+
symbol: token.symbol || '',
|
|
239
|
+
name: token.name || token.symbol || '',
|
|
240
|
+
decimals: token.decimals || 18,
|
|
241
|
+
logoURI: token.logoURI || token.logoUri,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
switch (module) {
|
|
245
|
+
case 'swaps': {
|
|
246
|
+
// Get supported swap tokens by chain ID
|
|
247
|
+
// SDK may require chainId to be cast to specific type
|
|
248
|
+
try {
|
|
249
|
+
const swapTokens = sodax.config.getSupportedSwapTokensByChainId(chainId as any);
|
|
250
|
+
tokens = (swapTokens || []).map(normalizeToken);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
console.warn('[discovery] Failed to get swap tokens:', e);
|
|
253
|
+
tokens = [];
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
case 'bridge': {
|
|
260
|
+
// Get bridgeable tokens via hub assets
|
|
261
|
+
// Hub assets represent tokens that can be bridged between chains
|
|
262
|
+
// Reference: sodax-frontend uses getHubAssets() for bridge token discovery
|
|
263
|
+
try {
|
|
264
|
+
const hubAssets = sodax.config.getHubAssets();
|
|
265
|
+
|
|
266
|
+
// Check if this is the hub chain (Sonic)
|
|
267
|
+
const isHubChain = rawChainId === 'sonic' || chainId === 'sonic';
|
|
268
|
+
|
|
269
|
+
if (isHubChain) {
|
|
270
|
+
// For Sonic (hub), show all bridgeable assets from all spoke chains
|
|
271
|
+
// These are the assets that can be bridged FROM Sonic to other chains
|
|
272
|
+
const allTokens: typeof tokens = [];
|
|
273
|
+
const seenAddresses = new Set<string>();
|
|
274
|
+
|
|
275
|
+
for (const spokeChainId of Object.keys(hubAssets)) {
|
|
276
|
+
const chainAssets = hubAssets[spokeChainId as keyof typeof hubAssets] || {};
|
|
277
|
+
for (const asset of Object.values(chainAssets)) {
|
|
278
|
+
// Add the hub asset (on Sonic) - dedupe by hub address
|
|
279
|
+
const hubAddress = (asset as any).asset || (asset as any).hubAddress || (asset as any).address;
|
|
280
|
+
if (hubAddress && !seenAddresses.has(hubAddress.toLowerCase())) {
|
|
281
|
+
seenAddresses.add(hubAddress.toLowerCase());
|
|
282
|
+
allTokens.push(normalizeToken({
|
|
283
|
+
address: hubAddress,
|
|
284
|
+
symbol: (asset as any).symbol || '',
|
|
285
|
+
name: (asset as any).name || (asset as any).symbol || '',
|
|
286
|
+
decimals: (asset as any).decimals || 18,
|
|
287
|
+
logoURI: (asset as any).logoURI || (asset as any).logoUri,
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
tokens = allTokens;
|
|
293
|
+
} else {
|
|
294
|
+
// For spoke chains, get assets bridgeable from that specific chain
|
|
295
|
+
const chainAssets = hubAssets[chainId as keyof typeof hubAssets] || {};
|
|
296
|
+
tokens = Object.values(chainAssets).map((asset: any) => normalizeToken({
|
|
297
|
+
address: (asset as any).asset || (asset as any).address || (asset as any).originalAddress || '',
|
|
298
|
+
symbol: asset.symbol || '',
|
|
299
|
+
name: asset.name || asset.symbol || '',
|
|
300
|
+
decimals: (asset as any).decimal || (asset as any).decimals || 18,
|
|
301
|
+
logoURI: asset.logoURI || asset.logoUri,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.warn('[discovery] Failed to get bridge tokens:', e);
|
|
306
|
+
tokens = [];
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
case 'moneyMarket': {
|
|
312
|
+
// Get money market supported tokens from config
|
|
313
|
+
// Reference: sodax-frontend ConfigService.getSupportedMoneyMarketTokensByChainId
|
|
314
|
+
try {
|
|
315
|
+
const mmTokens = sodax.config.getSupportedMoneyMarketTokensByChainId?.(chainId as any);
|
|
316
|
+
if (mmTokens && Array.isArray(mmTokens)) {
|
|
317
|
+
tokens = mmTokens.map(normalizeToken);
|
|
318
|
+
} else {
|
|
319
|
+
// Fallback: try supportedMoneyMarketTokens directly from config
|
|
320
|
+
const allMmTokens = (sodax.config as any).sodaxConfig?.supportedMoneyMarketTokens;
|
|
321
|
+
if (allMmTokens && allMmTokens[chainId]) {
|
|
322
|
+
tokens = allMmTokens[chainId].map(normalizeToken);
|
|
323
|
+
} else {
|
|
324
|
+
console.warn('[discovery] No money market tokens found for chain', chainId);
|
|
325
|
+
tokens = [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
console.warn('[discovery] Failed to get money market tokens:', e);
|
|
330
|
+
tokens = [];
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
default:
|
|
336
|
+
throw new Error(`Unknown module: ${module}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
module,
|
|
342
|
+
chainId,
|
|
343
|
+
tokens,
|
|
344
|
+
count: tokens.length,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get wallet address by walletId
|
|
350
|
+
* Returns enhanced wallet info with source and supported chains
|
|
351
|
+
*/
|
|
352
|
+
async function handleWalletAddress(
|
|
353
|
+
params: WalletAddressParams
|
|
354
|
+
): Promise<unknown> {
|
|
355
|
+
const { walletId } = params;
|
|
356
|
+
|
|
357
|
+
// Get wallet from unified WalletManager
|
|
358
|
+
const walletManager = getWalletManager();
|
|
359
|
+
const wallet = await walletManager.resolve(walletId);
|
|
360
|
+
|
|
361
|
+
const address = await wallet.getAddress();
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
walletId: wallet.nickname,
|
|
366
|
+
address,
|
|
367
|
+
type: wallet.type,
|
|
368
|
+
chains: [...wallet.supportedChains],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get user money market positions (humanized format)
|
|
374
|
+
*/
|
|
375
|
+
async function handleMoneyMarketPositions(
|
|
376
|
+
params: MoneyMarketPositionsParams
|
|
377
|
+
): Promise<unknown> {
|
|
378
|
+
const { walletId, chainId } = params;
|
|
379
|
+
|
|
380
|
+
// Get wallet from unified WalletManager
|
|
381
|
+
const walletManager = getWalletManager();
|
|
382
|
+
const wallet = await walletManager.resolve(walletId);
|
|
383
|
+
const walletAddress = await wallet.getAddress();
|
|
384
|
+
|
|
385
|
+
// Get spoke provider for this wallet and chain
|
|
386
|
+
const spokeProvider = await getSpokeProvider(walletId, chainId);
|
|
387
|
+
|
|
388
|
+
const sodax = getSodaxClient();
|
|
389
|
+
|
|
390
|
+
// IMPORTANT: getUserReservesHumanized() only returns raw balances without token metadata.
|
|
391
|
+
// To get token symbols/names, we must:
|
|
392
|
+
// 1. Fetch getReservesHumanized() for token metadata
|
|
393
|
+
// 2. Format with formatReservesUSD(buildReserveDataWithPrice())
|
|
394
|
+
// 3. Join with formatUserSummary(buildUserSummaryRequest())
|
|
395
|
+
// Reference: sodax-frontend/packages/dapp-kit/src/hooks/mm/useUserFormattedSummary.ts
|
|
396
|
+
|
|
397
|
+
// Step 1: Fetch reserves with token metadata (symbols, names, decimals)
|
|
398
|
+
const reserves = await sodax.moneyMarket.data.getReservesHumanized();
|
|
399
|
+
|
|
400
|
+
// Step 2: Format reserves with USD prices
|
|
401
|
+
const formattedReserves = sodax.moneyMarket.data.formatReservesUSD(
|
|
402
|
+
sodax.moneyMarket.data.buildReserveDataWithPrice(reserves)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Step 3: Fetch user-specific balances
|
|
406
|
+
const userReservesResult = await sodax.moneyMarket.data.getUserReservesHumanized(
|
|
407
|
+
spokeProvider
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Step 4: Join reserves metadata with user balances via formatUserSummary
|
|
411
|
+
const userSummary = sodax.moneyMarket.data.formatUserSummary(
|
|
412
|
+
sodax.moneyMarket.data.buildUserSummaryRequest(reserves, formattedReserves, userReservesResult)
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// Extract user reserves from the formatted summary
|
|
416
|
+
// The formatted summary has userReservesData with proper token metadata
|
|
417
|
+
const userReservesData = (userSummary as any).userReservesData || [];
|
|
418
|
+
|
|
419
|
+
// Format positions for readability
|
|
420
|
+
const positions = userReservesData.map((reserve: any) => {
|
|
421
|
+
// Get supply balance (underlyingBalance is the human-readable supply amount)
|
|
422
|
+
const supplyBalance = reserve.underlyingBalance || '0';
|
|
423
|
+
const supplyBalanceUsd = reserve.underlyingBalanceUSD || '0';
|
|
424
|
+
|
|
425
|
+
// Get borrow balance (variableBorrows is the human-readable borrow amount)
|
|
426
|
+
const borrowBalance = reserve.variableBorrows || reserve.totalBorrows || '0';
|
|
427
|
+
const borrowBalanceUsd = reserve.variableBorrowsUSD || reserve.totalBorrowsUSD || '0';
|
|
428
|
+
|
|
429
|
+
// Get APY values (formatted reserves have these)
|
|
430
|
+
const supplyApy = parseFloat(reserve.reserve?.supplyAPY || '0') * 100;
|
|
431
|
+
const borrowApy = parseFloat(reserve.reserve?.variableBorrowAPY || '0') * 100;
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
token: {
|
|
435
|
+
address: reserve.underlyingAsset || reserve.reserve?.underlyingAsset || '',
|
|
436
|
+
symbol: reserve.reserve?.symbol || '',
|
|
437
|
+
name: reserve.reserve?.name || '',
|
|
438
|
+
decimals: reserve.reserve?.decimals || 18,
|
|
439
|
+
},
|
|
440
|
+
supply: {
|
|
441
|
+
balance: supplyBalance,
|
|
442
|
+
balanceUsd: supplyBalanceUsd,
|
|
443
|
+
apy: supplyApy,
|
|
444
|
+
collateral: reserve.usageAsCollateralEnabledOnUser ?? false,
|
|
445
|
+
},
|
|
446
|
+
borrow: {
|
|
447
|
+
balance: borrowBalance,
|
|
448
|
+
balanceUsd: borrowBalanceUsd,
|
|
449
|
+
apy: borrowApy,
|
|
450
|
+
},
|
|
451
|
+
// Health indicators
|
|
452
|
+
loanToValue: parseFloat(reserve.reserve?.baseLTVasCollateral || '0') / 10000,
|
|
453
|
+
liquidationThreshold: parseFloat(reserve.reserve?.reserveLiquidationThreshold || '0') / 10000,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Filter to only positions with activity
|
|
458
|
+
const activePositions = positions.filter((p: any) =>
|
|
459
|
+
parseFloat(p.supply.balance) > 0 || parseFloat(p.borrow.balance) > 0
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// Calculate summary metrics
|
|
463
|
+
const totalSupplyUsd = activePositions.reduce(
|
|
464
|
+
(sum: number, p: any) => sum + (parseFloat(p.supply.balanceUsd) || 0),
|
|
465
|
+
0
|
|
466
|
+
);
|
|
467
|
+
const totalBorrowUsd = activePositions.reduce(
|
|
468
|
+
(sum: number, p: any) => sum + (parseFloat(p.borrow.balanceUsd) || 0),
|
|
469
|
+
0
|
|
470
|
+
);
|
|
471
|
+
const netWorthUsd = totalSupplyUsd - totalBorrowUsd;
|
|
472
|
+
const healthFactor =
|
|
473
|
+
totalBorrowUsd > 0 ? totalSupplyUsd / totalBorrowUsd : Infinity;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
success: true,
|
|
477
|
+
walletId,
|
|
478
|
+
address: walletAddress,
|
|
479
|
+
chainId,
|
|
480
|
+
positions: activePositions,
|
|
481
|
+
summary: {
|
|
482
|
+
totalSupplyUsd: totalSupplyUsd.toFixed(2),
|
|
483
|
+
totalBorrowUsd: totalBorrowUsd.toFixed(2),
|
|
484
|
+
netWorthUsd: netWorthUsd.toFixed(2),
|
|
485
|
+
healthFactor: healthFactor === Infinity ? '∞' : healthFactor.toFixed(2),
|
|
486
|
+
positionCount: activePositions.length,
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Get money market reserves (humanized format)
|
|
493
|
+
* Hub-centric: returns reserves across all markets
|
|
494
|
+
*/
|
|
495
|
+
async function handleMoneyMarketReserves(
|
|
496
|
+
params: MoneyMarketReservesParams
|
|
497
|
+
): Promise<unknown> {
|
|
498
|
+
const { chainId } = params;
|
|
499
|
+
|
|
500
|
+
const sodax = getSodaxClient();
|
|
501
|
+
|
|
502
|
+
// Get reserves in humanized format (hub-centric)
|
|
503
|
+
const reservesResult = await sodax.moneyMarket.data.getReservesHumanized();
|
|
504
|
+
|
|
505
|
+
// SDK may return ReservesDataHumanized object with .reservesData array or just array
|
|
506
|
+
const reservesArray = Array.isArray(reservesResult)
|
|
507
|
+
? reservesResult
|
|
508
|
+
: (reservesResult as any).reservesData || [];
|
|
509
|
+
|
|
510
|
+
// Filter by chainId if provided
|
|
511
|
+
let filteredReserves = reservesArray;
|
|
512
|
+
if (chainId) {
|
|
513
|
+
filteredReserves = reservesArray.filter(
|
|
514
|
+
(r: any) => r.token?.chainId === chainId || r.hubChainId === chainId || r.chainId === chainId
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Format reserves for readability
|
|
519
|
+
const formattedReserves = filteredReserves.map((reserve: any) => ({
|
|
520
|
+
token: {
|
|
521
|
+
address: reserve.token?.address || reserve.underlyingAsset || '',
|
|
522
|
+
symbol: reserve.token?.symbol || reserve.symbol || '',
|
|
523
|
+
name: reserve.token?.name || reserve.name || '',
|
|
524
|
+
decimals: reserve.token?.decimals || reserve.decimals || 18,
|
|
525
|
+
chainId: reserve.token?.chainId || reserve.chainId || '',
|
|
526
|
+
},
|
|
527
|
+
liquidity: {
|
|
528
|
+
totalSupply: reserve.liquidity?.totalSupply || reserve.totalScaledVariableDebt || '0',
|
|
529
|
+
availableLiquidity: reserve.liquidity?.availableLiquidity || reserve.availableLiquidity || '0',
|
|
530
|
+
totalBorrow: reserve.liquidity?.totalBorrow || reserve.totalVariableDebt || '0',
|
|
531
|
+
utilizationRate: reserve.liquidity?.utilizationRate || reserve.utilizationRate || '0',
|
|
532
|
+
},
|
|
533
|
+
rates: {
|
|
534
|
+
supplyApy: reserve.rates?.supplyApy || reserve.supplyAPY || '0',
|
|
535
|
+
borrowApy: reserve.rates?.borrowApy || reserve.variableBorrowAPY || '0',
|
|
536
|
+
},
|
|
537
|
+
parameters: {
|
|
538
|
+
loanToValue: reserve.parameters?.loanToValue || reserve.baseLTVasCollateral || '0',
|
|
539
|
+
liquidationThreshold: reserve.parameters?.liquidationThreshold || reserve.reserveLiquidationThreshold || '0',
|
|
540
|
+
liquidationBonus: reserve.parameters?.liquidationBonus || reserve.reserveLiquidationBonus || '0',
|
|
541
|
+
},
|
|
542
|
+
hubChainId: reserve.hubChainId || 'sonic',
|
|
543
|
+
}));
|
|
544
|
+
|
|
545
|
+
// Calculate aggregate metrics
|
|
546
|
+
const totalAvailableLiquidity = formattedReserves.reduce(
|
|
547
|
+
(sum: number, r: any) => sum + (parseFloat(r.liquidity.availableLiquidity) || 0),
|
|
548
|
+
0
|
|
549
|
+
);
|
|
550
|
+
const totalBorrowed = formattedReserves.reduce(
|
|
551
|
+
(sum: number, r: any) => sum + (parseFloat(r.liquidity.totalBorrow) || 0),
|
|
552
|
+
0
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
success: true,
|
|
557
|
+
chainId: chainId || 'all',
|
|
558
|
+
reserves: formattedReserves,
|
|
559
|
+
summary: {
|
|
560
|
+
reserveCount: formattedReserves.length,
|
|
561
|
+
totalAvailableLiquidity: totalAvailableLiquidity.toFixed(2),
|
|
562
|
+
totalBorrowed: totalBorrowed.toFixed(2),
|
|
563
|
+
globalUtilizationRate:
|
|
564
|
+
totalAvailableLiquidity + totalBorrowed > 0
|
|
565
|
+
? (
|
|
566
|
+
(totalBorrowed / (totalAvailableLiquidity + totalBorrowed)) *
|
|
567
|
+
100
|
|
568
|
+
).toFixed(2) + '%'
|
|
569
|
+
: '0%',
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ============================================================================
|
|
575
|
+
// Cross-Chain Positions Tool
|
|
576
|
+
// ============================================================================
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get aggregated money market positions across all chains
|
|
580
|
+
*
|
|
581
|
+
* This provides a unified view of:
|
|
582
|
+
* - Total supply/borrow across all networks
|
|
583
|
+
* - Health factor and liquidation risk
|
|
584
|
+
* - Available borrowing power
|
|
585
|
+
* - Net position and APY
|
|
586
|
+
* - Risk metrics and recommendations
|
|
587
|
+
*/
|
|
588
|
+
async function handleCrossChainPositions(
|
|
589
|
+
params: CrossChainPositionsParams
|
|
590
|
+
): Promise<unknown> {
|
|
591
|
+
const { walletId, chainIds, includeZeroBalances, minUsdValue } = params;
|
|
592
|
+
|
|
593
|
+
console.log('[discovery:crossChainPositions] Aggregating positions', {
|
|
594
|
+
walletId,
|
|
595
|
+
chainIds: chainIds || 'all',
|
|
596
|
+
includeZeroBalances,
|
|
597
|
+
minUsdValue,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const view = await aggregateCrossChainPositions(walletId, {
|
|
602
|
+
chainIds,
|
|
603
|
+
includeZeroBalances,
|
|
604
|
+
minUsdValue,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Get recommendations
|
|
608
|
+
const recommendations = getPositionRecommendation(view);
|
|
609
|
+
|
|
610
|
+
// Format response
|
|
611
|
+
const response = {
|
|
612
|
+
success: true,
|
|
613
|
+
walletId: view.walletId,
|
|
614
|
+
address: view.address,
|
|
615
|
+
timestamp: view.timestamp,
|
|
616
|
+
summary: {
|
|
617
|
+
totalSupplyUsd: view.summary.totalSupplyUsd.toFixed(2),
|
|
618
|
+
totalBorrowUsd: view.summary.totalBorrowUsd.toFixed(2),
|
|
619
|
+
netWorthUsd: view.summary.netWorthUsd.toFixed(2),
|
|
620
|
+
availableBorrowUsd: view.summary.availableBorrowUsd.toFixed(2),
|
|
621
|
+
healthFactor: formatHealthFactor(view.summary.healthFactor),
|
|
622
|
+
healthFactorStatus: getHealthFactorStatus(view.summary.healthFactor),
|
|
623
|
+
liquidationRisk: view.summary.liquidationRisk,
|
|
624
|
+
weightedSupplyApy: `${(view.summary.weightedSupplyApy * 100).toFixed(2)}%`,
|
|
625
|
+
weightedBorrowApy: `${(view.summary.weightedBorrowApy * 100).toFixed(2)}%`,
|
|
626
|
+
netApy: `${(view.summary.netApy * 100).toFixed(2)}%`,
|
|
627
|
+
},
|
|
628
|
+
chainBreakdown: view.chainSummaries.map(cs => ({
|
|
629
|
+
chainId: cs.chainId,
|
|
630
|
+
supplyUsd: cs.supplyUsd.toFixed(2),
|
|
631
|
+
borrowUsd: cs.borrowUsd.toFixed(2),
|
|
632
|
+
netWorthUsd: cs.netWorthUsd.toFixed(2),
|
|
633
|
+
healthFactor: formatHealthFactor(cs.healthFactor),
|
|
634
|
+
positionCount: cs.positionCount,
|
|
635
|
+
})),
|
|
636
|
+
collateralUtilization: {
|
|
637
|
+
totalCollateralUsd: view.collateralUtilization.totalCollateralUsd.toFixed(2),
|
|
638
|
+
usedCollateralUsd: view.collateralUtilization.usedCollateralUsd.toFixed(2),
|
|
639
|
+
availableCollateralUsd: view.collateralUtilization.availableCollateralUsd.toFixed(2),
|
|
640
|
+
utilizationRate: `${view.collateralUtilization.utilizationRate.toFixed(2)}%`,
|
|
641
|
+
},
|
|
642
|
+
riskMetrics: {
|
|
643
|
+
maxLtv: `${(view.riskMetrics.maxLtv * 100).toFixed(2)}%`,
|
|
644
|
+
currentLtv: `${(view.riskMetrics.currentLtv * 100).toFixed(2)}%`,
|
|
645
|
+
bufferUntilLiquidation: `${view.riskMetrics.bufferUntilLiquidation.toFixed(2)}%`,
|
|
646
|
+
safeMaxBorrowUsd: view.riskMetrics.safeMaxBorrowUsd.toFixed(2),
|
|
647
|
+
},
|
|
648
|
+
positions: view.positions.map(pos => ({
|
|
649
|
+
chainId: pos.chainId,
|
|
650
|
+
token: pos.token,
|
|
651
|
+
supply: {
|
|
652
|
+
balance: pos.supply.balance,
|
|
653
|
+
balanceUsd: pos.supply.balanceUsd,
|
|
654
|
+
apy: `${(pos.supply.apy * 100).toFixed(2)}%`,
|
|
655
|
+
isCollateral: pos.supply.isCollateral,
|
|
656
|
+
},
|
|
657
|
+
borrow: {
|
|
658
|
+
balance: pos.borrow.balance,
|
|
659
|
+
balanceUsd: pos.borrow.balanceUsd,
|
|
660
|
+
apy: `${(pos.borrow.apy * 100).toFixed(2)}%`,
|
|
661
|
+
},
|
|
662
|
+
loanToValue: `${(pos.loanToValue * 100).toFixed(2)}%`,
|
|
663
|
+
liquidationThreshold: `${(pos.liquidationThreshold * 100).toFixed(2)}%`,
|
|
664
|
+
})),
|
|
665
|
+
recommendations,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
console.log('[discovery:crossChainPositions] Aggregation complete', {
|
|
669
|
+
totalPositions: view.positions.length,
|
|
670
|
+
totalSupplyUsd: view.summary.totalSupplyUsd,
|
|
671
|
+
totalBorrowUsd: view.summary.totalBorrowUsd,
|
|
672
|
+
healthFactor: view.summary.healthFactor,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
return response;
|
|
676
|
+
} catch (error) {
|
|
677
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
678
|
+
console.error('[discovery:crossChainPositions] Failed to aggregate positions', {
|
|
679
|
+
error: errorMessage,
|
|
680
|
+
walletId,
|
|
681
|
+
});
|
|
682
|
+
throw new Error(`Failed to aggregate cross-chain positions: ${errorMessage}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ============================================================================
|
|
687
|
+
// User Intents Tool (SODAX API)
|
|
688
|
+
// ============================================================================
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Get user intents from SODAX API
|
|
692
|
+
*
|
|
693
|
+
* Queries the backend API for intent history including:
|
|
694
|
+
* - Open/pending intents
|
|
695
|
+
* - Filled intents
|
|
696
|
+
* - Cancelled/expired intents
|
|
697
|
+
* - Event history for each intent
|
|
698
|
+
*/
|
|
699
|
+
async function handleUserIntents(
|
|
700
|
+
params: UserIntentsParams
|
|
701
|
+
): Promise<unknown> {
|
|
702
|
+
const { walletId, status = 'all', limit = 50, offset = 0 } = params;
|
|
703
|
+
|
|
704
|
+
console.log('[discovery:userIntents] Fetching user intents', {
|
|
705
|
+
walletId,
|
|
706
|
+
status,
|
|
707
|
+
limit,
|
|
708
|
+
offset,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
// Get wallet address from unified WalletManager
|
|
713
|
+
const walletManager = getWalletManager();
|
|
714
|
+
const wallet = await walletManager.resolve(walletId);
|
|
715
|
+
const walletAddress = await wallet.getAddress();
|
|
716
|
+
|
|
717
|
+
// Initialize API client
|
|
718
|
+
const apiClient = getSodaxApiClient();
|
|
719
|
+
|
|
720
|
+
// Determine filters based on status
|
|
721
|
+
let filters;
|
|
722
|
+
if (status === 'open') {
|
|
723
|
+
filters = { open: true };
|
|
724
|
+
} else if (status === 'closed') {
|
|
725
|
+
filters = { open: false };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Fetch intents
|
|
729
|
+
const response = await apiClient.getUserIntents(
|
|
730
|
+
walletAddress,
|
|
731
|
+
{ limit, offset },
|
|
732
|
+
filters
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
// Format response
|
|
736
|
+
const formattedIntents = response.items.map(intent => ({
|
|
737
|
+
intentHash: intent.intentHash,
|
|
738
|
+
txHash: intent.txHash,
|
|
739
|
+
chainId: intent.chainId,
|
|
740
|
+
blockNumber: intent.blockNumber,
|
|
741
|
+
status: intent.open ? 'open' : 'closed',
|
|
742
|
+
createdAt: intent.createdAt,
|
|
743
|
+
input: {
|
|
744
|
+
token: intent.intent.inputToken,
|
|
745
|
+
amount: intent.intent.inputAmount,
|
|
746
|
+
chainId: intent.intent.srcChain,
|
|
747
|
+
},
|
|
748
|
+
output: {
|
|
749
|
+
token: intent.intent.outputToken,
|
|
750
|
+
minAmount: intent.intent.minOutputAmount,
|
|
751
|
+
chainId: intent.intent.dstChain,
|
|
752
|
+
},
|
|
753
|
+
deadline: new Date(parseInt(intent.intent.deadline) * 1000).toISOString(),
|
|
754
|
+
allowPartialFill: intent.intent.allowPartialFill,
|
|
755
|
+
events: intent.events
|
|
756
|
+
.filter((event): event is typeof event & { intentState: NonNullable<typeof event.intentState> } =>
|
|
757
|
+
event.intentState != null
|
|
758
|
+
)
|
|
759
|
+
.map(event => ({
|
|
760
|
+
type: event.eventType,
|
|
761
|
+
txHash: event.txHash,
|
|
762
|
+
blockNumber: event.blockNumber,
|
|
763
|
+
state: {
|
|
764
|
+
remainingInput: event.intentState.remainingInput,
|
|
765
|
+
receivedOutput: event.intentState.receivedOutput,
|
|
766
|
+
pendingPayment: event.intentState.pendingPayment,
|
|
767
|
+
},
|
|
768
|
+
})),
|
|
769
|
+
}));
|
|
770
|
+
|
|
771
|
+
const result = {
|
|
772
|
+
success: true,
|
|
773
|
+
walletId,
|
|
774
|
+
address: walletAddress,
|
|
775
|
+
pagination: {
|
|
776
|
+
total: response.total,
|
|
777
|
+
offset: response.offset,
|
|
778
|
+
limit: response.limit,
|
|
779
|
+
hasMore: response.offset + response.items.length < response.total,
|
|
780
|
+
},
|
|
781
|
+
intents: formattedIntents,
|
|
782
|
+
summary: {
|
|
783
|
+
totalIntents: response.total,
|
|
784
|
+
returned: formattedIntents.length,
|
|
785
|
+
openIntents: formattedIntents.filter((i: { status: string }) => i.status === 'open').length,
|
|
786
|
+
closedIntents: formattedIntents.filter((i: { status: string }) => i.status === 'closed').length,
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
console.log('[discovery:userIntents] User intents fetched', {
|
|
791
|
+
total: response.total,
|
|
792
|
+
returned: formattedIntents.length,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return result;
|
|
796
|
+
} catch (error) {
|
|
797
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
798
|
+
console.error('[discovery:userIntents] Failed to fetch user intents', {
|
|
799
|
+
error: errorMessage,
|
|
800
|
+
walletId,
|
|
801
|
+
});
|
|
802
|
+
throw new Error(`Failed to fetch user intents: ${errorMessage}`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// List Wallets Tool
|
|
808
|
+
// ============================================================================
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* List all configured wallets with their nicknames, types, and supported chains
|
|
812
|
+
*/
|
|
813
|
+
async function handleListWallets(
|
|
814
|
+
_params: ListWalletsParams
|
|
815
|
+
): Promise<unknown> {
|
|
816
|
+
console.log('[discovery:listWallets] Listing configured wallets');
|
|
817
|
+
|
|
818
|
+
const walletManager = getWalletManager();
|
|
819
|
+
const wallets = await walletManager.listWallets();
|
|
820
|
+
|
|
821
|
+
const formattedWallets = wallets.map(w => ({
|
|
822
|
+
nickname: w.nickname,
|
|
823
|
+
type: w.type,
|
|
824
|
+
address: w.address,
|
|
825
|
+
addressKnown: w.address !== '0x...',
|
|
826
|
+
supportedChains: w.chains,
|
|
827
|
+
isDefault: w.isDefault,
|
|
828
|
+
note: w.address === '0x...' && w.type === 'bankr'
|
|
829
|
+
? 'Address pending - will be fetched on first use'
|
|
830
|
+
: undefined,
|
|
831
|
+
}));
|
|
832
|
+
|
|
833
|
+
const defaultWallet = await walletManager.getDefaultWalletName();
|
|
834
|
+
|
|
835
|
+
// Group by type for summary
|
|
836
|
+
const byType = {
|
|
837
|
+
'evm-wallet-skill': formattedWallets.filter(w => w.type === 'evm-wallet-skill'),
|
|
838
|
+
'bankr': formattedWallets.filter(w => w.type === 'bankr'),
|
|
839
|
+
'env': formattedWallets.filter(w => w.type === 'env'),
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// Check if Bankr is configured but wallet not found
|
|
843
|
+
const bankrKeyPresent = !!process.env.BANKR_API_KEY;
|
|
844
|
+
const bankrWalletFound = byType.bankr.length > 0;
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
success: true,
|
|
848
|
+
wallets: formattedWallets,
|
|
849
|
+
defaultWallet,
|
|
850
|
+
count: formattedWallets.length,
|
|
851
|
+
summary: {
|
|
852
|
+
selfCustody: byType['evm-wallet-skill'].length + byType.env.length,
|
|
853
|
+
bankrManaged: byType.bankr.length,
|
|
854
|
+
},
|
|
855
|
+
sources: {
|
|
856
|
+
evmWalletSkill: byType['evm-wallet-skill'].length > 0,
|
|
857
|
+
bankr: bankrWalletFound,
|
|
858
|
+
bankrKeyConfigured: bankrKeyPresent,
|
|
859
|
+
env: byType.env.length > 0,
|
|
860
|
+
},
|
|
861
|
+
hint: wallets.length === 0
|
|
862
|
+
? 'No wallets configured. Install evm-wallet-skill: git clone https://github.com/amped-finance/evm-wallet-skill.git ~/.openclaw/skills/evm-wallet-skill'
|
|
863
|
+
: bankrKeyPresent && !bankrWalletFound
|
|
864
|
+
? 'Bankr API key found but wallet not loaded. Try "Add my bankr wallet" to register it.'
|
|
865
|
+
: 'Use wallet nickname in operations, e.g., "swap 100 USDC to ETH using main"',
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// Tool Registration
|
|
871
|
+
// ============================================================================
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Register all discovery tools with the agent tools registry
|
|
875
|
+
*
|
|
876
|
+
* @param agentTools - The OpenClaw AgentTools instance
|
|
877
|
+
*/
|
|
878
|
+
export function registerDiscoveryTools(agentTools: AgentTools): void {
|
|
879
|
+
// 1. amped_supported_chains - Get supported spoke chains
|
|
880
|
+
agentTools.register({
|
|
881
|
+
name: 'amped_supported_chains',
|
|
882
|
+
summary:
|
|
883
|
+
'Get a list of all supported spoke chains for swaps, bridging, and money market operations',
|
|
884
|
+
schema: SupportedChainsSchema,
|
|
885
|
+
handler: wrapHandler(handleSupportedChains),
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// 2. amped_supported_tokens - Get supported tokens by module
|
|
889
|
+
agentTools.register({
|
|
890
|
+
name: 'amped_supported_tokens',
|
|
891
|
+
summary:
|
|
892
|
+
'Get supported tokens for a specific module (swaps, bridge, or moneyMarket) on a given chain',
|
|
893
|
+
schema: SupportedTokensSchema,
|
|
894
|
+
handler: wrapHandler(handleSupportedTokens),
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// 3. amped_wallet_address - Get wallet address
|
|
898
|
+
agentTools.register({
|
|
899
|
+
name: 'amped_wallet_address',
|
|
900
|
+
summary:
|
|
901
|
+
'Get the resolved wallet address for a given walletId. Validates private key matches in execute mode.',
|
|
902
|
+
schema: WalletAddressSchema,
|
|
903
|
+
handler: wrapHandler(handleWalletAddress),
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// 4. amped_money_market_positions - Get user positions (humanized)
|
|
907
|
+
agentTools.register({
|
|
908
|
+
name: 'amped_money_market_positions',
|
|
909
|
+
summary:
|
|
910
|
+
'Get humanized money market positions for a wallet on a specific chain, including supply/borrow balances and health metrics',
|
|
911
|
+
schema: MoneyMarketPositionsSchema,
|
|
912
|
+
handler: wrapHandler(handleMoneyMarketPositions),
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// 5. amped_money_market_reserves - Get market reserves (humanized)
|
|
916
|
+
agentTools.register({
|
|
917
|
+
name: 'amped_money_market_reserves',
|
|
918
|
+
summary:
|
|
919
|
+
'Get humanized money market reserves data including liquidity, rates, and parameters. Hub-centric with optional chain filtering.',
|
|
920
|
+
schema: MoneyMarketReservesSchema,
|
|
921
|
+
handler: wrapHandler(handleMoneyMarketReserves),
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// 6. amped_cross_chain_positions - Get aggregated positions across all chains
|
|
925
|
+
agentTools.register({
|
|
926
|
+
name: 'amped_cross_chain_positions',
|
|
927
|
+
summary:
|
|
928
|
+
'Get a unified view of money market positions across ALL chains. Shows total supply/borrow, health factor, borrowing power, net APY, and risk metrics.',
|
|
929
|
+
description:
|
|
930
|
+
'Aggregates money market positions across all supported chains to provide a comprehensive portfolio view. ' +
|
|
931
|
+
'Includes: total supply/borrow in USD, health factor with risk status, available borrowing power, ' +
|
|
932
|
+
'weighted APYs, collateral utilization, and personalized recommendations. ' +
|
|
933
|
+
'This is the recommended tool for getting a complete picture of money market positions.',
|
|
934
|
+
schema: CrossChainPositionsSchema,
|
|
935
|
+
handler: wrapHandler(handleCrossChainPositions),
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// 7. amped_user_intents - Query user intent history from SODAX API
|
|
939
|
+
agentTools.register({
|
|
940
|
+
name: 'amped_user_intents',
|
|
941
|
+
summary:
|
|
942
|
+
'Query user swap intent history from SODAX backend API. Shows open, filled, and cancelled intents with event details.',
|
|
943
|
+
description:
|
|
944
|
+
'Retrieves the complete intent history for a wallet from the SODAX backend API. ' +
|
|
945
|
+
'Includes open intents (pending), filled intents (completed), and cancelled/expired intents. ' +
|
|
946
|
+
'Each intent includes input/output tokens, amounts, chain IDs, and event history. ' +
|
|
947
|
+
'Use this to track the status of past swaps and bridge operations.',
|
|
948
|
+
schema: UserIntentsSchema,
|
|
949
|
+
handler: wrapHandler(handleUserIntents),
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// 8. amped_list_wallets - List all configured wallets
|
|
953
|
+
agentTools.register({
|
|
954
|
+
name: 'amped_list_wallets',
|
|
955
|
+
summary:
|
|
956
|
+
'List all configured wallets with their nicknames, types, addresses, and supported chains.',
|
|
957
|
+
description:
|
|
958
|
+
'Shows all available wallets from evm-wallet-skill (~/.evm-wallet.json), Bankr API, ' +
|
|
959
|
+
'and environment variables (AMPED_OC_WALLETS_JSON). Each wallet has a nickname that can be ' +
|
|
960
|
+
'used in operations like "swap 100 USDC using bankr" or "check balance on main". ' +
|
|
961
|
+
'Also shows which chains each wallet supports.',
|
|
962
|
+
schema: ListWalletsSchema,
|
|
963
|
+
handler: wrapHandler(handleListWallets),
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Export schemas for testing and reuse
|
|
968
|
+
export {
|
|
969
|
+
SupportedChainsSchema,
|
|
970
|
+
SupportedTokensSchema,
|
|
971
|
+
WalletAddressSchema,
|
|
972
|
+
MoneyMarketPositionsSchema,
|
|
973
|
+
MoneyMarketReservesSchema,
|
|
974
|
+
CrossChainPositionsSchema,
|
|
975
|
+
UserIntentsSchema,
|
|
976
|
+
ListWalletsSchema,
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// Export handlers for testing
|
|
980
|
+
export {
|
|
981
|
+
handleSupportedChains,
|
|
982
|
+
handleSupportedTokens,
|
|
983
|
+
handleWalletAddress,
|
|
984
|
+
handleMoneyMarketPositions,
|
|
985
|
+
handleMoneyMarketReserves,
|
|
986
|
+
handleCrossChainPositions,
|
|
987
|
+
handleUserIntents,
|
|
988
|
+
handleListWallets,
|
|
989
|
+
};
|