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.
- wayfinder_paths/CONFIG_GUIDE.md +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- 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)
|