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
@@ -5,9 +5,7 @@ import json
5
5
  import uuid
6
6
  from datetime import UTC, datetime
7
7
  from pathlib import Path
8
- from typing import Any, NotRequired, Required, TypedDict
9
-
10
- from wayfinder_paths.core.adapters.models import Operation
8
+ from typing import Any, Literal, NotRequired, Required, TypedDict
11
9
 
12
10
 
13
11
  class StrategyTransaction(TypedDict):
@@ -19,6 +17,8 @@ class StrategyTransaction(TypedDict):
19
17
  usd_value: Required[str]
20
18
  strategy_name: NotRequired[str | None]
21
19
  chain_id: NotRequired[int | None]
20
+ created: NotRequired[str]
21
+ op_data: NotRequired[dict[str, Any]]
22
22
 
23
23
 
24
24
  class StrategyTransactionList(TypedDict):
@@ -28,148 +28,197 @@ class StrategyTransactionList(TypedDict):
28
28
  offset: Required[int]
29
29
 
30
30
 
31
- class NetDeposit(TypedDict):
32
- net_deposit: Required[str]
33
- total_deposits: Required[str]
34
- total_withdrawals: Required[str]
35
- wallet_address: NotRequired[str | None]
36
-
37
-
38
31
  class TransactionRecord(TypedDict):
39
32
  transaction_id: Required[str]
40
33
  status: Required[str]
41
34
  timestamp: Required[str]
42
35
 
43
36
 
37
+ class OperationData(TypedDict, total=False):
38
+ type: str
39
+ transaction_hash: str
40
+ transaction_chain_id: int
41
+
42
+
43
+ class StrategyOperationTransactionData(TypedDict):
44
+ op_data: OperationData
45
+
46
+
44
47
  class LedgerClient:
45
48
  def __init__(self, ledger_dir: Path | str | None = None) -> None:
46
49
  if ledger_dir is None:
47
- # Default to .ledger directory in project root
48
- project_root = Path(__file__).parent.parent.parent.parent
49
- ledger_dir = project_root / ".ledger"
50
-
50
+ ledger_dir = Path(__file__).resolve().parents[3] / ".ledger"
51
51
  self.ledger_dir = Path(ledger_dir)
52
52
  self.ledger_dir.mkdir(parents=True, exist_ok=True)
53
-
54
53
  self.transactions_file = self.ledger_dir / "transactions.json"
55
54
  self.snapshots_file = self.ledger_dir / "snapshots.json"
56
-
57
- # File locks for thread-safe operations
58
55
  self._transactions_lock = asyncio.Lock()
59
56
  self._snapshots_lock = asyncio.Lock()
60
-
61
- # Initialize files if they don't exist
62
- self._initialize_files()
63
-
64
- def _initialize_files(self) -> None:
65
- if not self.transactions_file.exists():
66
- self.transactions_file.write_text(
67
- json.dumps({"transactions": []}, indent=2)
68
- )
69
-
70
- if not self.snapshots_file.exists():
71
- self.snapshots_file.write_text(json.dumps({"snapshots": []}, indent=2))
57
+ for path, default in [
58
+ (self.transactions_file, {"transactions": []}),
59
+ (self.snapshots_file, {"snapshots": []}),
60
+ ]:
61
+ if not path.exists():
62
+ path.write_text(json.dumps(default, indent=2))
63
+
64
+ @staticmethod
65
+ def _load_json_file(path: Path, default: dict[str, Any]) -> dict[str, Any]:
66
+ if not path.exists():
67
+ return default
68
+ try:
69
+ return json.loads(path.read_text())
70
+ except json.JSONDecodeError:
71
+ return default
72
+
73
+ @staticmethod
74
+ def _save_json_file(path: Path, data: dict[str, Any]) -> None:
75
+ path.write_text(json.dumps(data, indent=2))
76
+
77
+ async def _read_json(
78
+ self, path: Path, lock: asyncio.Lock, default: dict[str, Any]
79
+ ) -> dict[str, Any]:
80
+ async with lock:
81
+ return self._load_json_file(path, default)
82
+
83
+ async def _write_json(
84
+ self, path: Path, lock: asyncio.Lock, data: dict[str, Any]
85
+ ) -> None:
86
+ async with lock:
87
+ self._save_json_file(path, data)
72
88
 
73
89
  async def _read_transactions(self) -> dict[str, Any]:
