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.
Files changed (189) hide show
  1. package/README.md +757 -0
  2. package/dist/__mocks__/@sodax/sdk.d.ts +24 -0
  3. package/dist/__mocks__/@sodax/sdk.d.ts.map +1 -0
  4. package/dist/__mocks__/@sodax/sdk.js +24 -0
  5. package/dist/__mocks__/@sodax/sdk.js.map +1 -0
  6. package/dist/__tests__/setup.d.ts +4 -0
  7. package/dist/__tests__/setup.d.ts.map +1 -0
  8. package/dist/__tests__/setup.js +32 -0
  9. package/dist/__tests__/setup.js.map +1 -0
  10. package/dist/index.d.ts +66 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +281 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/policy/policyEngine.d.ts +119 -0
  15. package/dist/policy/policyEngine.d.ts.map +1 -0
  16. package/dist/policy/policyEngine.js +322 -0
  17. package/dist/policy/policyEngine.js.map +1 -0
  18. package/dist/providers/spokeProviderFactory.d.ts +38 -0
  19. package/dist/providers/spokeProviderFactory.d.ts.map +1 -0
  20. package/dist/providers/spokeProviderFactory.js +212 -0
  21. package/dist/providers/spokeProviderFactory.js.map +1 -0
  22. package/dist/sodax/client.d.ts +34 -0
  23. package/dist/sodax/client.d.ts.map +1 -0
  24. package/dist/sodax/client.js +99 -0
  25. package/dist/sodax/client.js.map +1 -0
  26. package/dist/tools/bridge.d.ts +105 -0
  27. package/dist/tools/bridge.d.ts.map +1 -0
  28. package/dist/tools/bridge.js +334 -0
  29. package/dist/tools/bridge.js.map +1 -0
  30. package/dist/tools/discovery.d.ts +141 -0
  31. package/dist/tools/discovery.d.ts.map +1 -0
  32. package/dist/tools/discovery.js +777 -0
  33. package/dist/tools/discovery.js.map +1 -0
  34. package/dist/tools/moneyMarket.d.ts +227 -0
  35. package/dist/tools/moneyMarket.d.ts.map +1 -0
  36. package/dist/tools/moneyMarket.js +867 -0
  37. package/dist/tools/moneyMarket.js.map +1 -0
  38. package/dist/tools/portfolio.d.ts +43 -0
  39. package/dist/tools/portfolio.d.ts.map +1 -0
  40. package/dist/tools/portfolio.js +538 -0
  41. package/dist/tools/portfolio.js.map +1 -0
  42. package/dist/tools/swap.d.ts +71 -0
  43. package/dist/tools/swap.d.ts.map +1 -0
  44. package/dist/tools/swap.js +762 -0
  45. package/dist/tools/swap.js.map +1 -0
  46. package/dist/tools/walletManagement.d.ts +80 -0
  47. package/dist/tools/walletManagement.d.ts.map +1 -0
  48. package/dist/tools/walletManagement.js +289 -0
  49. package/dist/tools/walletManagement.js.map +1 -0
  50. package/dist/types.d.ts +205 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +5 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/utils/errorUtils.d.ts +2 -0
  55. package/dist/utils/errorUtils.d.ts.map +1 -0
  56. package/dist/utils/errorUtils.js +19 -0
  57. package/dist/utils/errorUtils.js.map +1 -0
  58. package/dist/utils/errors.d.ts +144 -0
  59. package/dist/utils/errors.d.ts.map +1 -0
  60. package/dist/utils/errors.js +310 -0
  61. package/dist/utils/errors.js.map +1 -0
  62. package/dist/utils/positionAggregator.d.ts +122 -0
  63. package/dist/utils/positionAggregator.d.ts.map +1 -0
  64. package/dist/utils/positionAggregator.js +377 -0
  65. package/dist/utils/positionAggregator.js.map +1 -0
  66. package/dist/utils/priceService.d.ts +45 -0
  67. package/dist/utils/priceService.d.ts.map +1 -0
  68. package/dist/utils/priceService.js +108 -0
  69. package/dist/utils/priceService.js.map +1 -0
  70. package/dist/utils/sodaxApi.d.ts +92 -0
  71. package/dist/utils/sodaxApi.d.ts.map +1 -0
  72. package/dist/utils/sodaxApi.js +143 -0
  73. package/dist/utils/sodaxApi.js.map +1 -0
  74. package/dist/utils/tokenResolver.d.ts +54 -0
  75. package/dist/utils/tokenResolver.d.ts.map +1 -0
  76. package/dist/utils/tokenResolver.js +252 -0
  77. package/dist/utils/tokenResolver.js.map +1 -0
  78. package/dist/wallet/backendConfig.d.ts +37 -0
  79. package/dist/wallet/backendConfig.d.ts.map +1 -0
  80. package/dist/wallet/backendConfig.js +125 -0
  81. package/dist/wallet/backendConfig.js.map +1 -0
  82. package/dist/wallet/backends/BankrBackend.d.ts +73 -0
  83. package/dist/wallet/backends/BankrBackend.d.ts.map +1 -0
  84. package/dist/wallet/backends/BankrBackend.js +315 -0
  85. package/dist/wallet/backends/BankrBackend.js.map +1 -0
  86. package/dist/wallet/backends/BankrWalletProvider.d.ts +75 -0
  87. package/dist/wallet/backends/BankrWalletProvider.d.ts.map +1 -0
  88. package/dist/wallet/backends/BankrWalletProvider.js +243 -0
  89. package/dist/wallet/backends/BankrWalletProvider.js.map +1 -0
  90. package/dist/wallet/backends/EnvBackend.d.ts +50 -0
  91. package/dist/wallet/backends/EnvBackend.d.ts.map +1 -0
  92. package/dist/wallet/backends/EnvBackend.js +114 -0
  93. package/dist/wallet/backends/EnvBackend.js.map +1 -0
  94. package/dist/wallet/backends/EvmWalletSkillBackend.d.ts +40 -0
  95. package/dist/wallet/backends/EvmWalletSkillBackend.d.ts.map +1 -0
  96. package/dist/wallet/backends/EvmWalletSkillBackend.js +81 -0
  97. package/dist/wallet/backends/EvmWalletSkillBackend.js.map +1 -0
  98. package/dist/wallet/backends/index.d.ts +10 -0
  99. package/dist/wallet/backends/index.d.ts.map +1 -0
  100. package/dist/wallet/backends/index.js +10 -0
  101. package/dist/wallet/backends/index.js.map +1 -0
  102. package/dist/wallet/index.d.ts +9 -0
  103. package/dist/wallet/index.d.ts.map +1 -0
  104. package/dist/wallet/index.js +12 -0
  105. package/dist/wallet/index.js.map +1 -0
  106. package/dist/wallet/providers/AmpedWalletProvider.d.ts +107 -0
  107. package/dist/wallet/providers/AmpedWalletProvider.d.ts.map +1 -0
  108. package/dist/wallet/providers/AmpedWalletProvider.js +208 -0
  109. package/dist/wallet/providers/AmpedWalletProvider.js.map +1 -0
  110. package/dist/wallet/providers/BankrBackend.d.ts +105 -0
  111. package/dist/wallet/providers/BankrBackend.d.ts.map +1 -0
  112. package/dist/wallet/providers/BankrBackend.js +327 -0
  113. package/dist/wallet/providers/BankrBackend.js.map +1 -0
  114. package/dist/wallet/providers/LocalKeyBackend.d.ts +62 -0
  115. package/dist/wallet/providers/LocalKeyBackend.d.ts.map +1 -0
  116. package/dist/wallet/providers/LocalKeyBackend.js +152 -0
  117. package/dist/wallet/providers/LocalKeyBackend.js.map +1 -0
  118. package/dist/wallet/providers/chainConfig.d.ts +209 -0
  119. package/dist/wallet/providers/chainConfig.d.ts.map +1 -0
  120. package/dist/wallet/providers/chainConfig.js +175 -0
  121. package/dist/wallet/providers/chainConfig.js.map +1 -0
  122. package/dist/wallet/providers/index.d.ts +30 -0
  123. package/dist/wallet/providers/index.d.ts.map +1 -0
  124. package/dist/wallet/providers/index.js +32 -0
  125. package/dist/wallet/providers/index.js.map +1 -0
  126. package/dist/wallet/providers/types.d.ts +156 -0
  127. package/dist/wallet/providers/types.d.ts.map +1 -0
  128. package/dist/wallet/providers/types.js +11 -0
  129. package/dist/wallet/providers/types.js.map +1 -0
  130. package/dist/wallet/skillWalletAdapter.d.ts +96 -0
  131. package/dist/wallet/skillWalletAdapter.d.ts.map +1 -0
  132. package/dist/wallet/skillWalletAdapter.js +280 -0
  133. package/dist/wallet/skillWalletAdapter.js.map +1 -0
  134. package/dist/wallet/types.d.ts +134 -0
  135. package/dist/wallet/types.d.ts.map +1 -0
  136. package/dist/wallet/types.js +138 -0
  137. package/dist/wallet/types.js.map +1 -0
  138. package/dist/wallet/walletManager.d.ts +111 -0
  139. package/dist/wallet/walletManager.d.ts.map +1 -0
  140. package/dist/wallet/walletManager.js +476 -0
  141. package/dist/wallet/walletManager.js.map +1 -0
  142. package/dist/wallet/walletRegistry.d.ts +95 -0
  143. package/dist/wallet/walletRegistry.d.ts.map +1 -0
  144. package/dist/wallet/walletRegistry.js +184 -0
  145. package/dist/wallet/walletRegistry.js.map +1 -0
  146. package/index.js +2 -0
  147. package/openclaw.plugin.json +37 -0
  148. package/package.json +69 -0
  149. package/src/__mocks__/@sodax/sdk.ts +28 -0
  150. package/src/__tests__/errors.test.ts +238 -0
  151. package/src/__tests__/policyEngine.test.ts +354 -0
  152. package/src/__tests__/positionAggregator.test.ts +271 -0
  153. package/src/__tests__/setup.ts +35 -0
  154. package/src/__tests__/sodaxApi.test.ts +203 -0
  155. package/src/__tests__/walletRegistry.test.ts +155 -0
  156. package/src/index.ts +376 -0
  157. package/src/policy/policyEngine.ts +389 -0
  158. package/src/providers/spokeProviderFactory.ts +283 -0
  159. package/src/sodax/client.ts +113 -0
  160. package/src/tools/bridge.ts +425 -0
  161. package/src/tools/discovery.ts +989 -0
  162. package/src/tools/moneyMarket.ts +1265 -0
  163. package/src/tools/portfolio.ts +697 -0
  164. package/src/tools/swap.ts +926 -0
  165. package/src/tools/walletManagement.ts +359 -0
  166. package/src/types.ts +228 -0
  167. package/src/utils/errorUtils.ts +16 -0
  168. package/src/utils/errors.ts +396 -0
  169. package/src/utils/positionAggregator.ts +559 -0
  170. package/src/utils/priceService.ts +153 -0
  171. package/src/utils/sodaxApi.ts +261 -0
  172. package/src/utils/tokenResolver.ts +286 -0
  173. package/src/wallet/backendConfig.ts +151 -0
  174. package/src/wallet/backends/BankrBackend.ts +399 -0
  175. package/src/wallet/backends/BankrWalletProvider.ts +329 -0
  176. package/src/wallet/backends/EnvBackend.ts +149 -0
  177. package/src/wallet/backends/EvmWalletSkillBackend.ts +110 -0
  178. package/src/wallet/backends/index.ts +10 -0
  179. package/src/wallet/index.ts +14 -0
  180. package/src/wallet/providers/AmpedWalletProvider.ts +267 -0
  181. package/src/wallet/providers/BankrBackend.ts +407 -0
  182. package/src/wallet/providers/LocalKeyBackend.ts +184 -0
  183. package/src/wallet/providers/chainConfig.ts +194 -0
  184. package/src/wallet/providers/index.ts +62 -0
  185. package/src/wallet/providers/types.ts +186 -0
  186. package/src/wallet/skillWalletAdapter.ts +335 -0
  187. package/src/wallet/types.ts +248 -0
  188. package/src/wallet/walletManager.ts +561 -0
  189. package/src/wallet/walletRegistry.ts +216 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * SODAX API Client
