wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.24__py3-none-any.whl

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.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
@@ -0,0 +1,460 @@
1
+ """Tests for BorosAdapter."""
2
+
3
+ from unittest.mock import AsyncMock, patch
4
+
5
+ import pytest
6
+
7
+ from wayfinder_paths.adapters.boros_adapter.adapter import (
8
+ BorosAdapter,
9
+ BorosLimitOrder,
10
+ BorosMarketQuote,
11
+ )
12
+
13
+
14
+ class TestBorosAdapter:
15
+ """Test cases for BorosAdapter."""
16
+
17
+ @pytest.fixture
18
+ def mock_boros_client(self):
19
+ """Mock BorosClient for testing."""
20
+ mock_client = AsyncMock()
21
+ return mock_client
22
+
23
+ @pytest.fixture
24
+ def adapter(self, mock_boros_client):
25
+ """Create a BorosAdapter instance with mocked client for testing."""
26
+ mock_config = {
27
+ "strategy_wallet": {
28
+ "address": "0x1234567890123456789012345678901234567890"
29
+ },
30
+ "boros_adapter": {},
31
+ }
32
+ with patch(
33
+ "wayfinder_paths.adapters.boros_adapter.adapter.BorosClient",
34
+ return_value=mock_boros_client,
35
+ ):
36
+ adapter = BorosAdapter(
37
+ config=mock_config,
38
+ simulation=True,
39
+ )
40
+ adapter.boros_client = mock_boros_client
41
+ return adapter
42
+
43
+ def test_adapter_type(self, adapter):
44
+ """Test adapter has correct type."""
45
+ assert adapter.adapter_type == "BOROS"
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_connect_success(self, adapter, mock_boros_client):
49
+ """Test successful connection."""
50
+ mock_boros_client.list_markets = AsyncMock(
51
+ return_value=[{"marketId": 1, "symbol": "HYPE-USD"}]
52
+ )
53
+
54
+ result = await adapter.connect()
55
+ assert result is True
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_connect_failure(self, adapter, mock_boros_client):
59
+ """Test connection failure."""
60
+ mock_boros_client.list_markets = AsyncMock(side_effect=Exception("API Error"))
61
+
62
+ result = await adapter.connect()
63
+ assert result is False
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_list_markets_success(self, adapter, mock_boros_client):
67
+ """Test successful market listing."""
68
+ mock_response = [
69
+ {"marketId": 1, "symbol": "HYPE-USD", "underlying": "HYPE"},
70
+ {"marketId": 2, "symbol": "BTC-USD", "underlying": "BTC"},
71
+ ]
72
+ mock_boros_client.list_markets = AsyncMock(return_value=mock_response)
73
+
74
+ success, markets = await adapter.list_markets()
75
+
76
+ assert success is True
77
+ assert len(markets) == 2
78
+ assert markets[0]["marketId"] == 1
79
+
80
+ @pytest.mark.asyncio
81
+ async def test_list_markets_failure(self, adapter, mock_boros_client):
82
+ """Test market listing failure."""
83
+ mock_boros_client.list_markets = AsyncMock(side_effect=Exception("API Error"))
84
+
85
+ success, data = await adapter.list_markets()
86
+
87
+ assert success is False
88
+ assert "API Error" in str(data)
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_get_market_success(self, adapter, mock_boros_client):
92
+ """Test successful single market fetch."""
93
+ mock_response = {
94
+ "marketId": 18,
95
+ "symbol": "HYPERLIQUID-HYPE-USD",
96
+ "underlying": "HYPE",
97
+ }
98
+ mock_boros_client.get_market = AsyncMock(return_value=mock_response)
99
+
100
+ success, market = await adapter.get_market(18)
101
+
102
+ assert success is True
103
+ assert market["marketId"] == 18
104
+
105
+ @pytest.mark.asyncio
106
+ async def test_get_orderbook_success(self, adapter, mock_boros_client):
107
+ """Test successful orderbook fetch."""
108
+ mock_response = {
109
+ "long": {"ia": [100, 105, 110], "sz": [1000, 2000, 3000]},
110
+ "short": {"ia": [115, 120, 125], "sz": [1500, 2500, 3500]},
111
+ }
112
+ mock_boros_client.get_order_book = AsyncMock(return_value=mock_response)
113
+
114
+ success, book = await adapter.get_orderbook(18)
115
+
116
+ assert success is True
117
+ assert "long" in book
118
+ assert "short" in book
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_quote_market_success(self, adapter, mock_boros_client):
122
+ """Test successful market quote."""
123
+ mock_market = {
124
+ "marketId": 18,
125
+ "address": "0xabcd",
126
+ "symbol": "HYPERLIQUID-HYPE-USD",
127
+ "underlying": "HYPE",
128
+ "imData": {"tickStep": 1, "maturity": 1735689600},
129
+ "tokenId": 3,
130
+ }
131
+ mock_orderbook = {
132
+ "long": {"ia": [100, 105]},
133
+ "short": {"ia": [115, 120]},
134
+ }
135
+ mock_boros_client.get_order_book = AsyncMock(return_value=mock_orderbook)
136
+
137
+ success, quote = await adapter.quote_market(mock_market)
138
+
139
+ assert success is True
140
+ assert isinstance(quote, BorosMarketQuote)
141
+ assert quote.market_id == 18
142
+ assert quote.best_bid_apr == 0.105 # max(long) * tick_size
143
+ assert quote.best_ask_apr == 0.115 # min(short) * tick_size
144
+
145
+ @pytest.mark.asyncio
146
+ async def test_quote_markets_for_underlying_success(
147
+ self, adapter, mock_boros_client
148
+ ):
149
+ """Test quoting markets for underlying."""
150
+ mock_markets = [
151
+ {
152
+ "marketId": 18,
153
+ "symbol": "HYPERLIQUID-HYPE-USD-30D",
154
+ "underlying": "HYPE",
155
+ "imData": {"maturity": 1735689600},
156
+ },
157
+ {
158
+ "marketId": 19,
159
+ "symbol": "HYPERLIQUID-HYPE-USD-60D",
160
+ "underlying": "HYPE",
161
+ "imData": {"maturity": 1738368000},
162
+ },
163
+ {
164
+ "marketId": 20,
165
+ "symbol": "BTC-USD-30D",
166
+ "underlying": "BTC",
167
+ "imData": {"maturity": 1735689600},
168
+ },
169
+ ]
170
+ mock_boros_client.list_markets = AsyncMock(return_value=mock_markets)
171
+ mock_boros_client.get_order_book = AsyncMock(
172
+ return_value={"long": {"ia": [100]}, "short": {"ia": [110]}}
173
+ )
174
+
175
+ success, quotes = await adapter.quote_markets_for_underlying("HYPE")
176
+
177
+ assert success is True
178
+ assert len(quotes) == 2 # Only HYPE markets
179
+ assert all(q.underlying == "HYPE" for q in quotes)
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_get_collaterals_success(self, adapter, mock_boros_client):
183
+ """Test collateral fetch."""
184
+ mock_response = {
185
+ "collaterals": [
186
+ {
187
+ "tokenId": 3,
188
+ "crossPosition": {"netBalance": "100000000000000000000"},
189
+ "isolatedPositions": [],
190
+ }
191
+ ]
192
+ }
193
+ mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
194
+
195
+ success, data = await adapter.get_collaterals()
196
+
197
+ assert success is True
198
+ assert "collaterals" in data
199
+
200
+ @pytest.mark.asyncio
201
+ async def test_get_account_balances_success(self, adapter, mock_boros_client):
202
+ """Test account balance fetch."""
203
+ mock_response = {
204
+ "collaterals": [
205
+ {
206
+ "tokenId": 3,
207
+ "crossPosition": {"availableBalance": "100000000000000000000"},
208
+ "isolatedPositions": [{"availableBalance": "50000000000000000000"}],
209
+ }
210
+ ]
211
+ }
212
+ mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
213
+
214
+ success, balances = await adapter.get_account_balances(token_id=3)
215
+
216
+ assert success is True
217
+ assert balances["cross"] == 100.0
218
+ assert balances["isolated"] == 50.0
219
+ assert balances["total"] == 150.0
220
+
221
+ @pytest.mark.asyncio
222
+ async def test_get_active_positions_success(self, adapter, mock_boros_client):
223
+ """Test active positions fetch."""
224
+ mock_response = {
225
+ "collaterals": [
226
+ {
227
+ "tokenId": 3,
228
+ "crossPosition": {
229
+ "marketPositions": [
230
+ {
231
+ "marketId": 18,
232
+ "side": 1,
233
+ "sizeWei": "1000000000000000000",
234
+ }
235
+ ]
236
+ },
237
+ "isolatedPositions": [],
238
+ }
239
+ ]
240
+ }
241
+ mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
242
+
243
+ success, positions = await adapter.get_active_positions(market_id=18)
244
+
245
+ assert success is True
246
+ assert len(positions) == 1
247
+ assert positions[0]["marketId"] == 18
248
+ assert positions[0]["size"] == 1.0
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_get_open_limit_orders_success(self, adapter, mock_boros_client):
252
+ """Test open orders fetch."""
253
+ mock_response = [
254
+ {
255
+ "orderId": "order-123",
256
+ "marketId": 18,
257
+ "side": 1,
258
+ "size": "1000000000000000000",
259
+ "filledSize": "500000000000000000",
260
+ "limitTick": 100,
261
+ "tickStep": 1,
262
+ "status": "open",
263
+ }
264
+ ]
265
+ mock_boros_client.get_open_orders = AsyncMock(return_value=mock_response)
266
+
267
+ success, orders = await adapter.get_open_limit_orders()
268
+
269
+ assert success is True
270
+ assert len(orders) == 1
271
+ assert isinstance(orders[0], BorosLimitOrder)
272
+ assert orders[0].order_id == "order-123"
273
+ assert orders[0].size == 1.0
274
+ assert orders[0].filled_size == 0.5
275
+ assert orders[0].remaining_size == 0.5
276
+
277
+ @pytest.mark.asyncio
278
+ async def test_get_full_user_state_success(self, adapter, mock_boros_client):
279
+ market_acc = "0x" + ("0" * 58) + "000012" # 0x12 == 18
280
+ mock_boros_client.get_collaterals = AsyncMock(
281
+ return_value={
282
+ "collaterals": [
283
+ {
284
+ "tokenId": 3,
285
+ "crossPosition": {
286
+ "availableBalance": "1000000000000000000",
287
+ "marketPositions": [
288
+ {
289
+ "marketId": 18,
290
+ "side": 1,
291
+ "notionalSize": "1000000000000000000",
292
+ "pnl": {
293
+ "unrealisedPnl": "0",
294
+ "rateSettlementPnl": "0",
295
+ },
296
+ }
297
+ ],
298
+ },
299
+ "isolatedPositions": [
300
+ {
301
+ "availableBalance": "2000000000000000000",
302
+ "marketAcc": market_acc,
303
+ "marketPositions": [
304
+ {
305
+ "marketId": 18,
306
+ "side": 0,
307
+ "notionalSize": "2000000000000000000",
308
+ "pnl": {
309
+ "unrealisedPnl": "0",
310
+ "rateSettlementPnl": "0",
311
+ },
312
+ }
313
+ ],
314
+ }
315
+ ],
316
+ "withdrawal": {
317
+ "lastWithdrawalRequestTime": 0,
318
+ "lastWithdrawalAmount": 0,
319
+ },
320
+ }
321
+ ]
322
+ }
323
+ )
324
+ mock_boros_client.get_open_orders = AsyncMock(
325
+ return_value=[
326
+ {
327
+ "orderId": "order-1",
328
+ "marketId": 18,
329
+ "side": 1,
330
+ "size": "1000000000000000000",
331
+ "filledSize": "0",
332
+ "limitTick": 100,
333
+ "tickStep": 1,
334
+ "status": "open",
335
+ }
336
+ ]
337
+ )
338
+
339
+ ok, state = await adapter.get_full_user_state(include_withdrawal_status=False)
340
+ assert ok is True
341
+ assert state["protocol"] == "boros"
342
+ assert state["chainId"] == adapter.chain_id
343
+ assert "collaterals" in state
344
+ assert state["balances"]["total"] == 3.0
345
+ assert len(state["positions"]) == 2
346
+ assert len(state["openOrders"]) == 1
347
+
348
+ @pytest.mark.asyncio
349
+ async def test_deposit_to_cross_margin_simulation(self, adapter):
350
+ """Test deposit in simulation mode."""
351
+ success, result = await adapter.deposit_to_cross_margin(
352
+ collateral_address="0xabcd",
353
+ amount_wei=1000000, # 1 USDT
354
+ token_id=3,
355
+ market_id=18,
356
+ )
357
+
358
+ assert success is True
359
+ assert result["status"] == "simulated"
360
+
361
+ @pytest.mark.asyncio
362
+ async def test_withdraw_collateral_simulation(self, adapter):
363
+ """Test withdraw in simulation mode."""
364
+ success, result = await adapter.withdraw_collateral(
365
+ token_id=3,
366
+ amount_wei=1000000000000000000,
367
+ )
368
+
369
+ assert success is True
370
+ assert result["status"] == "simulated"
371
+
372
+ @pytest.mark.asyncio
373
+ async def test_place_rate_order_simulation(self, adapter):
374
+ """Test place order in simulation mode."""
375
+ success, result = await adapter.place_rate_order(
376
+ market_id=18,
377
+ token_id=3,
378
+ size_yu_wei=1000000000000000000,
379
+ side="short",
380
+ limit_tick=100,
381
+ )
382
+
383
+ assert success is True
384
+ assert result["status"] == "simulated"
385
+
386
+ @pytest.mark.asyncio
387
+ async def test_close_positions_market_simulation(self, adapter, mock_boros_client):
388
+ """Test close position in simulation mode."""
389
+ mock_response = {
390
+ "collaterals": [
391
+ {
392
+ "tokenId": 3,
393
+ "crossPosition": {
394
+ "marketPositions": [
395
+ {
396
+ "marketId": 18,
397
+ "side": 1,
398
+ "sizeWei": "1000000000000000000",
399
+ }
400
+ ]
401
+ },
402
+ "isolatedPositions": [],
403
+ }
404
+ ]
405
+ }
406
+ mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
407
+
408
+ success, result = await adapter.close_positions_market(market_id=18)
409
+
410
+ assert success is True
411
+ assert result["status"] == "simulated"
412
+
413
+ @pytest.mark.asyncio
414
+ async def test_cancel_orders_simulation(self, adapter):
415
+ """Test cancel orders in simulation mode."""
416
+ success, result = await adapter.cancel_orders(
417
+ market_id=18,
418
+ cancel_all=True,
419
+ )
420
+
421
+ assert success is True
422
+ assert result["status"] == "simulated"
423
+
424
+ def test_tick_from_rate(self):
425
+ """Test APR to tick conversion."""
426
+ # 10% APR with tick_step=1
427
+ tick = BorosAdapter.tick_from_rate(0.10, tick_step=1, round_down=False)
428
+ assert tick > 0
429
+
430
+ # Verify roundtrip
431
+ rate_back = BorosAdapter.rate_from_tick(tick, tick_step=1)
432
+ assert abs(rate_back - 0.10) < 0.001
433
+
434
+ def test_rate_from_tick(self):
435
+ """Test tick to APR conversion."""
436
+ rate = BorosAdapter.rate_from_tick(954, tick_step=1)
437
+ assert rate > 0
438
+ assert rate < 1 # Should be a decimal
439
+
440
+ # Negative tick
441
+ rate_neg = BorosAdapter.rate_from_tick(-954, tick_step=1)
442
+ assert rate_neg < 0
443
+
444
+ def test_normalize_apr(self):
445
+ """Test APR normalization."""
446
+ # Already decimal
447
+ assert BorosAdapter.normalize_apr(0.10) == 0.10
448
+
449
+ # Percent (values between 1 and 1000)
450
+ assert BorosAdapter.normalize_apr(10.0) == 0.10
451
+
452
+ # BPS (values > 1000)
453
+ assert BorosAdapter.normalize_apr(1100) == 0.11 # 1100 bps = 11%
454
+
455
+ # 1e18 scaled
456
+ result = BorosAdapter.normalize_apr(100000000000000000)
457
+ assert abs(result - 0.10) < 0.001
458
+
459
+ # None
460
+ assert BorosAdapter.normalize_apr(None) is None
@@ -0,0 +1,156 @@
1
+ """Golden tests for BorosAdapter parsing/quoting behavior.
2
+
3
+ These are meant to be stable regression tests during refactors (types/utils split).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from unittest.mock import AsyncMock, patch
9
+
10
+ import pytest
11
+
12
+ from wayfinder_paths.adapters.boros_adapter.adapter import BorosAdapter
13
+
14
+
15
+ @pytest.fixture
16
+ def mock_boros_client():
17
+ return AsyncMock()
18
+
19
+
20
+ @pytest.fixture
21
+ def adapter(mock_boros_client):
22
+ mock_config = {
23
+ "strategy_wallet": {"address": "0x1234567890123456789012345678901234567890"},
24
+ "boros_adapter": {},
25
+ }
26
+ with patch(
27
+ "wayfinder_paths.adapters.boros_adapter.adapter.BorosClient",
28
+ return_value=mock_boros_client,
29
+ ):
30
+ a = BorosAdapter(config=mock_config, simulation=True)
31
+ a.boros_client = mock_boros_client
32
+ return a
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_quote_markets_for_underlying_golden(adapter, mock_boros_client):
37
+ # Make tenor_days deterministic (quote_market uses current wall-clock time).
38
+ adapter._time_to_maturity_days = lambda maturity_ts: (maturity_ts or 0) / 1000.0 # type: ignore[method-assign]
39
+
40
+ mock_markets = [
41
+ {
42
+ "marketId": 2,
43
+ "address": "0x2222",
44
+ "tokenId": 3,
45
+ "imData": {
46
+ "symbol": "HYPERLIQUID-HYPE-USD",
47
+ "underlying": "HYPE",
48
+ "maturity": 2000,
49
+ "tickStep": 7,
50
+ "collateral": "0xUSDT",
51
+ },
52
+ },
53
+ {
54
+ "marketId": 1,
55
+ "address": "0x1111",
56
+ "tokenId": 3,
57
+ "imData": {
58
+ "symbol": "HYPERLIQUID-HYPE-USD",
59
+ "underlying": "HYPE",
60
+ "maturity": 1000,
61
+ "tickStep": 5,
62
+ "collateral": "0xUSDT",
63
+ },
64
+ },
65
+ # Same underlying but wrong platform should be filtered out when platform is set.
66
+ {
67
+ "marketId": 3,
68
+ "address": "0x3333",
69
+ "tokenId": 3,
70
+ "imData": {
71
+ "symbol": "OTHER-HYPE-USD",
72
+ "underlying": "HYPE",
73
+ "maturity": 1500,
74
+ "tickStep": 1,
75
+ "collateral": "0xUSDT",
76
+ },
77
+ "metadata": {"platformName": "OTHER"},
78
+ "platform": {"name": "OTHER"},
79
+ },
80
+ ]
81
+
82
+ async def _get_order_book(market_id: int, tick_size: float = 0.001):
83
+ if market_id == 1:
84
+ return {"long": {"ia": [100, 110]}, "short": {"ia": [120, 130]}}
85
+ if market_id == 2:
86
+ return {"long": {"ia": [200]}, "short": {"ia": [250]}}
87
+ return {"long": {"ia": [1]}, "short": {"ia": [2]}}
88
+
89
+ mock_boros_client.list_markets = AsyncMock(return_value=mock_markets)
90
+ mock_boros_client.get_order_book = AsyncMock(side_effect=_get_order_book)
91
+
92
+ ok, quotes = await adapter.quote_markets_for_underlying(
93
+ "HYPE", platform="hyperliquid", tick_size=0.001
94
+ )
95
+ assert ok is True
96
+
97
+ # Only the two Hyperliquid-tagged markets should remain.
98
+ assert [q.market_id for q in quotes] == [1, 2]
99
+
100
+ q1 = quotes[0]
101
+ assert q1.underlying == "HYPE"
102
+ assert q1.symbol == "HYPERLIQUID-HYPE-USD"
103
+ assert q1.maturity_ts == 1000
104
+ assert q1.tenor_days == 1.0
105
+ assert q1.tick_step == 5
106
+ assert q1.best_bid_apr == 0.11
107
+ assert q1.best_ask_apr == 0.12
108
+ assert q1.mid_apr == pytest.approx(0.115)
109
+
110
+ q2 = quotes[1]
111
+ assert q2.maturity_ts == 2000
112
+ assert q2.tenor_days == 2.0
113
+ assert q2.tick_step == 7
114
+ assert q2.best_bid_apr == 0.2
115
+ assert q2.best_ask_apr == 0.25
116
+ assert q2.mid_apr == pytest.approx(0.225)
117
+
118
+
119
+ @pytest.mark.asyncio
120
+ async def test_get_account_balances_isolated_market_id_golden(
121
+ adapter, mock_boros_client
122
+ ):
123
+ # marketAcc parsing: last 6 hex chars represent the market id (3 bytes).
124
+ market_acc = "0x" + ("0" * 58) + "000012" # 0x12 == 18
125
+
126
+ mock_boros_client.get_collaterals = AsyncMock(
127
+ return_value={
128
+ "collaterals": [
129
+ {
130
+ "tokenId": 3,
131
+ "crossPosition": {"availableBalance": "100000000000000000000"},
132
+ "isolatedPositions": [
133
+ {
134
+ "availableBalance": "50000000000000000000",
135
+ "marketAcc": market_acc,
136
+ }
137
+ ],
138
+ }
139
+ ]
140
+ }
141
+ )
142
+
143
+ ok, balances = await adapter.get_account_balances(token_id=3)
144
+ assert ok is True
145
+ assert balances["cross"] == 100.0
146
+ assert balances["isolated"] == 50.0
147
+ assert balances["total"] == 150.0
148
+ assert balances["isolated_market_id"] == 18
149
+ assert balances["isolated_positions"] == [
150
+ {
151
+ "market_id": 18,
152
+ "balance": 50.0,
153
+ "balance_wei": 50000000000000000000,
154
+ "marketAcc": market_acc,
155
+ }
156
+ ]
@@ -0,0 +1,70 @@
1
+ """Types for BorosAdapter (dataclasses)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class BorosMarketQuote:
12
+ """Quote data for a Boros market."""
13
+
14
+ market_id: int
15
+ market_address: str
16
+ symbol: str
17
+ underlying: str
18
+ tenor_days: float
19
+ maturity_ts: int
20
+ collateral_address: str
21
+ collateral_token_id: int | None
22
+ tick_step: int | None
23
+ mid_apr: float | None
24
+ best_bid_apr: float | None
25
+ best_ask_apr: float | None
26
+
27
+
28
+ @dataclass
29
+ class BorosTenorQuote:
30
+ """Tenor curve data for a Boros market."""
31
+
32
+ market_id: int
33
+ address: str
34
+ symbol: str
35
+ underlying_symbol: str
36
+ maturity: int
37
+ tenor_days: float
38
+ mid_apr: float | None
39
+ mark_apr: float | None
40
+ floating_apr: float | None
41
+ long_yield_apr: float | None
42
+ volume_24h: float | None
43
+ notional_oi: float | None
44
+
45
+
46
+ @dataclass
47
+ class BorosLimitOrder:
48
+ """Represents an open limit order on Boros."""
49
+
50
+ order_id: str
51
+ market_id: int
52
+ side: str # "long" or "short"
53
+ size: float # Size in YU
54
+ limit_tick: int # APR in bps
55
+ limit_apr: float # APR as decimal (e.g., 0.05 = 5%)
56
+ filled_size: float
57
+ remaining_size: float
58
+ status: str # "open", "partially_filled", etc.
59
+ created_at: datetime | None = None
60
+ raw: dict[str, Any] | None = field(default=None, repr=False)
61
+
62
+
63
+ @dataclass
64
+ class MarginHealth:
65
+ """Margin health metrics for a Boros account."""
66
+
67
+ margin_ratio: float
68
+ maint_margin: float
69
+ net_balance: float
70
+ positions: list[dict[str, Any]]