wayfinder-paths 0.1.7__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 (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,443 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import uuid
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Any, NotRequired, Required, TypedDict
9
+
10
+ from wayfinder_paths.core.adapters.models import Operation
11
+
12
+
13
+ class StrategyTransaction(TypedDict):
14
+ """Individual strategy transaction structure"""
15
+
16
+ id: Required[str]
17
+ operation: Required[str]
18
+ timestamp: Required[str]
19
+ amount: Required[str]
20
+ token_address: Required[str]
21
+ usd_value: Required[str]
22
+ strategy_name: NotRequired[str | None]
23
+ chain_id: NotRequired[int | None]
24
+
25
+
26
+ class StrategyTransactionList(TypedDict):
27
+ """Vault transaction list response structure"""
28
+
29
+ transactions: Required[list[StrategyTransaction]]
30
+ total: Required[int]
31
+ limit: Required[int]
32
+ offset: Required[int]
33
+
34
+
35
+ class NetDeposit(TypedDict):
36
+ """Net deposit response structure"""
37
+
38
+ net_deposit: Required[str]
39
+ total_deposits: Required[str]
40
+ total_withdrawals: Required[str]
41
+ wallet_address: NotRequired[str | None]
42
+
43
+
44
+ class TransactionRecord(TypedDict):
45
+ """Transaction record response structure"""
46
+
47
+ transaction_id: Required[str]
48
+ status: Required[str]
49
+ timestamp: Required[str]
50
+
51
+
52
+ class LedgerClient:
53
+ """
54
+ Client for strategy transaction history and bookkeeping operations using local JSON files.
55
+
56
+ Supports:
57
+ - GET strategy transactions
58
+ - GET strategy net deposit
59
+ - GET strategy latest transactions
60
+ - POST add deposit
61
+ - POST add withdraw
62
+ - POST add operation
63
+ - POST record snapshot
64
+ """
65
+
66
+ def __init__(
67
+ self, api_key: str | None = None, ledger_dir: Path | str | None = None
68
+ ) -> None:
69
+ """
70
+ Initialize the ledger client.
71
+
72
+ Args:
73
+ api_key: Unused, kept for backward compatibility
74
+ ledger_dir: Directory to store ledger JSON files. Defaults to .ledger in project root
75
+ """
76
+ if ledger_dir is None:
77
+ # Default to .ledger directory in project root
78
+ project_root = Path(__file__).parent.parent.parent.parent
79
+ ledger_dir = project_root / ".ledger"
80
+
81
+ self.ledger_dir = Path(ledger_dir)
82
+ self.ledger_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ self.transactions_file = self.ledger_dir / "transactions.json"
85
+ self.snapshots_file = self.ledger_dir / "snapshots.json"
86
+
87
+ # File locks for thread-safe operations
88
+ self._transactions_lock = asyncio.Lock()
89
+ self._snapshots_lock = asyncio.Lock()
90
+
91
+ # Initialize files if they don't exist
92
+ self._initialize_files()
93
+
94
+ def _initialize_files(self) -> None:
95
+ """Create initial JSON files if they don't exist."""
96
+ if not self.transactions_file.exists():
97
+ self.transactions_file.write_text(
98
+ json.dumps({"transactions": []}, indent=2)
99
+ )
100
+
101
+ if not self.snapshots_file.exists():
102
+ self.snapshots_file.write_text(json.dumps({"snapshots": []}, indent=2))
103
+
104
+ async def _read_transactions(self) -> dict[str, Any]:
105
+ """Read transactions from file with lock."""
106
+ async with self._transactions_lock:
107
+ if not self.transactions_file.exists():
108
+ return {"transactions": []}
109
+ try:
110
+ content = self.transactions_file.read_text()
111
+ return json.loads(content)
112
+ except json.JSONDecodeError:
113
+ return {"transactions": []}
114
+
115
+ async def _write_transactions(self, data: dict[str, Any]) -> None:
116
+ """Write transactions to file with lock."""
117
+ async with self._transactions_lock:
118
+ self.transactions_file.write_text(json.dumps(data, indent=2))
119
+
120
+ async def _read_snapshots(self) -> dict[str, Any]:
121
+ """Read snapshots from file with lock."""
122
+ async with self._snapshots_lock:
123
+ if not self.snapshots_file.exists():
124
+ return {"snapshots": []}
125
+ try:
126
+ content = self.snapshots_file.read_text()
127
+ return json.loads(content)
128
+ except json.JSONDecodeError:
129
+ return {"snapshots": []}
130
+
131
+ async def _write_snapshots(self, data: dict[str, Any]) -> None:
132
+ """Write snapshots to file with lock."""
133
+ async with self._snapshots_lock:
134
+ self.snapshots_file.write_text(json.dumps(data, indent=2))
135
+
136
+ # ===================== Read Endpoints =====================
137
+
138
+ async def get_strategy_transactions(
139
+ self,
140
+ *,
141
+ wallet_address: str,
142
+ limit: int = 100,
143
+ offset: int = 0,
144
+ ) -> StrategyTransactionList:
145
+ """
146
+ Fetch a paginated list of transactions for a given strategy wallet address.
147
+
148
+ Args:
149
+ wallet_address: The strategy wallet address to filter by
150
+ limit: Maximum number of transactions to return
151
+ offset: Number of transactions to skip
152
+
153
+ Returns:
154
+ StrategyTransactionList with transactions, total, limit, and offset
155
+ """
156
+ data = await self._read_transactions()
157
+ all_transactions = data.get("transactions", [])
158
+
159
+ # Filter by wallet_address
160
+ filtered = [
161
+ tx
162
+ for tx in all_transactions
163
+ if tx.get("wallet_address", "").lower() == wallet_address.lower()
164
+ ]
165
+
166
+ # Sort by timestamp descending (most recent first)
167
+ filtered.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
168
+
169
+ total = len(filtered)
170
+ paginated = filtered[offset : offset + limit]
171
+
172
+ return {
173
+ "transactions": paginated,
174
+ "total": total,
175
+ "limit": limit,
176
+ "offset": offset,
177
+ }
178
+
179
+ async def get_strategy_net_deposit(self, *, wallet_address: str) -> float:
180
+ """
181
+ Calculate the net deposit (deposits - withdrawals) for a strategy wallet.
182
+
183
+ Args:
184
+ wallet_address: The strategy wallet address
185
+
186
+ Returns:
187
+ NetDeposit with net_deposit, total_deposits, total_withdrawals
188
+ """
189
+ data = await self._read_transactions()
190
+ all_transactions = data.get("transactions", [])
191
+
192
+ # Filter by wallet_address
193
+ filtered = [
194
+ tx
195
+ for tx in all_transactions
196
+ if tx.get("wallet_address", "").lower() == wallet_address.lower()
197
+ ]
198
+
199
+ total_deposits = 0.0
200
+ total_withdrawals = 0.0
201
+
202
+ for tx in filtered:
203
+ operation = tx.get("operation", "").upper()
204
+ usd_value = float(tx.get("usd_value", 0))
205
+
206
+ if operation == "DEPOSIT":
207
+ total_deposits += usd_value
208
+ elif operation == "WITHDRAW":
209
+ total_withdrawals += usd_value
210
+
211
+ net_deposit = total_deposits - total_withdrawals
212
+
213
+ return float(net_deposit)
214
+
215
+ async def get_strategy_latest_transactions(
216
+ self, *, wallet_address: str, limit: int = 10
217
+ ) -> StrategyTransactionList:
218
+ """
219
+ Fetch the most recent transactions for a strategy wallet.
220
+
221
+ Args:
222
+ wallet_address: The strategy wallet address
223
+ limit: Maximum number of transactions to return (default 10)
224
+
225
+ Returns:
226
+ StrategyTransactionList with the latest transactions
227
+ """
228
+ return await self.get_strategy_transactions(
229
+ wallet_address=wallet_address,
230
+ limit=limit,
231
+ offset=0,
232
+ )
233
+
234
+ # ===================== Write Endpoints =====================
235
+
236
+ async def add_strategy_deposit(
237
+ self,
238
+ *,
239
+ wallet_address: str,
240
+ chain_id: int,
241
+ token_address: str,
242
+ token_amount: str | float,
243
+ usd_value: str | float,
244
+ data: dict[str, Any] | None = None,
245
+ strategy_name: str | None = None,
246
+ ) -> TransactionRecord:
247
+ """
248
+ Record a deposit for a strategy.
249
+
250
+ Args:
251
+ wallet_address: The strategy wallet address
252
+ chain_id: The blockchain chain ID
253
+ token_address: The token contract address
254
+ token_amount: Amount of tokens deposited
255
+ usd_value: USD value of the deposit
256
+ data: Additional metadata
257
+ strategy_name: Name of the strategy
258
+
259
+ Returns:
260
+ TransactionRecord with transaction_id, status, and timestamp
261
+ """
262
+ transaction_id = str(uuid.uuid4())
263
+ timestamp = datetime.now(UTC).isoformat()
264
+
265
+ transaction = {
266
+ "id": transaction_id,
267
+ "wallet_address": wallet_address,
268
+ "operation": "DEPOSIT",
269
+ "timestamp": timestamp,
270
+ "chain_id": chain_id,
271
+ "token_address": token_address,
272
+ "token_amount": str(token_amount),
273
+ "amount": str(token_amount), # For backward compatibility
274
+ "usd_value": str(usd_value),
275
+ "data": data or {},
276
+ }
277
+
278
+ if strategy_name is not None:
279
+ transaction["strategy_name"] = strategy_name
280
+
281
+ # Add to transactions
282
+ file_data = await self._read_transactions()
283
+ file_data["transactions"].append(transaction)
284
+ await self._write_transactions(file_data)
285
+
286
+ return {
287
+ "transaction_id": transaction_id,
288
+ "status": "success",
289
+ "timestamp": timestamp,
290
+ }
291
+
292
+ async def add_strategy_withdraw(
293
+ self,
294
+ *,
295
+ wallet_address: str,
296
+ chain_id: int,
297
+ token_address: str,
298
+ token_amount: str | float,
299
+ usd_value: str | float,
300
+ data: dict[str, Any] | None = None,
301
+ strategy_name: str | None = None,
302
+ ) -> TransactionRecord:
303
+ """
304
+ Record a withdrawal for a strategy.
305
+
306
+ Args:
307
+ wallet_address: The strategy wallet address
308
+ chain_id: The blockchain chain ID
309
+ token_address: The token contract address
310
+ token_amount: Amount of tokens withdrawn
311
+ usd_value: USD value of the withdrawal
312
+ data: Additional metadata
313
+ strategy_name: Name of the strategy
314
+
315
+ Returns:
316
+ TransactionRecord with transaction_id, status, and timestamp
317
+ """
318
+ transaction_id = str(uuid.uuid4())
319
+ timestamp = datetime.now(UTC).isoformat()
320
+
321
+ transaction = {
322
+ "id": transaction_id,
323
+ "wallet_address": wallet_address,
324
+ "operation": "WITHDRAW",
325
+ "timestamp": timestamp,
326
+ "chain_id": chain_id,
327
+ "token_address": token_address,
328
+ "token_amount": str(token_amount),
329
+ "amount": str(token_amount), # For backward compatibility
330
+ "usd_value": str(usd_value),
331
+ "data": data or {},
332
+ }
333
+
334
+ if strategy_name is not None:
335
+ transaction["strategy_name"] = strategy_name
336
+
337
+ # Add to transactions
338
+ file_data = await self._read_transactions()
339
+ file_data["transactions"].append(transaction)
340
+ await self._write_transactions(file_data)
341
+
342
+ return {
343
+ "transaction_id": transaction_id,
344
+ "status": "success",
345
+ "timestamp": timestamp,
346
+ }
347
+
348
+ async def add_strategy_operation(
349
+ self,
350
+ *,
351
+ wallet_address: str,
352
+ operation_data: Operation,
353
+ usd_value: str | float,
354
+ strategy_name: str | None = None,
355
+ ) -> TransactionRecord:
356
+ """
357
+ Record a strategy operation (e.g., swaps, rebalances) for bookkeeping.
358
+
359
+ Args:
360
+ wallet_address: Strategy wallet address
361
+ operation_data: Operation model (SWAP, LEND, UNLEND, etc.)
362
+ usd_value: USD value of the operation
363
+ strategy_name: Optional strategy name
364
+
365
+ Returns:
366
+ TransactionRecord with transaction_id, status, and timestamp
367
+ """
368
+ transaction_id = str(uuid.uuid4())
369
+ timestamp = datetime.now(UTC).isoformat()
370
+
371
+ op_dict = operation_data.model_dump(mode="json")
372
+ operation_type = op_dict.get("type", "OPERATION")
373
+
374
+ transaction = {
375
+ "id": transaction_id,
376
+ "wallet_address": wallet_address,
377
+ "operation": operation_type,
378
+ "timestamp": timestamp,
379
+ "usd_value": str(usd_value),
380
+ "op_data": op_dict,
381
+ "data": {},
382
+ }
383
+
384
+ # Extract relevant fields from operation data for easier querying
385
+ if operation_type == "SWAP":
386
+ transaction["token_address"] = op_dict.get("to_token_id", "")
387
+ transaction["amount"] = op_dict.get("to_amount", "0")
388
+ elif operation_type in ("LEND", "UNLEND"):
389
+ transaction["token_address"] = op_dict.get("contract", "")
390
+ transaction["amount"] = str(op_dict.get("amount", 0))
391
+
392
+ if strategy_name is not None:
393
+ transaction["strategy_name"] = strategy_name
394
+
395
+ # Add to transactions
396
+ file_data = await self._read_transactions()
397
+ file_data["transactions"].append(transaction)
398
+ await self._write_transactions(file_data)
399
+
400
+ return {
401
+ "transaction_id": transaction_id,
402
+ "status": "success",
403
+ "timestamp": timestamp,
404
+ }
405
+
406
+ async def strategy_snapshot(
407
+ self,
408
+ wallet_address: str,
409
+ strat_portfolio_value: float,
410
+ net_deposit: float,
411
+ strategy_status: dict,
412
+ gas_available: float,
413
+ gassed_up: bool,
414
+ ) -> None:
415
+ """
416
+ Record a periodic snapshot of strategy state.
417
+
418
+ Args:
419
+ wallet_address: Strategy wallet address
420
+ strat_portfolio_value: Total portfolio value in USD
421
+ net_deposit: Net deposit amount in USD
422
+ strategy_status: Arbitrary strategy status data
423
+ gas_available: Available gas tokens
424
+ gassed_up: Whether strategy has sufficient gas
425
+ """
426
+ snapshot_id = str(uuid.uuid4())
427
+ timestamp = datetime.now(UTC).isoformat()
428
+
429
+ snapshot = {
430
+ "id": snapshot_id,
431
+ "wallet_address": wallet_address,
432
+ "timestamp": timestamp,
433
+ "portfolio_value": strat_portfolio_value,
434
+ "net_deposit": net_deposit,
435
+ "gas_available": gas_available,
436
+ "gassed_up": gassed_up,
437
+ "strategy_status": strategy_status,
438
+ }
439
+
440
+ # Add to snapshots
441
+ file_data = await self._read_snapshots()
442
+ file_data["snapshots"].append(snapshot)
443
+ await self._write_snapshots(file_data)
@@ -0,0 +1,128 @@
1
+ """
2
+ Pool Client
3
+ Provides read-only access to pool metadata and analytics via public endpoints.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, NotRequired, Required, TypedDict
9
+
10
+ from wayfinder_paths.core.clients.AuthClient import AuthClient
11
+ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
12
+ from wayfinder_paths.core.settings import settings
13
+
14
+
15
+ class PoolData(TypedDict):
16
+ """Individual pool data structure"""
17
+
18
+ id: Required[str]
19
+ name: Required[str]
20
+ symbol: Required[str]
21
+ address: Required[str]
22
+ chain_id: Required[int]
23
+ chain_code: Required[str]
24
+ apy: NotRequired[float]
25
+ tvl: NotRequired[float]
26
+ llama_apy_pct: NotRequired[float | None]
27
+ llama_tvl_usd: NotRequired[float | None]
28
+ llama_stablecoin: NotRequired[bool | None]
29
+ llama_il_risk: NotRequired[str | None]
30
+ network: NotRequired[str | None]
31
+
32
+
33
+ class PoolList(TypedDict):
34
+ """Pool list response structure"""
35
+
36
+ pools: Required[list[PoolData]]
37
+ total: NotRequired[int | None]
38
+
39
+
40
+ class LlamaMatch(TypedDict):
41
+ """Llama match data structure"""
42
+
43
+ id: Required[str]
44
+ llama_apy_pct: Required[float]
45
+ llama_tvl_usd: Required[float]
46
+ llama_stablecoin: Required[bool]
47
+ llama_il_risk: Required[str]
48
+ network: Required[str]
49
+
50
+
51
+ class LlamaReport(TypedDict):
52
+ """Llama report data structure"""
53
+
54
+ identifier: Required[str]
55
+ apy: NotRequired[float | None]
56
+ tvl: NotRequired[float | None]
57
+
58
+
59
+ class PoolClient(WayfinderClient):
60
+ """Client for pool-related read operations"""
61
+
62
+ def __init__(self, api_key: str | None = None):
63
+ super().__init__(api_key=api_key)
64
+ self.api_base_url = f"{settings.WAYFINDER_API_URL}"
65
+ self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
66
+
67
+ async def get_pools_by_ids(
68
+ self,
69
+ *,
70
+ pool_ids: str,
71
+ merge_external: bool | None = None,
72
+ ) -> PoolList:
73
+ """
74
+ Fetch pools by comma-separated pool ids.
75
+
76
+ Example:
77
+ GET /api/v1/public/pools/?pool_ids=a,b&merge_external=false
78
+ """
79
+ url = f"{self.api_base_url}/public/pools/"
80
+ params: dict[str, Any] = {"pool_ids": pool_ids}
81
+ if merge_external is not None:
82
+ params["merge_external"] = "true" if merge_external else "false"
83
+ response = await self._request("GET", url, params=params, headers={})
84
+ response.raise_for_status()
85
+ data = response.json()
86
+ return data.get("data", data)
87
+
88
+ async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
89
+ """
90
+ Fetch all pools.
91
+
92
+ Example:
93
+ GET /api/v1/public/pools/?merge_external=false
94
+ """
95
+ url = f"{self.api_base_url}/public/pools/"
96
+ params: dict[str, Any] = {}
97
+ if merge_external is not None:
98
+ params["merge_external"] = "true" if merge_external else "false"
99
+ response = await self._request("GET", url, params=params, headers={})
100
+ response.raise_for_status()
101
+ data = response.json()
102
+ return data.get("data", data)
103
+
104
+ async def get_llama_matches(self) -> dict[str, LlamaMatch]:
105
+ """
106
+ Fetch Llama matches for pools.
107
+
108
+ GET /api/v1/public/pools/llama/matches/
109
+ """
110
+ url = f"{self.api_base_url}/public/pools/llama/matches/"
111
+ response = await self._request("GET", url, headers={})
112
+ response.raise_for_status()
113
+ data = response.json()
114
+ return data.get("data", data)
115
+
116
+ async def get_llama_reports(self, *, identifiers: str) -> dict[str, LlamaReport]:
117
+ """
118
+ Fetch Llama reports using identifiers (token ids, address_network, or pool ids).
119
+
120
+ Example:
121
+ GET /api/v1/public/pools/llama/reports/?identifiers=pool-1,usd-coin
122
+ """
123
+ url = f"{self.api_base_url}/public/pools/llama/reports/"
124
+ params = {"identifiers": identifiers}
125
+ response = await self._request("GET", url, params=params, headers={})
126
+ response.raise_for_status()
127
+ data = response.json()
128
+ return data.get("data", data)