wayfinder-paths 0.1.3__py3-none-any.whl → 0.1.5__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 (109) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +37 -32
  2. wayfinder_paths/__init__.py +3 -3
  3. wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/README.md +12 -12
  4. wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/adapter.py +12 -11
  5. wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/examples.json +1 -1
  6. wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/manifest.yaml +1 -1
  7. wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/test_adapter.py +12 -6
  8. wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/README.md +2 -2
  9. wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/adapter.py +30 -23
  10. wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/manifest.yaml +1 -1
  11. wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/test_adapter.py +2 -2
  12. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  13. wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/adapter.py +33 -26
  14. wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/manifest.yaml +1 -1
  15. wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/test_adapter.py +2 -2
  16. wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/README.md +27 -40
  17. wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/adapter.py +78 -75
  18. wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/examples.json +10 -4
  19. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  20. wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/test_adapter.py +33 -28
  21. wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/README.md +2 -14
  22. wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/adapter.py +12 -19
  23. wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/manifest.yaml +1 -1
  24. wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/test_adapter.py +2 -2
  25. wayfinder_paths/{vaults/adapters → adapters}/token_adapter/README.md +1 -1
  26. wayfinder_paths/{vaults/adapters → adapters}/token_adapter/adapter.py +8 -4
  27. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  28. wayfinder_paths/{vaults/adapters → adapters}/token_adapter/manifest.yaml +1 -1
  29. wayfinder_paths/{vaults/adapters → adapters}/token_adapter/test_adapter.py +1 -1
  30. wayfinder_paths/config.example.json +3 -1
  31. wayfinder_paths/core/__init__.py +3 -3
  32. wayfinder_paths/core/adapters/BaseAdapter.py +20 -3
  33. wayfinder_paths/core/adapters/models.py +41 -0
  34. wayfinder_paths/core/clients/BRAPClient.py +21 -2
  35. wayfinder_paths/core/clients/ClientManager.py +42 -63
  36. wayfinder_paths/core/clients/HyperlendClient.py +46 -5
  37. wayfinder_paths/core/clients/LedgerClient.py +350 -124
  38. wayfinder_paths/core/clients/PoolClient.py +51 -19
  39. wayfinder_paths/core/clients/SimulationClient.py +16 -4
  40. wayfinder_paths/core/clients/TokenClient.py +34 -18
  41. wayfinder_paths/core/clients/TransactionClient.py +18 -2
  42. wayfinder_paths/core/clients/WalletClient.py +35 -4
  43. wayfinder_paths/core/clients/WayfinderClient.py +16 -5
  44. wayfinder_paths/core/clients/protocols.py +69 -62
  45. wayfinder_paths/core/clients/sdk_example.py +0 -5
  46. wayfinder_paths/core/config.py +192 -103
  47. wayfinder_paths/core/constants/base.py +17 -0
  48. wayfinder_paths/core/engine/{VaultJob.py → StrategyJob.py} +25 -19
  49. wayfinder_paths/core/engine/__init__.py +2 -2
  50. wayfinder_paths/core/engine/manifest.py +1 -1
  51. wayfinder_paths/core/services/base.py +6 -4
  52. wayfinder_paths/core/services/local_evm_txn.py +3 -2
  53. wayfinder_paths/core/settings.py +2 -2
  54. wayfinder_paths/core/strategies/Strategy.py +123 -37
  55. wayfinder_paths/core/utils/evm_helpers.py +12 -10
  56. wayfinder_paths/core/wallets/README.md +3 -3
  57. wayfinder_paths/core/wallets/WalletManager.py +3 -3
  58. wayfinder_paths/{vaults/policies → policies}/enso.py +1 -1
  59. wayfinder_paths/{vaults/policies → policies}/hyper_evm.py +2 -2
  60. wayfinder_paths/{vaults/policies → policies}/hyperlend.py +1 -1
  61. wayfinder_paths/{vaults/policies → policies}/moonwell.py +1 -1
  62. wayfinder_paths/{vaults/policies → policies}/prjx.py +1 -1
  63. wayfinder_paths/run_strategy.py +29 -27
  64. wayfinder_paths/scripts/create_strategy.py +3 -3
  65. wayfinder_paths/scripts/make_wallets.py +6 -6
  66. wayfinder_paths/scripts/validate_manifests.py +2 -2
  67. wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/README.md +10 -9
  68. wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/manifest.yaml +1 -1
  69. wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/strategy.py +47 -167
  70. wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/test_strategy.py +10 -8
  71. wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/README.md +15 -14
  72. wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/manifest.yaml +2 -2
  73. wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/strategy.py +97 -97
  74. wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/test_strategy.py +8 -8
  75. wayfinder_paths/{vaults/templates → templates}/adapter/README.md +5 -5
  76. wayfinder_paths/{vaults/templates → templates}/adapter/manifest.yaml +1 -1
  77. wayfinder_paths/{vaults/templates → templates}/adapter/test_adapter.py +1 -1
  78. wayfinder_paths/{vaults/templates → templates}/strategy/README.md +10 -9
  79. wayfinder_paths/{vaults/templates → templates}/strategy/manifest.yaml +1 -1
  80. wayfinder_paths/{vaults/templates → templates}/strategy/test_strategy.py +8 -8
  81. wayfinder_paths/tests/test_test_coverage.py +5 -5
  82. {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/METADATA +146 -69
  83. wayfinder_paths-0.1.5.dist-info/RECORD +126 -0
  84. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +0 -7
  85. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +0 -11
  86. wayfinder_paths/vaults/adapters/token_adapter/examples.json +0 -26
  87. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  88. wayfinder_paths-0.1.3.dist-info/RECORD +0 -126
  89. /wayfinder_paths/{vaults → adapters}/__init__.py +0 -0
  90. /wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/__init__.py +0 -0
  91. /wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/examples.json +0 -0
  92. /wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/__init__.py +0 -0
  93. /wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/__init__.py +0 -0
  94. /wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/examples.json +0 -0
  95. /wayfinder_paths/{vaults/adapters → adapters}/token_adapter/__init__.py +0 -0
  96. /wayfinder_paths/{vaults/policies → policies}/erc20.py +0 -0
  97. /wayfinder_paths/{vaults/policies → policies}/evm.py +0 -0
  98. /wayfinder_paths/{vaults/policies → policies}/hyperliquid.py +0 -0
  99. /wayfinder_paths/{vaults/policies → policies}/util.py +0 -0
  100. /wayfinder_paths/{vaults/adapters → strategies}/__init__.py +0 -0
  101. /wayfinder_paths/{vaults/strategies → strategies}/config.py +0 -0
  102. /wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/examples.json +0 -0
  103. /wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/examples.json +0 -0
  104. /wayfinder_paths/{vaults/templates → templates}/adapter/adapter.py +0 -0
  105. /wayfinder_paths/{vaults/templates → templates}/adapter/examples.json +0 -0
  106. /wayfinder_paths/{vaults/templates → templates}/strategy/examples.json +0 -0
  107. /wayfinder_paths/{vaults/templates → templates}/strategy/strategy.py +0 -0
  108. {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/LICENSE +0 -0
  109. {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/WHEEL +0 -0
@@ -1,86 +1,244 @@
1
- from typing import Any
1
+ from __future__ import annotations
2
2
 
3
- from wayfinder_paths.core.clients.AuthClient import AuthClient
4
- from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
5
- from wayfinder_paths.core.settings import settings
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
6
9
 
10
+ from wayfinder_paths.core.adapters.models import Operation
7
11
 
8
- class LedgerClient(WayfinderClient):
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:
9
53
  """
10
- Client for vault transaction history and bookkeeping operations.
54
+ Client for strategy transaction history and bookkeeping operations using local JSON files.
11
55
 
12
56
  Supports:
13
- - GET vault transactions
14
- - GET vault net deposit
15
- - GET vault last rotation time
57
+ - GET strategy transactions
58
+ - GET strategy net deposit
59
+ - GET strategy latest transactions
16
60
  - POST add deposit
17
61
  - POST add withdraw
18
62
  - POST add operation
19
- - POST add cashflow
63
+ - POST record snapshot
20
64
  """
21
65
 
22
- def __init__(self, api_key: str | None = None) -> None:
23
- super().__init__(api_key=api_key)
24
- self.api_base_url = f"{settings.WAYFINDER_API_URL}"
25
- self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
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))
26
135
 
27
136
  # ===================== Read Endpoints =====================
28
137
 
29
- async def get_vault_transactions(
138
+ async def get_strategy_transactions(
30
139
  self,
31
140
  *,
32
141
  wallet_address: str,
33
- limit: int = 50,
142
+ limit: int = 100,
34
143
  offset: int = 0,
35
- ) -> dict[str, Any]:
144
+ ) -> StrategyTransactionList:
36
145
  """
37
- Fetch a paginated list of transactions for a given vault wallet and address.
146
+ Fetch a paginated list of transactions for a given strategy wallet address.
38
147
 
39
- GET /api/v1/public/vaults/transactions/?wallet_address=...&limit=...&offset=...
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
40
155
  """
41
- url = f"{self.api_base_url}/public/vaults/transactions/"
42
- params = {
43
- "wallet_address": wallet_address,
44
- "limit": str(limit),
45
- "offset": str(offset),
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,
46
177
  }
47
- response = await self._authed_request("GET", url, params=params)
48
- data = response.json()
49
- return data.get("data", data)
50
178
 
51
- async def get_vault_net_deposit(self, *, wallet_address: str) -> dict[str, Any]:
179
+ async def get_strategy_net_deposit(self, *, wallet_address: str) -> NetDeposit:
52
180
  """
53
- Fetch the net deposit (deposits - withdrawals) for a vault and address.
181
+ Calculate the net deposit (deposits - withdrawals) for a strategy wallet.
54
182
 
55
- GET /api/v1/public/vaults/net-deposit/?wallet_address=...
183
+ Args:
184
+ wallet_address: The strategy wallet address
185
+
186
+ Returns:
187
+ NetDeposit with net_deposit, total_deposits, total_withdrawals
56
188
  """
57
- url = f"{self.api_base_url}/public/vaults/net-deposit/"
58
- params = {
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 {
214
+ "net_deposit": str(net_deposit),
215
+ "total_deposits": str(total_deposits),
216
+ "total_withdrawals": str(total_withdrawals),
59
217
  "wallet_address": wallet_address,
60
218
  }
61
- response = await self._authed_request("GET", url, params=params)
62
- data = response.json()
63
- return data.get("data", data)
64
219
 
65
- async def get_vault_latest_transactions(
66
- self, *, wallet_address: str
67
- ) -> dict[str, Any]:
220
+ async def get_strategy_latest_transactions(
221
+ self, *, wallet_address: str, limit: int = 10
222
+ ) -> StrategyTransactionList:
68
223
  """
69
- Fetch the last rotation time for a vault and address.
224
+ Fetch the most recent transactions for a strategy wallet.
225
+
226
+ Args:
227
+ wallet_address: The strategy wallet address
228
+ limit: Maximum number of transactions to return (default 10)
70
229
 
71
- GET /api/v1/public/vaults/last-rotation-time/?wallet_address=...
230
+ Returns:
231
+ StrategyTransactionList with the latest transactions
72
232
  """
73
- url = f"{self.api_base_url}/public/vaults/latest-transactions/"
74
- params = {
75
- "wallet_address": wallet_address,
76
- }
77
- response = await self._authed_request("GET", url, params=params)
78
- data = response.json()
79
- return data.get("data", data)
233
+ return await self.get_strategy_transactions(
234
+ wallet_address=wallet_address,
235
+ limit=limit,
236
+ offset=0,
237
+ )
80
238
 
81
239
  # ===================== Write Endpoints =====================
82
240
 
83
- async def add_vault_deposit(
241
+ async def add_strategy_deposit(
84
242
  self,
85
243
  *,
86
244
  wallet_address: str,
@@ -90,28 +248,53 @@ class LedgerClient(WayfinderClient):
90
248
  usd_value: str | float,
91
249
  data: dict[str, Any] | None = None,
92
250
  strategy_name: str | None = None,
93
- ) -> dict[str, Any]:
251
+ ) -> TransactionRecord:
94
252
  """
95
- Record a deposit for a vault.
253
+ Record a deposit for a strategy.
254
+
255
+ Args:
256
+ wallet_address: The strategy wallet address
257
+ chain_id: The blockchain chain ID
258
+ token_address: The token contract address
259
+ token_amount: Amount of tokens deposited
260
+ usd_value: USD value of the deposit
261
+ data: Additional metadata
262
+ strategy_name: Name of the strategy
96
263
 
97
- POST /api/v1/public/vaults/deposits/
264
+ Returns:
265
+ TransactionRecord with transaction_id, status, and timestamp
98
266
  """
99
- url = f"{self.api_base_url}/public/vaults/deposits/"
100
- payload: dict[str, Any] = {
267
+ transaction_id = str(uuid.uuid4())
268
+ timestamp = datetime.now(UTC).isoformat()
269
+
270
+ transaction = {
271
+ "id": transaction_id,
101
272
  "wallet_address": wallet_address,
273
+ "operation": "DEPOSIT",
274
+ "timestamp": timestamp,
102
275
  "chain_id": chain_id,
103
276
  "token_address": token_address,
104
277
  "token_amount": str(token_amount),
278
+ "amount": str(token_amount), # For backward compatibility
105
279
  "usd_value": str(usd_value),
106
280
  "data": data or {},
107
281
  }
282
+
108
283
  if strategy_name is not None:
109
- payload["strategy_name"] = strategy_name
110
- response = await self._authed_request("POST", url, json=payload)
111
- data_resp = response.json()
112
- return data_resp.get("data", data_resp)
284
+ transaction["strategy_name"] = strategy_name
113
285
 
114
- async def add_vault_withdraw(
286
+ # Add to transactions
287
+ file_data = await self._read_transactions()
288
+ file_data["transactions"].append(transaction)
289
+ await self._write_transactions(file_data)
290
+
291
+ return {
292
+ "transaction_id": transaction_id,
293
+ "status": "success",
294
+ "timestamp": timestamp,
295
+ }
296
+
297
+ async def add_strategy_withdraw(
115
298
  self,
116
299
  *,
117
300
  wallet_address: str,
@@ -121,102 +304,145 @@ class LedgerClient(WayfinderClient):
121
304
  usd_value: str | float,
122
305
  data: dict[str, Any] | None = None,
123
306
  strategy_name: str | None = None,
124
- ) -> dict[str, Any]:
307
+ ) -> TransactionRecord:
125
308
  """
126
- Record a withdrawal for a vault.
309
+ Record a withdrawal for a strategy.
127
310
 
128
- POST /api/v1/public/vaults/withdrawals/
311
+ Args:
312
+ wallet_address: The strategy wallet address
313
+ chain_id: The blockchain chain ID
314
+ token_address: The token contract address
315
+ token_amount: Amount of tokens withdrawn
316
+ usd_value: USD value of the withdrawal
317
+ data: Additional metadata
318
+ strategy_name: Name of the strategy
319
+
320
+ Returns:
321
+ TransactionRecord with transaction_id, status, and timestamp
129
322
  """
130
- url = f"{self.api_base_url}/public/vaults/withdrawals/"
131
- payload: dict[str, Any] = {
323
+ transaction_id = str(uuid.uuid4())
324
+ timestamp = datetime.now(UTC).isoformat()
325
+
326
+ transaction = {
327
+ "id": transaction_id,
132
328
  "wallet_address": wallet_address,
329
+ "operation": "WITHDRAW",
330
+ "timestamp": timestamp,
133
331
  "chain_id": chain_id,
134
332
  "token_address": token_address,
135
333
  "token_amount": str(token_amount),
334
+ "amount": str(token_amount), # For backward compatibility
136
335
  "usd_value": str(usd_value),
137
336
  "data": data or {},
138
337
  }
338
+
139
339
  if strategy_name is not None:
140
- payload["strategy_name"] = strategy_name
141
- response = await self._authed_request("POST", url, json=payload)
142
- data_resp = response.json()
143
- return data_resp.get("data", data_resp)
340
+ transaction["strategy_name"] = strategy_name
341
+
342
+ # Add to transactions
343
+ file_data = await self._read_transactions()
344
+ file_data["transactions"].append(transaction)
345
+ await self._write_transactions(file_data)
346
+
347
+ return {
348
+ "transaction_id": transaction_id,
349
+ "status": "success",
350
+ "timestamp": timestamp,
351
+ }
144
352
 
145
- async def add_vault_operation(
353
+ async def add_strategy_operation(
146
354
  self,
147
355
  *,
148
356
  wallet_address: str,
149
- operation_data: dict[str, Any],
357
+ operation_data: Operation,
150
358
  usd_value: str | float,
151
359
  strategy_name: str | None = None,
152
- ) -> dict[str, Any]:
360
+ ) -> TransactionRecord:
153
361
  """
154
- Record a vault operation (e.g., swaps, rebalances) for bookkeeping.
362
+ Record a strategy operation (e.g., swaps, rebalances) for bookkeeping.
155
363
 
156
- POST /api/v1/public/vaults/operations/
364
+ Args:
365
+ wallet_address: Strategy wallet address
366
+ operation_data: Operation model (SWAP, LEND, UNLEND, etc.)
367
+ usd_value: USD value of the operation
368
+ strategy_name: Optional strategy name
369
+
370
+ Returns:
371
+ TransactionRecord with transaction_id, status, and timestamp
157
372
  """
158
- url = f"{self.api_base_url}/public/vaults/operations/"
159
- payload: dict[str, Any] = {
373
+ transaction_id = str(uuid.uuid4())
374
+ timestamp = datetime.now(UTC).isoformat()
375
+
376
+ op_dict = operation_data.model_dump(mode="json")
377
+ operation_type = op_dict.get("type", "OPERATION")
378
+
379
+ transaction = {
380
+ "id": transaction_id,
160
381
  "wallet_address": wallet_address,
161
- "operation_data": operation_data,
382
+ "operation": operation_type,
383
+ "timestamp": timestamp,
162
384
  "usd_value": str(usd_value),
385
+ "op_data": op_dict,
386
+ "data": {},
163
387
  }
388
+
389
+ # Extract relevant fields from operation data for easier querying
390
+ if operation_type == "SWAP":
391
+ transaction["token_address"] = op_dict.get("to_token_id", "")
392
+ transaction["amount"] = op_dict.get("to_amount", "0")
393
+ elif operation_type in ("LEND", "UNLEND"):
394
+ transaction["token_address"] = op_dict.get("contract", "")
395
+ transaction["amount"] = str(op_dict.get("amount", 0))
396
+
164
397
  if strategy_name is not None:
165
- payload["strategy_name"] = strategy_name
166
- response = await self._authed_request("POST", url, json=payload)
167
- data_resp = response.json()
168
- return data_resp.get("data", data_resp)
398
+ transaction["strategy_name"] = strategy_name
399
+
400
+ # Add to transactions
401
+ file_data = await self._read_transactions()
402
+ file_data["transactions"].append(transaction)
403
+ await self._write_transactions(file_data)
404
+
405
+ return {
406
+ "transaction_id": transaction_id,
407
+ "status": "success",
408
+ "timestamp": timestamp,
409
+ }
169
410
 
170
- async def add_vault_cashflow(
411
+ async def strategy_snapshot(
171
412
  self,
172
- *,
173
413
  wallet_address: str,
174
- block_timestamp: int,
175
- token_addr: str,
176
- amount: str | int | float,
177
- description: str,
178
- strategy_name: str | None = None,
179
- ) -> dict[str, Any]:
414
+ strat_portfolio_value: float,
415
+ net_deposit: float,
416
+ strategy_status: dict,
417
+ gas_available: float,
418
+ gassed_up: bool,
419
+ ) -> None:
180
420
  """
181
- Record a cashflow for a vault (interest, funding, reward, or fee).
182
-
183
- POST /api/v1/public/vaults/cashflows/
421
+ Record a periodic snapshot of strategy state.
184
422
 
185
423
  Args:
186
- wallet_address: Vault wallet address
187
- block_timestamp: Block timestamp (Unix timestamp)
188
- token_addr: Token contract address
189
- amount: Cashflow amount (in token units)
190
- description: Cashflow type - must be one of: "interest", "funding", "reward", "fee", "lend", "unlend", "borrow"
191
- strategy_name: Optional strategy name
192
-
193
- Returns:
194
- Dict containing the cashflow record or error details
195
- """
196
- valid_descriptions = [
197
- "interest",
198
- "funding",
199
- "reward",
200
- "fee",
201
- "lend",
202
- "unlend",
203
- "borrow",
204
- ]
205
- if description not in valid_descriptions:
206
- raise ValueError(
207
- f"Invalid description '{description}'. Must be one of: {valid_descriptions}"
208
- )
424
+ wallet_address: Strategy wallet address
425
+ strat_portfolio_value: Total portfolio value in USD
426
+ net_deposit: Net deposit amount in USD
427
+ strategy_status: Arbitrary strategy status data
428
+ gas_available: Available gas tokens
429
+ gassed_up: Whether strategy has sufficient gas
430
+ """
431
+ snapshot_id = str(uuid.uuid4())
432
+ timestamp = datetime.now(UTC).isoformat()
209
433
 
210
- url = f"{self.api_base_url}/public/vaults/cashflows/"
211
- payload: dict[str, Any] = {
434
+ snapshot = {
435
+ "id": snapshot_id,
212
436
  "wallet_address": wallet_address,
213
- "block_timestamp": block_timestamp,
214
- "token_addr": token_addr,
215
- "amount": str(amount),
216
- "description": description,
437
+ "timestamp": timestamp,
438
+ "portfolio_value": strat_portfolio_value,
439
+ "net_deposit": net_deposit,
440
+ "gas_available": gas_available,
441
+ "gassed_up": gassed_up,
442
+ "strategy_status": strategy_status,
217
443
  }
218
- if strategy_name is not None:
219
- payload["strategy_name"] = strategy_name
220
- response = await self._authed_request("POST", url, json=payload)
221
- data_resp = response.json()
222
- return data_resp.get("data", data_resp)
444
+
445
+ # Add to snapshots
446
+ file_data = await self._read_snapshots()
447
+ file_data["snapshots"].append(snapshot)
448
+ await self._write_snapshots(file_data)
@@ -3,13 +3,59 @@ Pool Client
3
3
  Provides read-only access to pool metadata and analytics via public endpoints.
4
4
  """
5
5
 
6
- from typing import Any
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, NotRequired, Required, TypedDict
7
9
 
8
10
  from wayfinder_paths.core.clients.AuthClient import AuthClient
9
11
  from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
10
12
  from wayfinder_paths.core.settings import settings
11
13
 
12
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
+
13
59
  class PoolClient(WayfinderClient):
14
60
  """Client for pool-related read operations"""
15
61
 
@@ -23,7 +69,7 @@ class PoolClient(WayfinderClient):
23
69
  *,
24
70
  pool_ids: str,
25
71
  merge_external: bool | None = None,
26
- ) -> dict[str, Any]:
72
+ ) -> PoolList:
27
73
  """
28
74
  Fetch pools by comma-separated pool ids.
29
75
 
@@ -39,9 +85,7 @@ class PoolClient(WayfinderClient):
39
85
  data = response.json()
40
86
  return data.get("data", data)
41
87
 
42
- async def get_all_pools(
43
- self, *, merge_external: bool | None = None
44
- ) -> dict[str, Any]:
88
+ async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
45
89
  """
46
90
  Fetch all pools.
47
91
 
@@ -57,19 +101,7 @@ class PoolClient(WayfinderClient):
57
101
  data = response.json()
58
102
  return data.get("data", data)
59
103
 
60
- async def get_combined_pool_reports(self) -> dict[str, Any]:
61
- """
62
- Fetch combined pool reports.
63
-
64
- GET /api/v1/public/pools/combined/
65
- """
66
- url = f"{self.api_base_url}/public/pools/combined/"
67
- response = await self._request("GET", url, headers={})
68
- response.raise_for_status()
69
- data = response.json()
70
- return data.get("data", data)
71
-
72
- async def get_llama_matches(self) -> dict[str, Any]:
104
+ async def get_llama_matches(self) -> dict[str, LlamaMatch]:
73
105
  """
74
106
  Fetch Llama matches for pools.
75
107
 
@@ -81,7 +113,7 @@ class PoolClient(WayfinderClient):
81
113
  data = response.json()
82
114
  return data.get("data", data)
83
115
 
84
- async def get_llama_reports(self, *, identifiers: str) -> dict[str, Any]:
116
+ async def get_llama_reports(self, *, identifiers: str) -> dict[str, LlamaReport]:
85
117
  """
86
118
  Fetch Llama reports using identifiers (token ids, address_network, or pool ids).
87
119