3
+ *
4
+ * Provides access to SODAX backend API endpoints for querying intents,
5
+ * user history, and other off-chain data.
6
+ */
7
+
8
+ import { ErrorCode, AmpedDefiError } from './errors';
9
+
10
+ const DEFAULT_BASE_URL = 'https://canary-api.sodax.com';
11
+ const API_VERSION = 'v1';
12
+
13
+ export interface SodaxApiConfig {
14
+ baseUrl?: string;
15
+ apiKey?: string;
16
+ timeoutMs?: number;
17
+ }
18
+
19
+ export interface PaginationParams {
20
+ offset?: number;
21
+ limit?: number;
22
+ }
23
+
24
+ export interface PaginatedResponse<T> {
25
+ items: T[];
26
+ total: number;
27
+ offset: number;
28
+ limit: number;
29
+ }
30
+
31
+ export interface IntentState {
32
+ exists: boolean;
33
+ remainingInput: string;
34
+ receivedOutput: string;
35
+ pendingPayment: boolean;
36
+ }
37
+
38
+ export interface IntentEvent {
39
+ eventType: string;
40
+ txHash: string;
41
+ logIndex: number;
42
+ blockNumber: number;
43
+ intentState: IntentState;
44
+ }
45
+
46
+ export interface IntentDetails {
47
+ intentId: string;
48
+ creator: string;
49
+ inputToken: string;
50
+ outputToken: string;
51
+ inputAmount: string;
52
+ minOutputAmount: string;
53
+ deadline: string;
54
+ allowPartialFill: boolean;
55
+ srcChain: number;
56
+ dstChain: number;
57
+ srcAddress: string;
58
+ dstAddress: string;
59
+ solver: string;
60
+ data: string;
61
+ }
62
+
63
+ export interface UserIntent {
64
+ intentHash: string;
65
+ txHash: string;
66
+ logIndex: number;
67
+ chainId: number;
68
+ blockNumber: number;
69
+ open: boolean;
70
+ intent: IntentDetails;
71
+ events: IntentEvent[];
72
+ createdAt: string;
73
+ }
74
+
75
+ export interface UserIntentFilters {
76
+ open?: boolean;
77
+ srcChain?: number;
78
+ dstChain?: number;
79
+ inputToken?: string;
80
+ outputToken?: string;
81
+ }
82
+
83
+ export class SodaxApiClient {
84
+ private baseUrl: string;
85
+ private apiKey?: string;
86
+ private timeoutMs: number;
87
+
88
+ constructor(config: SodaxApiConfig = {}) {
89
+ this.baseUrl = config.baseUrl || process.env.SODAX_API_URL || DEFAULT_BASE_URL;
90
+ this.apiKey = config.apiKey || process.env.SODAX_API_KEY;
91
+ this.timeoutMs = config.timeoutMs || 30000;
92
+ }
93
+
94
+ /**
95
+ * Get intent by intentHash
96
+ * Most reliable lookup method - works for all intents
97
+ */
98
+ async getIntentByHash(intentHash: string): Promise<UserIntent | null> {
99
+ const normalizedHash = intentHash.startsWith('0x') ? intentHash : `0x${intentHash}`;
100
+ const url = `${this.baseUrl}/${API_VERSION}/be/intent/${normalizedHash}`;
101
+
102
+ console.log('[sodaxApi] Fetching intent by hash:', { intentHash: normalizedHash });
103
+
104
+ try {
105
+ const response = await this.fetchWithTimeout(url);
106
+
107
+ if (!response.ok) {
108
+ if (response.status === 404) {
109
+ return null;
110
+ }
111
+ const errorText = await response.text();
112
+ throw new AmpedDefiError(
113
+ ErrorCode.UNKNOWN_ERROR,
114
+ `SODAX API error: ${response.status} ${errorText}`
115
+ );
116
+ }
117
+
118
+ return await response.json() as UserIntent;
119
+ } catch (error) {
120
+ if (error instanceof AmpedDefiError) throw error;
121
+ throw new AmpedDefiError(
122
+ ErrorCode.UNKNOWN_ERROR,
123
+ `Failed to fetch intent by hash: ${error instanceof Error ? error.message : String(error)}`
124
+ );
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get intent by transaction hash
130
+ * NOTE: This expects the HUB chain (Sonic) transaction hash, NOT spoke chain tx
131
+ */
132
+ async getIntentByTxHash(txHash: string): Promise<UserIntent | null> {
133
+ const normalizedHash = txHash.startsWith('0x') ? txHash : `0x${txHash}`;
134
+ const url = `${this.baseUrl}/${API_VERSION}/be/intent/tx/${normalizedHash}`;
135
+
136
+ console.log('[sodaxApi] Fetching intent by txHash:', { txHash: normalizedHash });
137
+
138
+ try {
139
+ const response = await this.fetchWithTimeout(url);
140
+
141
+ if (!response.ok) {
142
+ if (response.status === 404) {
143
+ return null;
144
+ }
145
+ const errorText = await response.text();
146
+ throw new AmpedDefiError(
147
+ ErrorCode.UNKNOWN_ERROR,
148
+ `SODAX API error: ${response.status} ${errorText}`
149
+ );
150
+ }
151
+
152
+ return await response.json() as UserIntent;
153
+ } catch (error) {
154
+ if (error instanceof AmpedDefiError) throw error;
155
+ throw new AmpedDefiError(
156
+ ErrorCode.UNKNOWN_ERROR,
157
+ `Failed to fetch intent by txHash: ${error instanceof Error ? error.message : String(error)}`
158
+ );
159
+ }
160
+ }
161
+
162
+ async getUserIntents(
163
+ userAddress: string,
164
+ pagination: PaginationParams = {},
165
+ filters?: UserIntentFilters
166
+ ): Promise<PaginatedResponse<UserIntent>> {
167
+ if (!this.isValidAddress(userAddress)) {
168
+ throw new AmpedDefiError(
169
+ ErrorCode.WALLET_INVALID_ADDRESS,
170
+ `Invalid user address: ${userAddress}`
171
+ );
172
+ }
173
+
174
+ const normalizedAddress = userAddress.toLowerCase();
175
+ const queryParams = new URLSearchParams();
176
+
177
+ if (pagination.offset !== undefined) {
178
+ queryParams.set('offset', pagination.offset.toString());
179
+ }
180
+ if (pagination.limit !== undefined) {
181
+ queryParams.set('limit', pagination.limit.toString());
182
+ }
183
+
184
+ if (filters) {
185
+ if (filters.open !== undefined) queryParams.set('open', filters.open.toString());
186
+ if (filters.srcChain !== undefined) queryParams.set('srcChain', filters.srcChain.toString());
187
+ if (filters.dstChain !== undefined) queryParams.set('dstChain', filters.dstChain.toString());
188
+ if (filters.inputToken) queryParams.set('inputToken', filters.inputToken.toLowerCase());
189
+ if (filters.outputToken) queryParams.set('outputToken', filters.outputToken.toLowerCase());
190
+ }
191
+
192
+ const queryString = queryParams.toString();
193
+ const url = `${this.baseUrl}/${API_VERSION}/be/intent/user/${normalizedAddress}${queryString ? `?${queryString}` : ''}`;
194
+
195
+ console.log('[sodaxApi] Fetching user intents:', { userAddress: normalizedAddress });
196
+
197
+ try {
198
+ const response = await this.fetchWithTimeout(url);
199
+
200
+ if (!response.ok) {
201
+ const errorText = await response.text();
202
+ throw new AmpedDefiError(
203
+ ErrorCode.UNKNOWN_ERROR,
204
+ `SODAX API error: ${response.status} ${errorText}`
205
+ );
206
+ }
207
+
208
+ return await response.json() as PaginatedResponse<UserIntent>;
209
+ } catch (error) {
210
+ if (error instanceof AmpedDefiError) throw error;
211
+ throw new AmpedDefiError(
212
+ ErrorCode.UNKNOWN_ERROR,
213
+ `Failed to fetch user intents: ${error instanceof Error ? error.message : String(error)}`
214
+ );
215
+ }
216
+ }
217
+
218
+ async getOpenIntents(
219
+ userAddress: string,
220
+ pagination: PaginationParams = {}
221
+ ): Promise<PaginatedResponse<UserIntent>> {
222
+ return this.getUserIntents(userAddress, pagination, { open: true });
223
+ }
224
+
225
+ async getIntentHistory(
226
+ userAddress: string,
227
+ pagination: PaginationParams = {}
228
+ ): Promise<PaginatedResponse<UserIntent>> {
229
+ return this.getUserIntents(userAddress, pagination, { open: false });
230
+ }
231
+
232
+ private async fetchWithTimeout(url: string): Promise<Response> {
233
+ const controller = new AbortController();
234
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
235
+
236
+ try {
237
+ const headers: Record<string, string> = { 'Accept': 'application/json' };
238
+ if (this.apiKey) headers['Authorization'] = `Bearer ${this.apiKey}`;
239
+ return await fetch(url, { signal: controller.signal, headers });
240
+ } finally {
241
+ clearTimeout(timeoutId);
242
+ }
243
+ }
244
+
245
+ private isValidAddress(address: string): boolean {
246
+ return /^0x[a-fA-F0-9]{40}$/.test(address);
247
+ }
248
+ }
249
+
250
+ let apiClient: SodaxApiClient | null = null;
251
+
252
+ export function getSodaxApiClient(config?: SodaxApiConfig): SodaxApiClient {
253
+ if (!apiClient) {
254
+ apiClient = new SodaxApiClient(config);
255
+ }
256
+ return apiClient;
257
+ }
258
+
259
+ export function resetSodaxApiClient(): void {
260
+ apiClient = null;
261
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Token Resolution Utility
3
+ *
4
+ * Resolves token symbols to addresses using the SODAX SDK config service.
5
+ * Supports case-insensitive symbol lookup with caching.
6
+ * Handles both EVM (0x) and Solana (base58) address formats.
7
+ */
8
+
9
+ import type { Token } from '@sodax/types';
10
+ import { getSodaxClient } from '../sodax/client';
11
+ import { toSodaxChainId } from '../wallet/types';
12
+
13
+ // Cache tokens per chain to avoid repeated lookups
14
+ const tokenCache = new Map<string, Token[]>();
15
+
16
+ // Native token addresses
17
+ const EVM_NATIVE_ADDRESS = '0x0000000000000000000000000000000000000000';
18
+ const SOLANA_NATIVE_ADDRESS = '11111111111111111111111111111111';
19
+
20
+ // Native token configs per chain (18 decimals for all EVM chains, 9 for Solana)
21
+ const NATIVE_TOKENS: Record<string, { symbol: string; name: string; decimals: number; address: string }> = {
22
+ sonic: { symbol: 'S', name: 'Sonic', decimals: 18, address: EVM_NATIVE_ADDRESS },
23
+ ethereum: { symbol: 'ETH', name: 'Ether', decimals: 18, address: EVM_NATIVE_ADDRESS },
24
+ '0xa4b1.arbitrum': { symbol: 'ETH', name: 'Ether', decimals: 18, address: EVM_NATIVE_ADDRESS },
25
+ '0x2105.base': { symbol: 'ETH', name: 'Ether', decimals: 18, address: EVM_NATIVE_ADDRESS },
26
+ '0xa.optimism': { symbol: 'ETH', name: 'Ether', decimals: 18, address: EVM_NATIVE_ADDRESS },
27
+ '0x38.bsc': { symbol: 'BNB', name: 'BNB', decimals: 18, address: EVM_NATIVE_ADDRESS },
28
+ '0x89.polygon': { symbol: 'POL', name: 'POL', decimals: 18, address: EVM_NATIVE_ADDRESS },
29
+ '0xa86a.avax': { symbol: 'AVAX', name: 'Avalanche', decimals: 18, address: EVM_NATIVE_ADDRESS },
30
+ hyper: { symbol: 'HYPE', name: 'Hyperliquid', decimals: 18, address: EVM_NATIVE_ADDRESS },
31
+ lightlink: { symbol: 'ETH', name: 'Ether', decimals: 18, address: EVM_NATIVE_ADDRESS },
32
+ solana: { symbol: 'SOL', name: 'Solana', decimals: 9, address: SOLANA_NATIVE_ADDRESS },
33
+ };
34
+
35
+ // Fallback token list for common chains when SDK config is unavailable
36
+ const FALLBACK_TOKENS: Record<string, { address: string; symbol: string; name: string; decimals: number }[]> = {
37
+ '0x2105.base': [
38
+ { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', symbol: 'USDC', name: 'USD Coin', decimals: 6 },
39
+ { address: '0x4200000000000000000000000000000000000006', symbol: 'WETH', name: 'Wrapped Ether', decimals: 18 },
40
+ { address: EVM_NATIVE_ADDRESS, symbol: 'ETH', name: 'Ether', decimals: 18 },
41
+ ],
42
+ 'ethereum': [
43
+ { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', name: 'USD Coin', decimals: 6 },
44
+ { address: '0xdac17f958d2ee523a2206206994597c13d831ec7', symbol: 'USDT', name: 'Tether USD', decimals: 6 },
45
+ { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', symbol: 'DAI', name: 'Dai Stablecoin', decimals: 18 },
46
+ { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', name: 'Wrapped Ether', decimals: 18 },
47
+ { address: EVM_NATIVE_ADDRESS, symbol: 'ETH', name: 'Ether', decimals: 18 },
48
+ ],
49
+ '0xa4b1.arbitrum': [
50
+ { address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', symbol: 'USDC', name: 'USD Coin', decimals: 6 },
51
+ { address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', symbol: 'USDT', name: 'Tether USD', decimals: 6 },
52
+ { address: EVM_NATIVE_ADDRESS, symbol: 'ETH', name: 'Ether', decimals: 18 },
53
+ ],
54
+ 'sonic': [
55
+ { address: '0x29219dd400f2Bf60E5a23d13Be72B486D4038894', symbol: 'USDC', name: 'USD Coin', decimals: 6 },
56
+ { address: '0x6047828dc181963ba44974801FF68e538dA5eaF9', symbol: 'USDT', name: 'Tether USD', decimals: 6 },
57
+ { address: EVM_NATIVE_ADDRESS, symbol: 'S', name: 'Sonic', decimals: 18 },
58
+ ],
59
+ 'solana': [
60
+ { address: SOLANA_NATIVE_ADDRESS, symbol: 'SOL', name: 'Solana', decimals: 9 },
61
+ { address: '3rSPCLNEF7Quw4wX8S1NyKivELoyij8eYA2gJwBgt4V5', symbol: 'bnUSD', name: 'bnUSD', decimals: 9 },
62
+ { address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', symbol: 'USDC', name: 'USD Coin', decimals: 6 },
63
+ ],
64
+ };
65
+
66
+ // Chain type detection
67
+ const SOLANA_CHAINS = new Set(['solana']);
68
+
69
+ function isSolanaChain(chainId: string): boolean {
70
+ return SOLANA_CHAINS.has(chainId.toLowerCase());
71
+ }
72
+
73
+ /**
74
+ * Check if an address is a native token for the given chain
75
+ */
76
+ function isNativeToken(address: string, chainId?: string): boolean {
77
+ const addrLower = address.toLowerCase();
78
+ if (chainId && isSolanaChain(chainId)) {
79
+ return addrLower === SOLANA_NATIVE_ADDRESS.toLowerCase();
80
+ }
81
+ return addrLower === EVM_NATIVE_ADDRESS;
82
+ }
83
+
84
+ /**
85
+ * Get native token info for a chain
86
+ */
87
+ function getNativeTokenInfo(chainId: string): { address: string; symbol: string; name: string; decimals: number } | null {
88
+ const native = NATIVE_TOKENS[chainId];
89
+ if (!native) return null;
90
+ return { ...native };
91
+ }
92
+
93
+ /**
94
+ * Check if a string is a valid EVM address (0x format)
95
+ */
96
+ function isEvmAddress(value: string): boolean {
97
+ return /^0x[a-fA-F0-9]{40}$/i.test(value);
98
+ }
99
+
100
+ /**
101
+ * Check if a string is a valid Solana address (base58 format)
102
+ * Solana addresses are 32-44 characters, base58 encoded
103
+ */
104
+ function isSolanaAddress(value: string): boolean {
105
+ // Base58 charset: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
106
+ // Excludes: 0, O, I, l
107
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value);
108
+ }
109
+
110
+ /**
111
+ * Check if a string is a valid token address (EVM or Solana)
112
+ */
113
+ function isValidTokenAddress(value: string, chainId?: string): boolean {
114
+ if (chainId && isSolanaChain(chainId)) {
115
+ return isSolanaAddress(value);
116
+ }
117
+ // For EVM chains or unknown chains, check both formats
118
+ return isEvmAddress(value) || isSolanaAddress(value);
119
+ }
120
+
121
+ /**
122
+ * Populate the token cache for a chain from SDK config service
123
+ * This is the canonical way to get tokens - used by both resolveToken and getTokenInfo
124
+ */
125
+ function populateTokenCache(chainId: string): Token[] {
126
+ // Convert to SDK chain ID format (e.g., "base" -> "0x2105.base")
127
+ const sdkChainId = toSodaxChainId(chainId);
128
+ let tokens = tokenCache.get(chainId);
129
+ if (tokens) return tokens;
130
+
131
+ try {
132
+ const client = getSodaxClient();
133
+ const configService = (client as any).config;
134
+
135
+ if (configService?.getSupportedSwapTokensByChainId) {
136
+ // Preferred method - returns readonly Token[]
137
+ tokens = [...configService.getSupportedSwapTokensByChainId(sdkChainId)] as Token[];
138
+ } else if (configService?.getSwapTokensByChainId) {
139
+ tokens = configService.getSwapTokensByChainId(sdkChainId) as Token[];
140
+ } else if (configService?.getSwapTokens) {
141
+ const allTokens = configService.getSwapTokens();
142
+ tokens = allTokens[sdkChainId] || [];
143
+ } else {
144
+ console.warn(`[tokenResolver] configService not available for chain ${chainId}`);
145
+ tokens = [];
146
+ }
147
+
148
+ // Log what we got from SDK
149
+ if (tokens && tokens.length > 0) {
150
+ console.log(`[tokenResolver] Loaded ${tokens.length} tokens from SDK for ${chainId}`);
151
+ }
152
+ } catch (err) {
153
+ console.error(`[tokenResolver] Failed to fetch tokens for chain ${chainId}:`, err);
154
+ tokens = [];
155
+ }
156
+
157
+ // Use fallback tokens if SDK returned empty list
158
+ if ((!tokens || tokens.length === 0) && FALLBACK_TOKENS[chainId] || FALLBACK_TOKENS[sdkChainId]) {
159
+ console.log(`[tokenResolver] Using fallback token list for ${chainId}`);
160
+ tokens = FALLBACK_TOKENS[chainId] || FALLBACK_TOKENS[sdkChainId] as unknown as Token[];
161
+ }
162
+
163
+ tokenCache.set(chainId, tokens || []);
164
+ return tokens || [];
165
+ }
166
+
167
+ /**
168
+ * Resolve a token symbol or address to a normalized address
169
+ *
170
+ * @param chainId - The chain ID to resolve the token on
171
+ * @param tokenInput - Token symbol (e.g., "USDC") or address
172
+ * @returns The token address (lowercase for EVM, original case for Solana)
173
+ * @throws Error if token symbol is not found on the chain
174
+ */
175
+ export async function resolveToken(
176
+ chainId: string,
177
+ tokenInput: string
178
+ ): Promise<string> {
179
+ // If already a valid address, normalize and return
180
+ if (isValidTokenAddress(tokenInput, chainId)) {
181
+ // EVM addresses are lowercased, Solana addresses preserve case
182
+ return isEvmAddress(tokenInput) ? tokenInput.toLowerCase() : tokenInput;
183
+ }
184
+
185
+ // Get tokens from cache or SDK
186
+ const tokens = populateTokenCache(chainId);
187
+
188
+ // Find by symbol (case-insensitive)
189
+ const symbolUpper = tokenInput.toUpperCase();
190
+ const token = tokens.find(t => t.symbol.toUpperCase() === symbolUpper);
191
+
192
+ if (!token) {
193
+ // Build helpful error message with available tokens
194
+ const available = tokens.length > 0
195
+ ? tokens.map(t => t.symbol).join(', ')
196
+ : 'No tokens loaded';
197
+ throw new Error(
198
+ `Unknown token "${tokenInput}" on chain ${chainId}. ` +
199
+ `Available: ${available}. ` +
200
+ `Alternatively, provide the token address directly.`
201
+ );
202
+ }
203
+
204
+ return isEvmAddress(token.address) ? token.address.toLowerCase() : token.address;
205
+ }
206
+
207
+ /**
208
+ * Resolve multiple tokens at once (for efficiency)
209
+ *
210
+ * @param chainId - The chain ID
211
+ * @param tokenInputs - Array of token symbols or addresses
212
+ * @returns Array of resolved addresses
213
+ */
214
+ export async function resolveTokens(
215
+ chainId: string,
216
+ tokenInputs: string[]
217
+ ): Promise<string[]> {
218
+ return Promise.all(tokenInputs.map(t => resolveToken(chainId, t)));
219
+ }
220
+
221
+ /**
222
+ * Get token info by symbol or address
223
+ * Returns null if not found
224
+ */
225
+ export async function getTokenInfo(
226
+ chainId: string,
227
+ tokenInput: string
228
+ ): Promise<Token | null> {
229
+ const sdkChainId = toSodaxChainId(chainId);
230
+ // Handle native tokens first
231
+ if (isValidTokenAddress(tokenInput, chainId) && isNativeToken(tokenInput, chainId)) {
232
+ const nativeInfo = getNativeTokenInfo(chainId);
233
+ if (nativeInfo) {
234
+ return nativeInfo as unknown as Token;
235
+ }
236
+ }
237
+
238
+ // Get tokens from cache or SDK (same path as resolveToken)
239
+ const tokens = populateTokenCache(chainId);
240
+
241
+ // Find by address or symbol
242
+ if (isValidTokenAddress(tokenInput, chainId)) {
243
+ // Normalize address for comparison
244
+ const addrNorm = isEvmAddress(tokenInput) ? tokenInput.toLowerCase() : tokenInput;
245
+ const found = tokens.find(t => {
246
+ const tokenAddr = isEvmAddress(t.address) ? t.address.toLowerCase() : t.address;
247
+ return tokenAddr === addrNorm;
248
+ });
249
+ if (found) {
250
+ return found;
251
+ }
252
+ // Check fallback tokens even if SDK tokens were loaded
253
+ const fallback = FALLBACK_TOKENS[chainId] || FALLBACK_TOKENS[sdkChainId];
254
+ if (fallback) {
255
+ const fallbackToken = fallback.find(t => {
256
+ const tokenAddr = isEvmAddress(t.address) ? t.address.toLowerCase() : t.address;
257
+ return tokenAddr === addrNorm;
258
+ });
259
+ if (fallbackToken) {
260
+ console.log(`[tokenResolver] Found ${fallbackToken.symbol} in fallback for ${chainId}`);
261
+ return fallbackToken as unknown as Token;
262
+ }
263
+ }
264
+ return null;
265
+ } else {
266
+ const symbolUpper = tokenInput.toUpperCase();
267
+ return tokens.find(t => t.symbol.toUpperCase() === symbolUpper) || null;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Clear the token cache (useful for testing or after config refresh)
273
+ */
274
+ export function clearTokenCache(): void {
275
+ tokenCache.clear();
276
+ }
277
+
278
+ /**
279
+ * Get all cached tokens for a chain
280
+ */
281
+ export function getCachedTokens(chainId: string): Token[] | undefined {
282
+ return tokenCache.get(chainId);
283
+ }
284
+
285
+ // Export address validation utilities for use by other modules
286
+ export { isEvmAddress, isSolanaAddress, isValidTokenAddress, isSolanaChain };
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Wallet Backend Configuration
3
+ *
4
+ * Detects and configures the appropriate wallet backend based on
5
+ * environment variables or config file.
6
+ *
7
+ * Supported backends:
8
+ * - localKey (default): Uses evm-wallet-skill local private keys
9
+ * - bankr: Uses Bankr Agent API for transaction execution
10
+ */
11
+
12
+ import { existsSync, readFileSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { homedir } from 'os';
15
+ import type { WalletBackendType } from './providers';
16
+
17
+ export interface BackendConfig {
18
+ backend: WalletBackendType;
19
+ bankrApiKey?: string;
20
+ bankrApiUrl?: string;
21
+ }
22
+
23
+ /**
24
+ * Default configuration
25
+ */
26
+ const DEFAULT_CONFIG: BackendConfig = {
27
+ backend: 'localKey',
28
+ };
29
+
30
+ /**
31
+ * Path to plugin config file
32
+ */
33
+ function getConfigPath(): string {
34
+ return join(homedir(), '.openclaw', 'extensions', 'amped-defi', 'config.json');
35
+ }
36
+
37
+ /**
38
+ * Load configuration from file
39
+ */
40
+ function loadConfigFile(): Partial<BackendConfig> | null {
41
+ const configPath = getConfigPath();
42
+
43
+ if (!existsSync(configPath)) {
44
+ return null;
45
+ }
46
+
47
+ try {
48
+ const content = readFileSync(configPath, 'utf-8');
49
+ const config = JSON.parse(content);
50
+ return {
51
+ backend: config.walletBackend as WalletBackendType,
52
+ bankrApiKey: config.bankrApiKey,
53
+ bankrApiUrl: config.bankrApiUrl,
54
+ };
55
+ } catch (error) {
56
+ console.warn('[backendConfig] Failed to load config file:', error);
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Load configuration from environment variables
63
+ */
64
+ function loadEnvConfig(): Partial<BackendConfig> {
65
+ const config: Partial<BackendConfig> = {};
66
+
67
+ // Check for explicit backend selection
68
+ const backendEnv = process.env.AMPED_OC_WALLET_BACKEND;
69
+ if (backendEnv === 'bankr' || backendEnv === 'localKey') {
70
+ config.backend = backendEnv;
71
+ }
72
+
73
+ // Check for Bankr API key (implies bankr backend)
74
+ const bankrApiKey = process.env.BANKR_API_KEY;
75
+ if (bankrApiKey) {
76
+ config.bankrApiKey = bankrApiKey;
77
+ // Auto-select bankr backend if API key is present
78
+ if (!config.backend) {
79
+ config.backend = 'bankr';
80
+ }
81
+ }
82
+
83
+ // Optional Bankr API URL override
84
+ const bankrApiUrl = process.env.BANKR_API_URL;
85
+ if (bankrApiUrl) {
86
+ config.bankrApiUrl = bankrApiUrl;
87
+ }
88
+
89
+ return config;
90
+ }
91
+
92
+ /**
93
+ * Get the resolved backend configuration
94
+ *
95
+ * Priority:
96
+ * 1. Environment variables
97
+ * 2. Config file
98
+ * 3. Defaults
99
+ */
100
+ export function getBackendConfig(): BackendConfig {
101
+ const fileConfig = loadConfigFile() || {};
102
+ const envConfig = loadEnvConfig();
103
+
104
+ // Merge with priority: env > file > default
105
+ const config: BackendConfig = {
106
+ ...DEFAULT_CONFIG,
107
+ ...fileConfig,
108
+ ...envConfig,
109
+ };
110
+
111
+ // Validate bankr configuration
112
+ if (config.backend === 'bankr' && !config.bankrApiKey) {
113
+ console.warn('[backendConfig] Bankr backend selected but no API key provided');
114
+ console.warn('[backendConfig] Set BANKR_API_KEY environment variable or add bankrApiKey to config.json');
115
+ console.warn('[backendConfig] Falling back to localKey backend');
116
+ config.backend = 'localKey';
117
+ }
118
+
119
+ // Set default Bankr API URL if not specified
120
+ if (config.backend === 'bankr' && !config.bankrApiUrl) {
121
+ config.bankrApiUrl = 'https://api.bankr.bot';
122
+ }
123
+
124
+ console.log(`[backendConfig] Using wallet backend: ${config.backend}`);
125
+
126
+ return config;
127
+ }
128
+
129
+ /**
130
+ * Check if Bankr backend is configured and available
131
+ */
132
+ export function isBankrConfigured(): boolean {
133
+ const config = getBackendConfig();
134
+ return config.backend === 'bankr' && !!config.bankrApiKey;
135
+ }
136
+
137
+ /**
138
+ * Get Bankr configuration if available
139
+ */
140
+ export function getBankrConfig(): { apiKey: string; apiUrl: string } | null {
141
+ const config = getBackendConfig();
142
+
143
+ if (config.backend !== 'bankr' || !config.bankrApiKey) {
144
+ return null;
145
+ }
146
+
147
+ return {
148
+ apiKey: config.bankrApiKey,
149
+ apiUrl: config.bankrApiUrl || 'https://api.bankr.bot',
150
+ };
151
+ }