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,448 @@
1
+ import asyncio
2
+ import json
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from wayfinder_paths.core.adapters.models import LEND, SWAP, UNLEND
9
+ from wayfinder_paths.core.clients.LedgerClient import LedgerClient
10
+
11
+
12
+ @pytest.fixture
13
+ def temp_ledger_dir():
14
+ with tempfile.TemporaryDirectory() as tmpdir:
15
+ yield Path(tmpdir)
16
+
17
+
18
+ @pytest.fixture
19
+ def ledger_client(temp_ledger_dir):
20
+ return LedgerClient(ledger_dir=temp_ledger_dir)
21
+
22
+
23
+ @pytest.fixture
24
+ def test_wallet_address():
25
+ return "0x1234567890abcdef1234567890abcdef12345678"
26
+
27
+
28
+ class TestLedgerClientInitialization:
29
+ def test_creates_ledger_directory(self, temp_ledger_dir):
30
+ assert not (temp_ledger_dir / "transactions.json").exists()
31
+
32
+ LedgerClient(ledger_dir=temp_ledger_dir)
33
+
34
+ assert (temp_ledger_dir / "transactions.json").exists()
35
+ assert (temp_ledger_dir / "snapshots.json").exists()
36
+
37
+ def test_initializes_empty_json_files(self, ledger_client, temp_ledger_dir):
38
+ transactions_data = json.loads(
39
+ (temp_ledger_dir / "transactions.json").read_text()
40
+ )
41
+ snapshots_data = json.loads((temp_ledger_dir / "snapshots.json").read_text())
42
+
43
+ assert transactions_data == {"transactions": []}
44
+ assert snapshots_data == {"snapshots": []}
45
+
46
+
47
+ class TestDepositOperations:
48
+ @pytest.mark.asyncio
49
+ async def test_add_deposit(self, ledger_client, test_wallet_address):
50
+ result = await ledger_client.add_strategy_deposit(
51
+ wallet_address=test_wallet_address,
52
+ chain_id=1,
53
+ token_address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
54
+ token_amount="1000.0",
55
+ usd_value="1000.0",
56
+ strategy_name="Test Strategy",
57
+ data={"note": "Test deposit"},
58
+ )
59
+
60
+ assert result["status"] == "success"
61
+ assert "transaction_id" in result
62
+ assert "timestamp" in result
63
+
64
+ @pytest.mark.asyncio
65
+ async def test_deposit_creates_transaction_record(
66
+ self, ledger_client, test_wallet_address, temp_ledger_dir
67
+ ):
68
+ await ledger_client.add_strategy_deposit(
69
+ wallet_address=test_wallet_address,
70
+ chain_id=1,
71
+ token_address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
72
+ token_amount="1000.0",
73
+ usd_value="1000.0",
74
+ strategy_name="Test Strategy",
75
+ )
76
+
77
+ data = json.loads((temp_ledger_dir / "transactions.json").read_text())
78
+ transactions = data["transactions"]
79
+
80
+ assert len(transactions) == 1
81
+ assert transactions[0]["operation"] == "DEPOSIT"
82
+ assert transactions[0]["wallet_address"] == test_wallet_address
83
+ assert transactions[0]["usd_value"] == "1000.0"
84
+ assert transactions[0]["strategy_name"] == "Test Strategy"
85
+
86
+
87
+ class TestWithdrawalOperations:
88
+ @pytest.mark.asyncio
89
+ async def test_add_withdrawal(self, ledger_client, test_wallet_address):
90
+ result = await ledger_client.add_strategy_withdraw(
91
+ wallet_address=test_wallet_address,
92
+ chain_id=1,
93
+ token_address="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
94
+ token_amount="500.0",
95
+ usd_value="500.0",
96
+ strategy_name="Test Strategy",
97
+ )
98
+
99
+ assert result["status"] == "success"
100
+ assert "transaction_id" in result
101
+
102
+
103
+ class TestOperationRecording:
104
+ @pytest.mark.asyncio
105
+ async def test_add_swap_operation(self, ledger_client, test_wallet_address):
106
+ swap_op = SWAP(
107
+ adapter="TestAdapter",
108
+ from_token_id="usd-coin-base",
109
+ to_token_id="aerodrome-usdc-base",
110
+ from_amount="1000000000",
111
+ to_amount="1000000000",
112
+ from_amount_usd=1000.0,
113
+ to_amount_usd=1000.0,
114
+ transaction_hash="0xabc123",
115
+ transaction_chain_id=8453,
116
+ )
117
+
118
+ result = await ledger_client.add_strategy_operation(
119
+ wallet_address=test_wallet_address,
120
+ operation_data=swap_op.model_dump(mode="json"),
121
+ usd_value="1000.0",
122
+ strategy_name="Test Strategy",
123
+ )
124
+
125
+ assert result["status"] == "success"
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_add_lend_operation(self, ledger_client, test_wallet_address):
129
+ lend_op = LEND(
130
+ adapter="TestAdapter",
131
+ token_address="0xTokenAddress",
132
+ pool_address="0xPoolContract",
133
+ amount="1000000000",
134
+ amount_usd=1000.0,
135
+ transaction_hash="0xdef456",
136
+ transaction_chain_id=8453,
137
+ )
138
+
139
+ result = await ledger_client.add_strategy_operation(
140
+ wallet_address=test_wallet_address,
141
+ operation_data=lend_op.model_dump(mode="json"),
142
+ usd_value="1000.0",
143
+ )
144
+
145
+ assert result["status"] == "success"
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_add_unlend_operation(self, ledger_client, test_wallet_address):
149
+ unlend_op = UNLEND(
150
+ adapter="TestAdapter",
151
+ token_address="0xTokenAddress",
152
+ pool_address="0xPoolContract",
153
+ amount="1000000000",
154
+ amount_usd=1000.0,
155
+ transaction_hash="0xghi789",
156
+ transaction_chain_id=8453,
157
+ )
158
+
159
+ result = await ledger_client.add_strategy_operation(
160
+ wallet_address=test_wallet_address,
161
+ operation_data=unlend_op.model_dump(mode="json"),
162
+ usd_value="1000.0",
163
+ )
164
+
165
+ assert result["status"] == "success"
166
+
167
+ @pytest.mark.asyncio
168
+ async def test_operation_stores_op_data(
169
+ self, ledger_client, test_wallet_address, temp_ledger_dir
170
+ ):
171
+ swap_op = SWAP(
172
+ adapter="TestAdapter",
173
+ from_token_id="token-a",
174
+ to_token_id="token-b",
175
+ from_amount="100",
176
+ to_amount="95",
177
+ from_amount_usd=100.0,
178
+ to_amount_usd=95.0,
179
+ transaction_hash="0xjkl012",
180
+ transaction_chain_id=8453,
181
+ )
182
+
183
+ await ledger_client.add_strategy_operation(
184
+ wallet_address=test_wallet_address,
185
+ operation_data=swap_op.model_dump(mode="json"),
186
+ usd_value="100.0",
187
+ )
188
+
189
+ data = json.loads((temp_ledger_dir / "transactions.json").read_text())
190
+ transaction = data["transactions"][0]
191
+
192
+ assert transaction["operation"] == "STRAT_OP"
193
+ assert "data" in transaction
194
+ assert "op_data" in transaction["data"]
195
+ assert transaction["data"]["op_data"]["type"] == "SWAP"
196
+ assert transaction["usd_value"] == "100.0"
197
+ assert "id" in transaction
198
+ assert "timestamp" in transaction
199
+ assert "wallet_address" in transaction
200
+ # Stored minimal; amount/token_address derived when formatting
201
+ assert transaction["amount"] == "0"
202
+ assert transaction["token_address"] == ""
203
+
204
+ # Formatted output derives amount/token_address from op_data
205
+ list_result = await ledger_client.get_strategy_transactions(
206
+ wallet_address=test_wallet_address
207
+ )
208
+ txn = list_result["transactions"][0]
209
+ assert txn["amount"] == "95"
210
+ assert txn["token_address"] == "token-b"
211
+
212
+
213
+ class TestTransactionRetrieval:
214
+ @pytest.mark.asyncio
215
+ async def test_get_empty_transactions(self, ledger_client, test_wallet_address):
216
+ result = await ledger_client.get_strategy_transactions(
217
+ wallet_address=test_wallet_address
218
+ )
219
+
220
+ assert result["transactions"] == []
221
+ assert result["total"] == 0
222
+
223
+ @pytest.mark.asyncio
224
+ async def test_get_transactions_filters_by_wallet(
225
+ self, ledger_client, test_wallet_address
226
+ ):
227
+ await ledger_client.add_strategy_deposit(
228
+ wallet_address=test_wallet_address,
229
+ chain_id=1,
230
+ token_address="0xTest",
231
+ token_amount="100",
232
+ usd_value="100",
233
+ )
234
+
235
+ await ledger_client.add_strategy_deposit(
236
+ wallet_address="0xDifferentWallet",
237
+ chain_id=1,
238
+ token_address="0xTest",
239
+ token_amount="200",
240
+ usd_value="200",
241
+ )
242
+
243
+ result = await ledger_client.get_strategy_transactions(
244
+ wallet_address=test_wallet_address
245
+ )
246
+
247
+ assert result["total"] == 1
248
+ # Return shape is StrategyTransaction (no wallet_address in list items)
249
+ tx = result["transactions"][0]
250
+ assert tx["operation"] == "DEPOSIT"
251
+ assert tx["amount"] == "100"
252
+ assert tx["token_address"] == "0xTest"
253
+ assert tx["usd_value"] == "100"
254
+
255
+ @pytest.mark.asyncio
256
+ async def test_get_transactions_pagination(
257
+ self, ledger_client, test_wallet_address
258
+ ):
259
+ for i in range(5):
260
+ await ledger_client.add_strategy_deposit(
261
+ wallet_address=test_wallet_address,
262
+ chain_id=1,
263
+ token_address="0xTest",
264
+ token_amount=str(100 * (i + 1)),
265
+ usd_value=str(100 * (i + 1)),
266
+ )
267
+
268
+ result = await ledger_client.get_strategy_transactions(
269
+ wallet_address=test_wallet_address, limit=2, offset=0
270
+ )
271
+
272
+ assert result["total"] == 5
273
+ assert len(result["transactions"]) == 2
274
+ assert result["limit"] == 2
275
+ assert result["offset"] == 0
276
+
277
+ result = await ledger_client.get_strategy_transactions(
278
+ wallet_address=test_wallet_address, limit=2, offset=2
279
+ )
280
+
281
+ assert len(result["transactions"]) == 2
282
+ assert result["offset"] == 2
283
+
284
+ @pytest.mark.asyncio
285
+ async def test_get_latest_transactions(self, ledger_client, test_wallet_address):
286
+ # get_strategy_latest_transactions returns only STRAT_OP (limit 80), matching vault
287
+ for i in range(3):
288
+ await ledger_client.add_strategy_operation(
289
+ wallet_address=test_wallet_address,
290
+ operation_data={"type": "SWAP", "to_token_id": f"token-{i}"},
291
+ usd_value=str(i),
292
+ )
293
+
294
+ result = await ledger_client.get_strategy_latest_transactions(
295
+ wallet_address=test_wallet_address
296
+ )
297
+
298
+ assert len(result["transactions"]) == 3
299
+ assert result["limit"] == 80
300
+ assert result["offset"] == 0
301
+ assert result["total"] == 3
302
+ # Should be sorted by timestamp descending (most recent first)
303
+
304
+
305
+ class TestNetDepositCalculation:
306
+ @pytest.mark.asyncio
307
+ async def test_net_deposit_empty(self, ledger_client, test_wallet_address):
308
+ result = await ledger_client.get_strategy_net_deposit(
309
+ wallet_address=test_wallet_address
310
+ )
311
+
312
+ assert result == 0.0
313
+
314
+ @pytest.mark.asyncio
315
+ async def test_net_deposit_only_deposits(self, ledger_client, test_wallet_address):
316
+ await ledger_client.add_strategy_deposit(
317
+ wallet_address=test_wallet_address,
318
+ chain_id=1,
319
+ token_address="0xTest",
320
+ token_amount="1000",
321
+ usd_value="1000",
322
+ )
323
+
324
+ await ledger_client.add_strategy_deposit(
325
+ wallet_address=test_wallet_address,
326
+ chain_id=1,
327
+ token_address="0xTest",
328
+ token_amount="500",
329
+ usd_value="500",
330
+ )
331
+
332
+ result = await ledger_client.get_strategy_net_deposit(
333
+ wallet_address=test_wallet_address
334
+ )
335
+
336
+ assert result == 1500.0
337
+
338
+ @pytest.mark.asyncio
339
+ async def test_net_deposit_with_withdrawals(
340
+ self, ledger_client, test_wallet_address
341
+ ):
342
+ await ledger_client.add_strategy_deposit(
343
+ wallet_address=test_wallet_address,
344
+ chain_id=1,
345
+ token_address="0xTest",
346
+ token_amount="1000",
347
+ usd_value="1000",
348
+ )
349
+
350
+ await ledger_client.add_strategy_withdraw(
351
+ wallet_address=test_wallet_address,
352
+ chain_id=1,
353
+ token_address="0xTest",
354
+ token_amount="300",
355
+ usd_value="300",
356
+ )
357
+
358
+ result = await ledger_client.get_strategy_net_deposit(
359
+ wallet_address=test_wallet_address
360
+ )
361
+
362
+ assert result == 700.0
363
+
364
+
365
+ class TestSnapshotRecording:
366
+ @pytest.mark.asyncio
367
+ async def test_record_snapshot(self, ledger_client, test_wallet_address):
368
+ await ledger_client.strategy_snapshot(
369
+ wallet_address=test_wallet_address,
370
+ strat_portfolio_value=1050.0,
371
+ net_deposit=1000.0,
372
+ strategy_status={"current_pool": "test-pool", "apy": "5.2%"},
373
+ gas_available=0.01,
374
+ gassed_up=True,
375
+ )
376
+
377
+ # Verify snapshot was saved (no return value)
378
+ # We verify by reading the file
379
+ # This is done indirectly through the client
380
+
381
+ @pytest.mark.asyncio
382
+ async def test_snapshot_creates_record(
383
+ self, ledger_client, test_wallet_address, temp_ledger_dir
384
+ ):
385
+ await ledger_client.strategy_snapshot(
386
+ wallet_address=test_wallet_address,
387
+ strat_portfolio_value=1050.0,
388
+ net_deposit=1000.0,
389
+ strategy_status={"pool": "test"},
390
+ gas_available=0.01,
391
+ gassed_up=True,
392
+ )
393
+
394
+ data = json.loads((temp_ledger_dir / "snapshots.json").read_text())
395
+ snapshots = data["snapshots"]
396
+
397
+ assert len(snapshots) == 1
398
+ assert snapshots[0]["wallet_address"] == test_wallet_address
399
+ assert snapshots[0]["portfolio_value"] == 1050.0
400
+ assert snapshots[0]["net_deposit"] == 1000.0
401
+ assert snapshots[0]["gas_available"] == 0.01
402
+ assert snapshots[0]["gassed_up"] is True
403
+ assert snapshots[0]["strategy_status"] == {"pool": "test"}
404
+
405
+ @pytest.mark.asyncio
406
+ async def test_multiple_snapshots(
407
+ self, ledger_client, test_wallet_address, temp_ledger_dir
408
+ ):
409
+ for i in range(3):
410
+ await ledger_client.strategy_snapshot(
411
+ wallet_address=test_wallet_address,
412
+ strat_portfolio_value=1000.0 + (i * 10),
413
+ net_deposit=1000.0,
414
+ strategy_status={"iteration": i},
415
+ gas_available=0.01,
416
+ gassed_up=True,
417
+ )
418
+
419
+ data = json.loads((temp_ledger_dir / "snapshots.json").read_text())
420
+ snapshots = data["snapshots"]
421
+
422
+ assert len(snapshots) == 3
423
+ assert snapshots[0]["portfolio_value"] == 1000.0
424
+ assert snapshots[1]["portfolio_value"] == 1010.0
425
+ assert snapshots[2]["portfolio_value"] == 1020.0
426
+
427
+
428
+ class TestConcurrency:
429
+ @pytest.mark.asyncio
430
+ async def test_concurrent_writes(self, ledger_client, test_wallet_address):
431
+ async def add_deposit(amount):
432
+ await ledger_client.add_strategy_deposit(
433
+ wallet_address=test_wallet_address,
434
+ chain_id=1,
435
+ token_address="0xTest",
436
+ token_amount=str(amount),
437
+ usd_value=str(amount),
438
+ )
439
+
440
+ # Execute multiple deposits concurrently
441
+ await asyncio.gather(*[add_deposit(i * 100) for i in range(5)])
442
+
443
+ result = await ledger_client.get_strategy_transactions(
444
+ wallet_address=test_wallet_address
445
+ )
446
+
447
+ # All 5 transactions should be recorded
448
+ assert result["total"] == 5
@@ -1,5 +1,5 @@
1
1
  import json