74
- async with self._transactions_lock:
75
- if not self.transactions_file.exists():
76
- return {"transactions": []}
77
- try:
78
- content = self.transactions_file.read_text()
79
- return json.loads(content)
80
- except json.JSONDecodeError:
81
- return {"transactions": []}
90
+ return await self._read_json(
91
+ self.transactions_file, self._transactions_lock, {"transactions": []}
92
+ )
82
93
 
83
94
  async def _write_transactions(self, data: dict[str, Any]) -> None:
84
- async with self._transactions_lock:
85
- self.transactions_file.write_text(json.dumps(data, indent=2))
95
+ await self._write_json(self.transactions_file, self._transactions_lock, data)
86
96
 
87
97
  async def _read_snapshots(self) -> dict[str, Any]:
88
- async with self._snapshots_lock:
89
- if not self.snapshots_file.exists():
90
- return {"snapshots": []}
91
- try:
92
- content = self.snapshots_file.read_text()
93
- return json.loads(content)
94
- except json.JSONDecodeError:
95
- return {"snapshots": []}
98
+ return await self._read_json(
99
+ self.snapshots_file, self._snapshots_lock, {"snapshots": []}
100
+ )
96
101
 
97
102
  async def _write_snapshots(self, data: dict[str, Any]) -> None:
98
- async with self._snapshots_lock:
99
- self.snapshots_file.write_text(json.dumps(data, indent=2))
100
-
101
- # ===================== Read Endpoints =====================
103
+ await self._write_json(self.snapshots_file, self._snapshots_lock, data)
102
104
 
103
- async def get_strategy_transactions(
105
+ async def _transactions_for_wallet(
104
106
  self,
105
- *,
106
107
  wallet_address: str,
107
- limit: int = 100,
108
- offset: int = 0,
109
- ) -> StrategyTransactionList:
108
+ *,
109
+ operation: str | None = None,
110
+ ) -> list[dict[str, Any]]:
110
111
  data = await self._read_transactions()
