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,354 @@
1
+ /**
2
+ * Policy Engine Tests
3
+ */
4
+
5
+ import { PolicyEngine, PolicyCheckResult } from '../policy/policyEngine';
6
+
7
+ // Mock environment variables
8
+ const originalEnv = process.env;
9
+
10
+ describe('PolicyEngine', () => {
11
+ beforeEach(() => {
12
+ jest.resetModules();
13
+ process.env = { ...originalEnv };
14
+ delete process.env.AMPED_OC_LIMITS_JSON;
15
+ });
16
+
17
+ afterAll(() => {
18
+ process.env = originalEnv;
19
+ });
20
+
21
+ describe('Configuration Loading', () => {
22
+ it('should load empty config when env not set', () => {
23
+ const engine = new PolicyEngine();
24
+ const config = engine.getConfig();
25
+ expect(config).toEqual({});
26
+ });
27
+
28
+ it('should load default policy config', () => {
29
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
30
+ default: {
31
+ maxSlippageBps: 100,
32
+ allowedChains: ['ethereum', 'arbitrum'],
33
+ },
34
+ });
35
+
36
+ const engine = new PolicyEngine();
37
+ const config = engine.getConfig();
38
+ expect(config.maxSlippageBps).toBe(100);
39
+ expect(config.allowedChains).toEqual(['ethereum', 'arbitrum']);
40
+ });
41
+
42
+ it('should load specific policy by ID', () => {
43
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
44
+ default: { maxSlippageBps: 100 },
45
+ aggressive: { maxSlippageBps: 300 },
46
+ conservative: { maxSlippageBps: 50 },
47
+ });
48
+
49
+ const engine = new PolicyEngine('aggressive');
50
+ const config = engine.getConfig();
51
+ expect(config.maxSlippageBps).toBe(300);
52
+ });
53
+
54
+ it('should fallback to default when policy ID not found', () => {
55
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
56
+ default: { maxSlippageBps: 100 },
57
+ });
58
+
59
+ const engine = new PolicyEngine('nonexistent');
60
+ const config = engine.getConfig();
61
+ expect(config.maxSlippageBps).toBe(100);
62
+ });
63
+
64
+ it('should handle invalid JSON gracefully', () => {
65
+ process.env.AMPED_OC_LIMITS_JSON = 'invalid json';
66
+
67
+ const engine = new PolicyEngine();
68
+ const config = engine.getConfig();
69
+ expect(config).toEqual({});
70
+ });
71
+ });
72
+
73
+ describe('Bridge Policy Checks', () => {
74
+ beforeEach(() => {
75
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
76
+ default: {
77
+ allowedChains: ['ethereum', 'arbitrum', 'sonic'],
78
+ allowedTokensByChain: {
79
+ ethereum: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'],
80
+ },
81
+ maxBridgeAmountToken: {
82
+ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 10000,
83
+ },
84
+ blockedRecipients: ['0x0000000000000000000000000000000000000000'],
85
+ },
86
+ });
87
+ });
88
+
89
+ it('should allow valid bridge operation', async () => {
90
+ const engine = new PolicyEngine();
91
+ const result = await engine.checkBridge({
92
+ walletId: 'test',
93
+ srcChainId: 'ethereum',
94
+ dstChainId: 'arbitrum',
95
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
96
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
97
+ amount: '1000',
98
+ });
99
+
100
+ expect(result.allowed).toBe(true);
101
+ });
102
+
103
+ it('should reject disallowed source chain', async () => {
104
+ const engine = new PolicyEngine();
105
+ const result = await engine.checkBridge({
106
+ walletId: 'test',
107
+ srcChainId: 'polygon',
108
+ dstChainId: 'arbitrum',
109
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
110
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
111
+ amount: '1000',
112
+ });
113
+
114
+ expect(result.allowed).toBe(false);
115
+ expect(result.reason).toContain('Chain not allowed');
116
+ });
117
+
118
+ it('should reject disallowed destination chain', async () => {
119
+ const engine = new PolicyEngine();
120
+ const result = await engine.checkBridge({
121
+ walletId: 'test',
122
+ srcChainId: 'ethereum',
123
+ dstChainId: 'polygon',
124
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
125
+ dstToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
126
+ amount: '1000',
127
+ });
128
+
129
+ expect(result.allowed).toBe(false);
130
+ expect(result.reason).toContain('Chain not allowed');
131
+ });
132
+
133
+ it('should reject disallowed token', async () => {
134
+ const engine = new PolicyEngine();
135
+ const result = await engine.checkBridge({
136
+ walletId: 'test',
137
+ srcChainId: 'ethereum',
138
+ dstChainId: 'arbitrum',
139
+ srcToken: '0xInvalidToken',
140
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
141
+ amount: '1000',
142
+ });
143
+
144
+ expect(result.allowed).toBe(false);
145
+ expect(result.reason).toContain('Token not allowed');
146
+ });
147
+
148
+ it('should reject amount exceeding limit', async () => {
149
+ const engine = new PolicyEngine();
150
+ const result = await engine.checkBridge({
151
+ walletId: 'test',
152
+ srcChainId: 'ethereum',
153
+ dstChainId: 'arbitrum',
154
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
155
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
156
+ amount: '15000',
157
+ });
158
+
159
+ expect(result.allowed).toBe(false);
160
+ expect(result.reason).toContain('exceeds maximum');
161
+ expect(result.details).toEqual({ maxAllowed: 10000, requested: 15000 });
162
+ });
163
+
164
+ it('should reject blocked recipient', async () => {
165
+ const engine = new PolicyEngine();
166
+ const result = await engine.checkBridge({
167
+ walletId: 'test',
168
+ srcChainId: 'ethereum',
169
+ dstChainId: 'arbitrum',
170
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
171
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
172
+ amount: '1000',
173
+ recipient: '0x0000000000000000000000000000000000000000',
174
+ });
175
+
176
+ expect(result.allowed).toBe(false);
177
+ expect(result.reason).toContain('Recipient is blocked');
178
+ });
179
+ });
180
+
181
+ describe('Swap Policy Checks', () => {
182
+ beforeEach(() => {
183
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
184
+ default: {
185
+ allowedChains: ['ethereum', 'arbitrum'],
186
+ maxSlippageBps: 100,
187
+ maxSwapInputToken: {
188
+ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 50000,
189
+ },
190
+ },
191
+ });
192
+ });
193
+
194
+ it('should allow valid swap', async () => {
195
+ const engine = new PolicyEngine();
196
+ const result = await engine.checkSwap({
197
+ walletId: 'test',
198
+ srcChainId: 'ethereum',
199
+ dstChainId: 'arbitrum',
200
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
201
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
202
+ inputAmount: '1000',
203
+ slippageBps: 50,
204
+ });
205
+
206
+ expect(result.allowed).toBe(true);
207
+ });
208
+
209
+ it('should reject excessive slippage', async () => {
210
+ const engine = new PolicyEngine();
211
+ const result = await engine.checkSwap({
212
+ walletId: 'test',
213
+ srcChainId: 'ethereum',
214
+ dstChainId: 'arbitrum',
215
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
216
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
217
+ inputAmount: '1000',
218
+ slippageBps: 150,
219
+ });
220
+
221
+ expect(result.allowed).toBe(false);
222
+ expect(result.reason).toContain('Slippage');
223
+ expect(result.details).toEqual({ maxAllowed: 100, requested: 150 });
224
+ });
225
+
226
+ it('should reject swap exceeding token limit', async () => {
227
+ const engine = new PolicyEngine();
228
+ const result = await engine.checkSwap({
229
+ walletId: 'test',
230
+ srcChainId: 'ethereum',
231
+ dstChainId: 'arbitrum',
232
+ srcToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
233
+ dstToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
234
+ inputAmount: '60000',
235
+ slippageBps: 50,
236
+ });
237
+
238
+ expect(result.allowed).toBe(false);
239
+ expect(result.reason).toContain('exceeds maximum');
240
+ });
241
+ });
242
+
243
+ describe('Money Market Policy Checks', () => {
244
+ beforeEach(() => {
245
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
246
+ default: {
247
+ allowedChains: ['ethereum', 'sonic'],
248
+ allowedTokensByChain: {
249
+ ethereum: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'],
250
+ sonic: ['0x29219dd400f2bf60e5a23d13be72b486d4038894'],
251
+ },
252
+ maxBorrowUsd: 50000,
253
+ maxBorrowToken: {
254
+ '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': 30000,
255
+ },
256
+ },
257
+ });
258
+ });
259
+
260
+ it('should allow valid supply', async () => {
261
+ const engine = new PolicyEngine();
262
+ const result = await engine.checkMoneyMarket({
263
+ walletId: 'test',
264
+ chainId: 'ethereum',
265
+ token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
266
+ amount: '10000',
267
+ operation: 'supply',
268
+ });
269
+
270
+ expect(result.allowed).toBe(true);
271
+ });
272
+
273
+ it('should allow valid borrow within limits', async () => {
274
+ const engine = new PolicyEngine();
275
+ const result = await engine.checkMoneyMarket({
276
+ walletId: 'test',
277
+ chainId: 'ethereum',
278
+ token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
279
+ amount: '20000',
280
+ amountUsd: 20000,
281
+ operation: 'borrow',
282
+ });
283
+
284
+ expect(result.allowed).toBe(true);
285
+ });
286
+
287
+ it('should reject borrow exceeding USD limit', async () => {
288
+ const engine = new PolicyEngine();
289
+ const result = await engine.checkMoneyMarket({
290
+ walletId: 'test',
291
+ chainId: 'ethereum',
292
+ token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Use allowed token
293
+ amount: '60000',
294
+ amountUsd: 60000,
295
+ operation: 'borrow',
296
+ });
297
+
298
+ expect(result.allowed).toBe(false);
299
+ expect(result.reason).toContain('exceeds maximum');
300
+ });
301
+
302
+ it('should reject borrow exceeding token limit', async () => {
303
+ const engine = new PolicyEngine();
304
+ const result = await engine.checkMoneyMarket({
305
+ walletId: 'test',
306
+ chainId: 'ethereum',
307
+ token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
308
+ amount: '35000',
309
+ amountUsd: 35000,
310
+ operation: 'borrow',
311
+ });
312
+
313
+ expect(result.allowed).toBe(false);
314
+ expect(result.reason).toContain('exceeds maximum');
315
+ });
316
+
317
+ it('should check cross-chain destination', async () => {
318
+ const engine = new PolicyEngine();
319
+ const result = await engine.checkMoneyMarket({
320
+ walletId: 'test',
321
+ chainId: 'ethereum',
322
+ dstChainId: 'polygon',
323
+ token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
324
+ amount: '10000',
325
+ operation: 'supply',
326
+ });
327
+
328
+ expect(result.allowed).toBe(false);
329
+ expect(result.reason).toContain('Chain not allowed');
330
+ });
331
+ });
332
+
333
+ describe('getAvailablePolicies', () => {
334
+ it('should return empty array when no config', () => {
335
+ const engine = new PolicyEngine();
336
+ expect(engine.getAvailablePolicies()).toEqual([]);
337
+ });
338
+
339
+ it('should return policy IDs', () => {
340
+ process.env.AMPED_OC_LIMITS_JSON = JSON.stringify({
341
+ default: {},
342
+ aggressive: {},
343
+ conservative: {},
344
+ });
345
+
346
+ const engine = new PolicyEngine();
347
+ const policies = engine.getAvailablePolicies();
348
+ expect(policies).toContain('default');
349
+ expect(policies).toContain('aggressive');
350
+ expect(policies).toContain('conservative');
351
+ expect(policies).toHaveLength(3);
352
+ });
353
+ });
354
+ });
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Position Aggregator Tests
3
+ */
4
+
5
+ import {
6
+ formatHealthFactor,
7
+ getHealthFactorStatus,
8
+ getPositionRecommendation,
9
+ TokenPosition,
10
+ CrossChainPositionView,
11
+ } from '../utils/positionAggregator';
12
+
13
+ // Mock dependencies
14
+ jest.mock('../sodax/client');
15
+ jest.mock('../providers/spokeProviderFactory');
16
+ jest.mock('../wallet/walletRegistry');
17
+
18
+ describe('Position Aggregator Utilities', () => {
19
+ describe('formatHealthFactor', () => {
20
+ it('should format finite health factors', () => {
21
+ expect(formatHealthFactor(1.5)).toBe('1.50');
22
+ expect(formatHealthFactor(2.345)).toBe('2.35');
23
+ expect(formatHealthFactor(0.95)).toBe('0.95');
24
+ });
25
+
26
+ it('should format infinity', () => {
27
+ expect(formatHealthFactor(Infinity)).toBe('∞');
28
+ });
29
+
30
+ it('should handle null', () => {
31
+ expect(formatHealthFactor(null)).toBe('N/A');
32
+ });
33
+ });
34
+
35
+ describe('getHealthFactorStatus', () => {
36
+ it('should return critical for HF < 1.1', () => {
37
+ expect(getHealthFactorStatus(1.0)).toEqual({ status: 'critical', color: 'red' });
38
+ expect(getHealthFactorStatus(1.05)).toEqual({ status: 'critical', color: 'red' });
39
+ });
40
+
41
+ it('should return danger for HF 1.1-1.5', () => {
42
+ expect(getHealthFactorStatus(1.2)).toEqual({ status: 'danger', color: 'orange' });
43
+ expect(getHealthFactorStatus(1.49)).toEqual({ status: 'danger', color: 'orange' });
44
+ });
45
+
46
+ it('should return caution for HF 1.5-2', () => {
47
+ expect(getHealthFactorStatus(1.6)).toEqual({ status: 'caution', color: 'yellow' });
48
+ expect(getHealthFactorStatus(1.9)).toEqual({ status: 'caution', color: 'yellow' });
49
+ });
50
+
51
+ it('should return healthy for HF >= 2', () => {
52
+ expect(getHealthFactorStatus(2.0)).toEqual({ status: 'healthy', color: 'green' });
53
+ expect(getHealthFactorStatus(5.0)).toEqual({ status: 'healthy', color: 'green' });
54
+ expect(getHealthFactorStatus(Infinity)).toEqual({ status: 'healthy', color: 'green' });
55
+ });
56
+
57
+ it('should return healthy for null', () => {
58
+ expect(getHealthFactorStatus(null)).toEqual({ status: 'healthy', color: 'green' });
59
+ });
60
+ });
61
+
62
+ describe('getPositionRecommendation', () => {
63
+ it('should warn about low health factor', () => {
64
+ const view = createMockView({
65
+ healthFactor: 1.3,
66
+ availableBorrowUsd: 0,
67
+ utilizationRate: 60,
68
+ netApy: 0.02,
69
+ });
70
+
71
+ const recommendations = getPositionRecommendation(view);
72
+ expect(recommendations.some(r => r.includes('Health factor is low'))).toBe(true);
73
+ });
74
+
75
+ it('should suggest available borrowing power', () => {
76
+ const view = createMockView({
77
+ healthFactor: 2.5,
78
+ availableBorrowUsd: 5000,
79
+ utilizationRate: 40,
80
+ netApy: 0.02,
81
+ });
82
+
83
+ const recommendations = getPositionRecommendation(view);
84
+ expect(recommendations.some(r => r.includes('borrowing power'))).toBe(true);
85
+ });
86
+
87
+ it('should warn about high utilization', () => {
88
+ const view = createMockView({
89
+ healthFactor: 1.8,
90
+ availableBorrowUsd: 100,
91
+ utilizationRate: 85,
92
+ netApy: 0.02,
93
+ });
94
+
95
+ const recommendations = getPositionRecommendation(view);
96
+ expect(recommendations.some(r => r.includes('High collateral utilization'))).toBe(true);
97
+ });
98
+
99
+ it('should warn about negative net APY', () => {
100
+ const view = createMockView({
101
+ healthFactor: 2.0,
102
+ availableBorrowUsd: 100,
103
+ utilizationRate: 50,
104
+ netApy: -0.01,
105
+ });
106
+
107
+ const recommendations = getPositionRecommendation(view);
108
+ expect(recommendations.some(r => r.includes('borrowing costs exceed'))).toBe(true);
109
+ });
110
+
111
+ it('should mention cross-chain positions', () => {
112
+ const view = createMockView({
113
+ healthFactor: 2.0,
114
+ availableBorrowUsd: 100,
115
+ utilizationRate: 50,
116
+ netApy: 0.02,
117
+ chainCount: 3,
118
+ });
119
+
120
+ const recommendations = getPositionRecommendation(view);
121
+ expect(recommendations.some(r => r.includes('chains'))).toBe(true);
122
+ });
123
+
124
+ it('should return empty array for healthy position', () => {
125
+ const view = createMockView({
126
+ healthFactor: 2.5,
127
+ availableBorrowUsd: 100,
128
+ utilizationRate: 50,
129
+ netApy: 0.05,
130
+ chainCount: 1,
131
+ });
132
+
133
+ const recommendations = getPositionRecommendation(view);
134
+ expect(recommendations.length).toBe(0);
135
+ });
136
+ });
137
+ });
138
+
139
+ // Helper function to create mock views
140
+ function createMockView(params: {
141
+ healthFactor: number;
142
+ availableBorrowUsd: number;
143
+ utilizationRate: number;
144
+ netApy: number;
145
+ chainCount?: number;
146
+ }): CrossChainPositionView {
147
+ const chainSummaries = Array(params.chainCount || 1).fill(null).map((_, i) => ({
148
+ chainId: `chain${i}`,
149
+ supplyUsd: 10000,
150
+ borrowUsd: 5000,
151
+ netWorthUsd: 5000,
152
+ healthFactor: params.healthFactor,
153
+ positionCount: 2,
154
+ }));
155
+
156
+ return {
157
+ walletId: 'test',
158
+ address: '0x123',
159
+ timestamp: new Date().toISOString(),
160
+ summary: {
161
+ totalSupplyUsd: chainSummaries.reduce((s, c) => s + c.supplyUsd, 0),
162
+ totalBorrowUsd: chainSummaries.reduce((s, c) => s + c.borrowUsd, 0),
163
+ netWorthUsd: chainSummaries.reduce((s, c) => s + c.netWorthUsd, 0),
164
+ availableBorrowUsd: params.availableBorrowUsd,
165
+ healthFactor: params.healthFactor,
166
+ liquidationRisk: params.healthFactor < 1.5 ? 'medium' : 'none',
167
+ weightedSupplyApy: 0.05,
168
+ weightedBorrowApy: 0.03,
169
+ netApy: params.netApy,
170
+ },
171
+ chainSummaries,
172
+ positions: [],
173
+ collateralUtilization: {
174
+ totalCollateralUsd: 10000,
175
+ usedCollateralUsd: params.utilizationRate * 100,
176
+ availableCollateralUsd: 10000 - params.utilizationRate * 100,
177
+ utilizationRate: params.utilizationRate,
178
+ },
179
+ riskMetrics: {
180
+ maxLtv: 0.8,
181
+ currentLtv: 0.5,
182
+ bufferUntilLiquidation: 30,
183
+ safeMaxBorrowUsd: 8000,
184
+ },
185
+ };
186
+ }
187
+
188
+ describe('Position Calculations', () => {
189
+ const mockPositions: TokenPosition[] = [
190
+ {
191
+ chainId: 'ethereum',
192
+ token: {
193
+ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
194
+ symbol: 'USDC',
195
+ name: 'USD Coin',
196
+ decimals: 6,
197
+ },
198
+ supply: {
199
+ balance: '10000',
200
+ balanceUsd: '10000',
201
+ balanceRaw: '10000000000',
202
+ apy: 0.05,
203
+ isCollateral: true,
204
+ },
205
+ borrow: {
206
+ balance: '0',
207
+ balanceUsd: '0',
208
+ balanceRaw: '0',
209
+ apy: 0,
210
+ },
211
+ loanToValue: 0.8,
212
+ liquidationThreshold: 0.85,
213
+ },
214
+ {
215
+ chainId: 'ethereum',
216
+ token: {
217
+ address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
218
+ symbol: 'WETH',
219
+ name: 'Wrapped Ether',
220
+ decimals: 18,
221
+ },
222
+ supply: {
223
+ balance: '0',
224
+ balanceUsd: '0',
225
+ balanceRaw: '0',
226
+ apy: 0,
227
+ isCollateral: false,
228
+ },
229
+ borrow: {
230
+ balance: '2',
231
+ balanceUsd: '5000',
232
+ balanceRaw: '2000000000000000000',
233
+ apy: 0.03,
234
+ },
235
+ loanToValue: 0.75,
236
+ liquidationThreshold: 0.8,
237
+ },
238
+ ];
239
+
240
+ it('should calculate total supply correctly', () => {
241
+ const totalSupply = mockPositions.reduce(
242
+ (sum, p) => sum + parseFloat(p.supply.balanceUsd || '0'),
243
+ 0
244
+ );
245
+ expect(totalSupply).toBe(10000);
246
+ });
247
+
248
+ it('should calculate total borrow correctly', () => {
249
+ const totalBorrow = mockPositions.reduce(
250
+ (sum, p) => sum + parseFloat(p.borrow.balanceUsd || '0'),
251
+ 0
252
+ );
253
+ expect(totalBorrow).toBe(5000);
254
+ });
255
+
256
+ it('should identify collateral assets', () => {
257
+ const collateralPositions = mockPositions.filter(p => p.supply.isCollateral);
258
+ expect(collateralPositions).toHaveLength(1);
259
+ expect(collateralPositions[0].token.symbol).toBe('USDC');
260
+ });
261
+
262
+ it('should calculate weighted APY correctly', () => {
263
+ const totalSupply = 10000;
264
+ const weightedSupplyApy = mockPositions.reduce(
265
+ (sum, p) => sum + parseFloat(p.supply.balanceUsd || '0') * p.supply.apy,
266
+ 0
267
+ ) / totalSupply;
268
+
269
+ expect(weightedSupplyApy).toBe(0.05);
270
+ });
271
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Jest Test Setup
3
+ */
4
+
5
+ // Mock console methods to reduce noise during tests
6
+ global.console = {
7
+ ...console,
8
+ log: jest.fn(),
9
+ debug: jest.fn(),
10
+ info: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ };
14
+
15
+ // Set default test environment
16
+ process.env.AMPED_OC_MODE = 'execute';
17
+ process.env.AMPED_OC_WALLETS_JSON = JSON.stringify({
18
+ test: {
19
+ address: '0x1234567890123456789012345678901234567890',
20
+ privateKey: '0xabc123def456',
21
+ },
22
+ });
23
+ process.env.AMPED_OC_RPC_URLS_JSON = JSON.stringify({
24
+ ethereum: 'https://eth-mainnet.example.com',
25
+ arbitrum: 'https://arb-mainnet.example.com',
26
+ sonic: 'https://rpc.sonic.example.com',
27
+ });
28
+
29
+ // Global test timeout
30
+ jest.setTimeout(10000);
31
+
32
+ // Clean up after each test
33
+ afterEach(() => {
34
+ jest.clearAllMocks();
35
+ });