2
- from dataclasses import dataclass, field
2
+ import os
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
@@ -18,157 +18,6 @@ def _load_config_file() -> dict[str, Any]:
18
18
 
19
19
 
20
20
  CONFIG = _load_config_file()
21
- SUPPORTED_CHAINS = [
22
- 1,
23
- 8453,
24
- 56,
25
- 42161,
26
- 137,
27
- 999,
28
- ]
29
-
30
-
31
- @dataclass
32
- class SystemConfig:
33
- api_base_url: str = field(default="https://api.wayfinder.ai")
34
- job_id: str | None = None
35
- job_type: str = "strategy"
36
- update_interval: int = 60
37
- max_retries: int = 3
38
- retry_delay: int = 5
39
- log_path: str | None = None
40
- data_path: str | None = None
41
- wallet_id: str | None = None
42
-
43
-
44
- @dataclass
45
- class StrategyJobConfig:
46
- system: SystemConfig
47
- strategy_config: dict[str, Any] = field(default_factory=dict)
48
-
49
- def __post_init__(self) -> None:
50
- try:
51
- if not isinstance(self.strategy_config, dict):
52
- self.strategy_config = {}
53
-
54
- wallet_type = self._get_wallet_type()
55
- if wallet_type and wallet_type != "local":
56
- return
57
-
58
- by_label, by_addr = self._load_wallets_from_file()
59
-
60
- self._enrich_wallet_addresses(by_label)
61
- if wallet_type in (None, "local"):
62
- self._enrich_wallet_private_keys(by_addr)
63
- except Exception as e:
64
- logger.warning(
65
- f"Failed to enrich strategy config with wallet information: {e}"
66
- )
67
-
68
- def _get_wallet_type(self) -> str | None:
69
- wallet_type = self.strategy_config.get("wallet_type")
70
- if wallet_type:
71
- return wallet_type
72
-
73
- main_wallet = self.strategy_config.get("main_wallet")
74
- if isinstance(main_wallet, dict):
75
- wallet_type = main_wallet.get("wallet_type")
76
- if wallet_type:
77
- return wallet_type
78
-
79
- strategy_wallet = self.strategy_config.get("strategy_wallet")
80
- if isinstance(strategy_wallet, dict):
81
- wallet_type = strategy_wallet.get("wallet_type")
82
- if wallet_type:
83
- return wallet_type
84
-
85
- return None
86
-
87
- def _load_wallets_from_file(
88
- self,
89
- ) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
90
- entries = _read_wallets_from_config()
91
- by_label: dict[str, dict[str, Any]] = {}
92
- by_addr: dict[str, dict[str, Any]] = {}
93
-
94
- if entries and isinstance(entries, list):
95
- for e in entries:
96
- if isinstance(e, dict):
97
- label = e.get("label")
98
- if isinstance(label, str):
99
- by_label[label] = e
100
- addr = e.get("address")
101
- if isinstance(addr, str):
102
- by_addr[addr.lower()] = e
103
-
104
- return by_label, by_addr
105
-
106
- def _enrich_wallet_addresses(self, by_label: dict[str, dict[str, Any]]) -> None:
107
- if "main_wallet" not in self.strategy_config:
108
- main_wallet = by_label.get("main")
109
- if main_wallet:
110
- self.strategy_config["main_wallet"] = {
111
- "address": main_wallet["address"]
112
- }
113
-
114
- strategy_name = self.strategy_config.get("_strategy_name")
115
- if strategy_name and isinstance(strategy_name, str):
116
- strategy_wallet = by_label.get(strategy_name)
117
- if strategy_wallet:
118
- if "strategy_wallet" not in self.strategy_config:
119
- self.strategy_config["strategy_wallet"] = {
120
- "address": strategy_wallet["address"]
121
- }
122
- elif isinstance(self.strategy_config.get("strategy_wallet"), dict):
123
- if not self.strategy_config["strategy_wallet"].get("address"):
124
- self.strategy_config["strategy_wallet"]["address"] = (
125
- strategy_wallet["address"]
126
- )
127
-
128
- def _enrich_wallet_private_keys(self, by_addr: dict[str, dict[str, Any]]) -> None:
129
- try:
130
- for key in ("main_wallet", "strategy_wallet"):
131
- wallet_obj = self.strategy_config.get(key)
132
- if isinstance(wallet_obj, dict):
133
- addr = (wallet_obj.get("address") or "").lower()
134
- entry = by_addr.get(addr)
135
- if entry:
136
- pk = entry.get("private_key") or entry.get("private_key_hex")
137
- if (
138
- pk
139
- and not wallet_obj.get("private_key")
140
- and not wallet_obj.get("private_key_hex")
141
- ):
142
- wallet_obj["private_key_hex"] = pk
143
- except Exception as e:
144
- logger.warning(
145
- f"Failed to enrich wallet private keys from config.json: {e}"
146
- )
147
-
148
- @classmethod
149
- def from_dict(
150
- cls, data: dict[str, Any], strategy_name: str | None = None
151
- ) -> "StrategyJobConfig":
152
- system_data = data.get("system", {})
153
- sys_cfg = SystemConfig(
154
- api_base_url=system_data.get("api_base_url", "https://api.wayfinder.ai"),
155
- job_id=system_data.get("job_id"),
156
- job_type=system_data.get("job_type", "strategy"),
157
- update_interval=system_data.get("update_interval", 60),
158
- max_retries=system_data.get("max_retries", 3),
159
- retry_delay=system_data.get("retry_delay", 5),
160
- log_path=system_data.get("log_path"),
161
- data_path=system_data.get("data_path"),
162
- wallet_id=system_data.get("wallet_id"),
163
- )
164
-
165
- strategy_config = data.get("strategy", {})
166
- if strategy_name:
167
- strategy_config["_strategy_name"] = strategy_name
168
- return cls(
169
- system=sys_cfg,
170
- strategy_config=strategy_config,
171
- )
172
21
 
173
22
 
174
23
  def set_rpc_urls(rpc_urls):
@@ -191,13 +40,12 @@ def get_api_base_url() -> str:
191
40
  return "https://wayfinder.ai/api/v1"
192
41
 
193
42
 
194
- def _read_wallets_from_config() -> list[dict[str, Any]]:
195
- try:
196
- wallets = CONFIG.get("wallets", [])
197
- if isinstance(wallets, list):
198
- return wallets
199
- logger.warning("Wallets section in config.json is not a list")
200
- return []
201
- except Exception as e:
202
- logger.warning(f"Failed to read wallets from config.json: {e}")
203
- return []
43
+ def get_api_key() -> str | None:
44
+ """Get API key from config or environment."""
45
+ # Check config first
46
+ system = CONFIG.get("system", {}) if isinstance(CONFIG, dict) else {}
47
+ api_key = system.get("api_key")
48
+ if api_key and isinstance(api_key, str):
49
+ return api_key.strip()
50
+ # Fall back to environment variable
51
+ return os.environ.get("WAYFINDER_API_KEY")