111
- all_transactions = data.get("transactions", [])
112
-
113
- # Filter by wallet_address
114
- filtered = [
112
+ txs = [
115
113
  tx
116
- for tx in all_transactions
114
+ for tx in data.get("transactions", [])
117
115
  if tx.get("wallet_address", "").lower() == wallet_address.lower()
118
116
  ]
117
+ if operation is not None:
118
+ txs = [tx for tx in txs if tx.get("operation") == operation]
119
+ txs.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
120
+ return txs
121
+
122
+ def _to_strategy_transaction(self, tx: dict[str, Any]) -> StrategyTransaction:
123
+ operation = tx["operation"]
124
+ op_data: dict[str, Any] = {}
125
+ if operation == "STRAT_OP":
126
+ op_data = (tx.get("data") or {}).get("op_data") or {}
127
+ if op_data:
128
+ operation = op_data.get("type") or operation
129
+
130
+ amount = tx["amount"]
131
+ token_address = tx["token_address"]
132
+ if op_data:
133
+ op_type = op_data.get("type", "")
134
+ if op_type == "SWAP":
135
+ token_address = op_data.get("to_token_id", "")
136
+ amount = op_data.get("to_amount", "0")
137
+ elif op_type in ("LEND", "UNLEND"):
138
+ token_address = op_data.get("contract", "")
139
+ amount = str(op_data.get("amount", 0))
140
+ else:
141
+ amount = amount or "0"
142
+ token_address = token_address or ""
143
+
144
+ out: StrategyTransaction = {
145
+ "id": tx["id"],
146
+ "operation": str(operation),
147
+ "timestamp": tx["timestamp"],
148
+ "created": tx["timestamp"],
149
+ "amount": amount,
150
+ "token_address": token_address,
151
+ "usd_value": tx["usd_value"],
152
+ }
153
+ if "chain_id" in tx:
154
+ out["chain_id"] = tx["chain_id"]
155
+ if "strategy_name" in tx:
156
+ out["strategy_name"] = tx["strategy_name"]
157
+ if op_data:
158
+ out["op_data"] = op_data
159
+ return out
160
+
161
+ @staticmethod
162
+ def _record(transaction_id: str, timestamp: str) -> TransactionRecord:
163
+ return {
164
+ "transaction_id": transaction_id,
165
+ "status": "success",
166
+ "timestamp": timestamp,
167
+ }
119
168
 
120
- # Sort by timestamp descending (most recent first)
121
- filtered.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
169
+ async def _append_transaction(self, transaction: dict[str, Any]) -> None:
170
+ async with self._transactions_lock:
171
+ data = self._load_json_file(self.transactions_file, {"transactions": []})
172
+ data.setdefault("transactions", []).append(transaction)
173
+ self._save_json_file(self.transactions_file, data)
122
174
 
175
+ async def get_strategy_transactions(
176
+ self,
177
+ *,
178
+ wallet_address: str,
179
+ limit: int = 50,
180
+ offset: int = 0,
181
+ operation: str | None = None,
182
+ ) -> StrategyTransactionList:
183
+ filtered = await self._transactions_for_wallet(
184
+ wallet_address, operation=operation
185
+ )
123
186
  total = len(filtered)
124
187
  paginated = filtered[offset : offset + limit]
125
-
188
+ transactions = [self._to_strategy_transaction(tx) for tx in paginated]
126
189
  return {
127
- "transactions": paginated,
190
+ "transactions": transactions,
128
191
  "total": total,
129
192
  "limit": limit,
130
193
  "offset": offset,
131
194
  }
132
195
 
133
196
  async def get_strategy_net_deposit(self, *, wallet_address: str) -> float:
134
- data = await self._read_transactions()
135
- all_transactions = data.get("transactions", [])
136
-
137
- # Filter by wallet_address
138
- filtered = [
139
- tx
140
- for tx in all_transactions
141
- if tx.get("wallet_address", "").lower() == wallet_address.lower()
142
- ]
143
-
197
+ filtered = await self._transactions_for_wallet(wallet_address)
144
198
  total_deposits = 0.0
145
199
  total_withdrawals = 0.0
146
-
147
200
  for tx in filtered:
148
- operation = tx.get("operation", "").upper()
149
- usd_value = float(tx.get("usd_value", 0))
150
-
151
- if operation == "DEPOSIT":
152
- total_deposits += usd_value
153
- elif operation == "WITHDRAW":
154
- total_withdrawals += usd_value
155
-
156
- net_deposit = total_deposits - total_withdrawals
157
-
158
- return float(net_deposit)
201
+ op = tx.get("operation", "").upper()
202
+ usd = float(tx.get("usd_value", 0))
203
+ if op == "DEPOSIT":
204
+ total_deposits += usd
205
+ elif op == "WITHDRAW":
206
+ total_withdrawals += usd
207
+ return total_deposits - total_withdrawals
159
208
 
160
209
  async def get_strategy_latest_transactions(
161
- self, *, wallet_address: str, limit: int = 10
210
+ self, *, wallet_address: str
162
211
  ) -> StrategyTransactionList:
163
212
  return await self.get_strategy_transactions(
164
213
  wallet_address=wallet_address,
165
- limit=limit,
214
+ limit=80,
166
215
  offset=0,
216
+ operation="STRAT_OP",
167
217
  )
168
218
 
169
- # ===================== Write Endpoints =====================
170
-
171
- async def add_strategy_deposit(
219
+ async def _add_deposit_or_withdraw(
172
220
  self,
221
+ operation: Literal["DEPOSIT", "WITHDRAW"],
173
222
  *,
174
223
  wallet_address: str,
175
224
  chain_id: int,
@@ -181,32 +230,44 @@ class LedgerClient:
181
230
  ) -> TransactionRecord:
182
231
  transaction_id = str(uuid.uuid4())
183
232
  timestamp = datetime.now(UTC).isoformat()
184
-
185
- transaction = {
233
+ transaction: dict[str, Any] = {
186
234
  "id": transaction_id,
187
235
  "wallet_address": wallet_address,
188
- "operation": "DEPOSIT",
236
+ "operation": operation,
189
237
  "timestamp": timestamp,
190
- "chain_id": chain_id,
191
- "token_address": token_address,
192
- "token_amount": str(token_amount),
193
238
  "amount": str(token_amount),
239
+ "token_address": token_address,
194
240
  "usd_value": str(usd_value),
195
- "data": data or {},
241
+ "chain_id": chain_id,
196
242
  }
197
-
243
+ if data is not None:
244
+ transaction["data"] = data
198
245
  if strategy_name is not None:
199
246
  transaction["strategy_name"] = strategy_name
247
+ await self._append_transaction(transaction)
248
+ return self._record(transaction_id, timestamp)
200
249
 
201
- file_data = await self._read_transactions()
202
- file_data["transactions"].append(transaction)
203
- await self._write_transactions(file_data)
204
-
205
- return {
206
- "transaction_id": transaction_id,
207
- "status": "success",
208
- "timestamp": timestamp,
209
- }
250
+ async def add_strategy_deposit(
251
+ self,
252
+ *,
253
+ wallet_address: str,
254
+ chain_id: int,
255
+ token_address: str,
256
+ token_amount: str | float,
257
+ usd_value: str | float,
258
+ data: dict[str, Any] | None = None,
259
+ strategy_name: str | None = None,
260
+ ) -> TransactionRecord:
261
+ return await self._add_deposit_or_withdraw(
262
+ "DEPOSIT",
263
+ wallet_address=wallet_address,
264
+ chain_id=chain_id,
265
+ token_address=token_address,
266
+ token_amount=token_amount,
267
+ usd_value=usd_value,
268
+ data=data,
269
+ strategy_name=strategy_name,
270
+ )
210
271
 
211
272
  async def add_strategy_withdraw(
212
273
  self,
@@ -219,78 +280,47 @@ class LedgerClient:
219
280
  data: dict[str, Any] | None = None,
220
281
  strategy_name: str | None = None,
221
282
  ) -> TransactionRecord:
222
- transaction_id = str(uuid.uuid4())
223
- timestamp = datetime.now(UTC).isoformat()
224
-
225
- transaction = {
226
- "id": transaction_id,
227
- "wallet_address": wallet_address,
228
- "operation": "WITHDRAW",
229
- "timestamp": timestamp,
230
- "chain_id": chain_id,
231
- "token_address": token_address,
232
- "token_amount": str(token_amount),
233
- "amount": str(token_amount),
234
- "usd_value": str(usd_value),
235
- "data": data or {},
236
- }
237
-
238
- if strategy_name is not None:
239
- transaction["strategy_name"] = strategy_name
240
-
241
- file_data = await self._read_transactions()
242
- file_data["transactions"].append(transaction)
243
- await self._write_transactions(file_data)
244
-
245
- return {
246
- "transaction_id": transaction_id,
247
- "status": "success",
248
- "timestamp": timestamp,
249
- }
283
+ return await self._add_deposit_or_withdraw(
284
+ "WITHDRAW",
285
+ wallet_address=wallet_address,
286
+ chain_id=chain_id,
287
+ token_address=token_address,
288
+ token_amount=token_amount,
289
+ usd_value=usd_value,
290
+ data=data,
291
+ strategy_name=strategy_name,
292
+ )
250
293
 
251
294
  async def add_strategy_operation(
252
295
  self,
253
296
  *,
254
297
  wallet_address: str,
255
- operation_data: Operation,
298
+ operation_data: dict[str, Any],
256
299
  usd_value: str | float,
257
300
  strategy_name: str | None = None,
258
301
  ) -> TransactionRecord:
259
302
  transaction_id = str(uuid.uuid4())
260
303
  timestamp = datetime.now(UTC).isoformat()
261
-
262
- op_dict = operation_data.model_dump(mode="json")
263
- operation_type = op_dict.get("type", "OPERATION")
264
-
265
- transaction = {
304
+ transaction: dict[str, Any] = {
266
305
  "id": transaction_id,
267
306
  "wallet_address": wallet_address,
268
- "operation": operation_type,
307
+ "operation": "STRAT_OP",
269
308
  "timestamp": timestamp,
309
+ "amount": "0",
310
+ "token_address": "",
270
311
  "usd_value": str(usd_value),
271
- "op_data": op_dict,
272
- "data": {},
312
+ "data": {"op_data": operation_data},
273
313
  }
274
-
275
- if operation_type == "SWAP":
276
- transaction["token_address"] = op_dict.get("to_token_id", "")
277
- transaction["amount"] = op_dict.get("to_amount", "0")
278
- elif operation_type in ("LEND", "UNLEND"):
279
- transaction["token_address"] = op_dict.get("contract", "")
280
- transaction["amount"] = str(op_dict.get("amount", 0))
281
-
282
314
  if strategy_name is not None:
283
315
  transaction["strategy_name"] = strategy_name
316
+ await self._append_transaction(transaction)
317
+ return self._record(transaction_id, timestamp)
284
318
 
285
- file_data = await self._read_transactions()
286
- file_data["transactions"].append(transaction)
287
- await self._write_transactions(file_data)
288
-
289
- return {
290
- "transaction_id": transaction_id,
291
- "status": "success",
292
- "timestamp": timestamp,
293
- }
319
+ async def _append_snapshot(self, snapshot: dict[str, Any]) -> None:
320
+ async with self._snapshots_lock:
321
+ data = self._load_json_file(self.snapshots_file, {"snapshots": []})
322
+ data.setdefault("snapshots", []).append(snapshot)
323
+ self._save_json_file(self.snapshots_file, data)
294
324
 
295
325
  async def strategy_snapshot(
296
326
  self,
@@ -301,20 +331,14 @@ class LedgerClient:
301
331
  gas_available: float,
302
332
  gassed_up: bool,
303
333
  ) -> None:
304
- snapshot_id = str(uuid.uuid4())
305
- timestamp = datetime.now(UTC).isoformat()
306
-
307
334
  snapshot = {
308
- "id": snapshot_id,
335
+ "id": str(uuid.uuid4()),
309
336
  "wallet_address": wallet_address,
310
- "timestamp": timestamp,
337
+ "timestamp": datetime.now(UTC).isoformat(),
311
338
  "portfolio_value": strat_portfolio_value,
312
339
  "net_deposit": net_deposit,
313
340
  "gas_available": gas_available,
314
341
  "gassed_up": gassed_up,
315
342
  "strategy_status": strategy_status,
316
343
  }
317
-
318
- file_data = await self._read_snapshots()
319
- file_data["snapshots"].append(snapshot)
320
- await self._write_snapshots(file_data)
344
+ await self._append_snapshot(snapshot)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import xml.etree.ElementTree as ET
3
4
  from typing import NotRequired, Required, TypedDict
4
5
 
5
6
  from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
@@ -76,10 +77,20 @@ class GasToken(TypedDict):
76
77
  chain: NotRequired[ChainInfo]
77
78
 
78
79
 
80
+ class FuzzyTokenResult(TypedDict):
81
+ coingecko_id: NotRequired[str]
82
+ address: NotRequired[str]
83
+ chain: NotRequired[str]
84
+ name: NotRequired[str]
85
+ symbol: NotRequired[str]
86
+ price: NotRequired[float]
87
+ confidence: NotRequired[int]
88
+
89
+
79
90
  class TokenClient(WayfinderClient):
80
91
  def __init__(self):
81
92
  super().__init__()
82
- self.api_base_url = f"{get_api_base_url()}/v1/blockchain/tokens"
93
+ self.api_base_url = f"{get_api_base_url()}/blockchain/tokens"
83
94
 
84
95
  async def get_token_details(
85
96
  self, query: str, market_data: bool = False, chain_id: int | None = None
@@ -103,3 +114,38 @@ class TokenClient(WayfinderClient):
103
114
  response.raise_for_status()
104
115
  data = response.json()
105
116
  return data.get("data", data)
117
+
118
+ async def fuzzy_search(
119
+ self, query: str, chain: str | None = None
120
+ ) -> dict[str, list[FuzzyTokenResult] | str | None]:
121
+ url = f"{self.api_base_url}/fuzzy/"
122
+ params: dict[str, str] = {"query": query}
123
+ if chain:
124
+ params["chain"] = chain
125
+ response = await self._authed_request("GET", url, params=params)
126
+ response.raise_for_status()
127
+ trace_id = response.headers.get("X-Token-Fuzzy-Trace")
128
+ tokens = self._parse_fuzzy_xml(response.text)
129
+ return {"tokens": tokens, "trace_id": trace_id}
130
+
131
+ def _parse_fuzzy_xml(self, xml_content: str) -> list[FuzzyTokenResult]:
132
+ root = ET.fromstring(xml_content)
133
+ tokens: list[FuzzyTokenResult] = []
134
+ for token_elem in root.findall("token"):
135
+ token: FuzzyTokenResult = {}
136
+ for field in ["coingecko_id", "address", "chain", "name", "symbol"]:
137
+ elem = token_elem.find(field)
138
+ if elem is not None and elem.text:
139
+ token[field] = elem.text # type: ignore[literal-required]
140
+ for num_field in ["price", "confidence"]:
141
+ elem = token_elem.find(num_field)
142
+ if elem is not None and elem.text:
143
+ try:
144
+ if num_field == "price":
145
+ token["price"] = float(elem.text)
146
+ else:
147
+ token["confidence"] = int(elem.text)
148
+ except ValueError:
149
+ pass
150
+ tokens.append(token)
151
+ return tokens
@@ -60,14 +60,12 @@ class WayfinderClient:
60
60
  url: str,
61
61
  *,
62
62
  headers: dict[str, str] | None = None,
63
- retry_on_401: bool = False,
64
63
  **kwargs: Any,
65
64
  ) -> httpx.Response:
66
65
  logger.debug(f"Making {method} request to {url}")
67
66
  start_time = time.time()
68
67
 
69
- # Ensure API key is set in headers if available and not already set
70
- # This ensures API keys are passed to all endpoints (including public ones) for rate limiting
68
+ # Pass API key to all endpoints (including public ones) for rate limiting
71
69
  if not self.headers.get("X-API-KEY"):
72
70
  creds = self._load_config_credentials()
73
71
  api_key = creds.get("api_key")
@@ -9,26 +9,21 @@ from wayfinder_paths.core.clients.protocols import (
9
9
  LedgerClientProtocol,
10
10
  PoolClientProtocol,
11
11
  TokenClientProtocol,
12
- WalletClientProtocol,
13
12
  )
14
13
  from wayfinder_paths.core.clients.TokenClient import TokenClient
15
- from wayfinder_paths.core.clients.WalletClient import WalletClient
16
14
  from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
17
15
 
18
16
  __all__ = [
19
17
  "WayfinderClient",
20
18
  "ClientManager",
21
19
  "TokenClient",
22
- "WalletClient",
23
20
  "LedgerClient",
24
21
  "PoolClient",
25
22
  "BRAPClient",
26
23
  "HyperlendClient",
27
- # Protocols for SDK usage
28
24
  "TokenClientProtocol",
29
25
  "HyperlendClientProtocol",
30
26
  "LedgerClientProtocol",
31
- "WalletClientProtocol",
32
27
  "PoolClientProtocol",
33
28
  "BRAPClientProtocol",
34
29
  ]
@@ -1,30 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Protocol
4
-
5
- if TYPE_CHECKING:
6
- from wayfinder_paths.core.clients.BRAPClient import BRAPQuoteResponse
7
- from wayfinder_paths.core.clients.HyperlendClient import (
8
- AssetsView,
9
- LendRateHistory,
10
- MarketEntry,
11
- StableMarketsHeadroomResponse,
12
- )
13
- from wayfinder_paths.core.clients.LedgerClient import (
14
- StrategyTransactionList,
15
- TransactionRecord,
16
- )
17
- from wayfinder_paths.core.clients.PoolClient import (
18
- LlamaMatchesResponse,
19
- PoolList,
20
- )
21
- from wayfinder_paths.core.clients.TokenClient import (
22
- GasToken,
23
- TokenDetails,
24
- )
25
- from wayfinder_paths.core.clients.WalletClient import (
26
- AddressBalance,
27
- )
3
+ from typing import Any, Protocol
4
+
5
+ from wayfinder_paths.core.clients.BRAPClient import BRAPQuoteResponse
6
+ from wayfinder_paths.core.clients.HyperlendClient import (
7
+ AssetsView,
8
+ LendRateHistory,
9
+ MarketEntry,
10
+ StableMarketsHeadroomResponse,
11
+ )
12
+ from wayfinder_paths.core.clients.LedgerClient import (
13
+ StrategyTransactionList,
14
+ TransactionRecord,
15
+ )
16
+ from wayfinder_paths.core.clients.PoolClient import (
17
+ LlamaMatchesResponse,
18
+ PoolList,
19
+ )
20
+ from wayfinder_paths.core.clients.TokenClient import (
21
+ GasToken,
22
+ TokenDetails,
23
+ )
28
24
 
29
25
 
30
26
  class TokenClientProtocol(Protocol):
@@ -117,16 +113,6 @@ class LedgerClientProtocol(Protocol):
117
113
  ) -> TransactionRecord: ...
118
114
 
119
115
 
120
- class WalletClientProtocol(Protocol):
121
- async def get_token_balance_for_address(
122
- self,
123
- *,
124
- wallet_address: str,
125
- query: str,
126
- chain_id: int | None = None,
127
- ) -> AddressBalance: ...
128
-
129
-
130
116
  class PoolClientProtocol(Protocol):
131
117
  async def get_pools_by_ids(
132
118
  self,