wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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 (61) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +13 -14
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
  4. wayfinder_paths/adapters/brap_adapter/README.md +11 -16
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
  6. wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
  7. wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
  9. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  11. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  12. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
  15. wayfinder_paths/adapters/pool_adapter/README.md +9 -10
  16. wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
  17. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  18. wayfinder_paths/adapters/token_adapter/README.md +2 -14
  19. wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
  20. wayfinder_paths/adapters/token_adapter/examples.json +4 -8
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
  22. wayfinder_paths/core/clients/BRAPClient.py +102 -61
  23. wayfinder_paths/core/clients/ClientManager.py +1 -68
  24. wayfinder_paths/core/clients/HyperlendClient.py +125 -64
  25. wayfinder_paths/core/clients/LedgerClient.py +1 -4
  26. wayfinder_paths/core/clients/PoolClient.py +122 -48
  27. wayfinder_paths/core/clients/TokenClient.py +91 -36
  28. wayfinder_paths/core/clients/WalletClient.py +26 -56
  29. wayfinder_paths/core/clients/WayfinderClient.py +28 -160
  30. wayfinder_paths/core/clients/__init__.py +0 -2
  31. wayfinder_paths/core/clients/protocols.py +35 -46
  32. wayfinder_paths/core/clients/sdk_example.py +37 -22
  33. wayfinder_paths/core/constants/erc20_abi.py +0 -11
  34. wayfinder_paths/core/engine/StrategyJob.py +10 -56
  35. wayfinder_paths/core/services/base.py +1 -0
  36. wayfinder_paths/core/services/local_evm_txn.py +25 -9
  37. wayfinder_paths/core/services/local_token_txn.py +2 -6
  38. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  39. wayfinder_paths/core/strategies/Strategy.py +16 -4
  40. wayfinder_paths/core/utils/evm_helpers.py +2 -9
  41. wayfinder_paths/policies/erc20.py +1 -1
  42. wayfinder_paths/run_strategy.py +13 -19
  43. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
  44. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
  45. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
  46. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
  47. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
  48. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
  49. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
  51. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
  52. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
  53. wayfinder_paths/templates/adapter/README.md +1 -1
  54. wayfinder_paths/templates/strategy/README.md +3 -3
  55. wayfinder_paths/templates/strategy/test_strategy.py +3 -2
  56. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
  57. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
  58. wayfinder_paths/abis/generic/erc20.json +0 -383
  59. wayfinder_paths/core/clients/AuthClient.py +0 -83
  60. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
  61. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
@@ -23,24 +23,23 @@ balance = BalanceAdapter(config, web3_service=web3_service)
23
23
 
24
24
  ## API surface
25
25
 
26
- ### `get_balance(token_id: str, wallet_address: str)`
27
- Returns the raw balance (as an integer) for a specific token on a wallet.
26
+ ### `get_balance(*, query: str | dict, wallet_address: str, chain_id: int | None = None)`
27
+ Returns the raw balance (as an integer) for a specific token or pool on a wallet.
28
+
29
+ `query`: token_id/address string or a dict with a `"token_id"` key. When `query` is a token identifier (e.g. `"usd-coin-base"`), `chain_id` is auto-resolved from token info; when it is a pool address, `chain_id` must be provided.
28
30
 
29
31
  ```python
32
+ # Token balance (chain_id auto-resolved)
30
33
  success, balance = await balance.get_balance(
31
- token_id="usd-coin-base",
34
+ query="usd-coin-base",
32
35
  wallet_address=config["main_wallet"]["address"],
33
36
  )
34
- ```
35
37
 
36
- ### `get_pool_balance(pool_address: str, chain_id: int, user_address: str)`
37
- Fetches the amount supplied to a specific pool, using the `/wallets/pool-balance` endpoint.
38
-
39
- ```python
40
- success, amount = await balance.get_pool_balance(
41
- pool_address="0xPool",
38
+ # Pool balance (chain_id required)
39
+ success, pool_balance = await balance.get_balance(
40
+ query="0xPool...",
41
+ wallet_address=config["strategy_wallet"]["address"],
42
42
  chain_id=8453,
43
- user_address=config["strategy_wallet"]["address"],
44
43
  )
45
44
  ```
46
45
 
@@ -80,10 +79,10 @@ class MyStrategy(Strategy):
80
79
  self.balance_adapter = balance_adapter
81
80
 
82
81
  async def _status(self):
83
- success, pool_balance = await self.balance_adapter.get_pool_balance(
84
- pool_address=self.current_pool["address"],
82
+ success, pool_balance = await self.balance_adapter.get_balance(
83
+ query=self.current_pool["address"],
84
+ wallet_address=self.config["strategy_wallet"]["address"],
85
85
  chain_id=self.current_pool["chain"]["id"],
86
- user_address=self.config["strategy_wallet"]["address"],
87
86
  )
88
87
  return {"portfolio_value": float(pool_balance or 0), ...}
89
88
  ```
@@ -42,16 +42,39 @@ class BalanceAdapter(BaseAdapter):
42
42
  async def get_balance(
43
43
  self,
44
44
  *,
45
- token_id: str,
45
+ query: str | dict[str, Any],
46
46
  wallet_address: str,
47
+ chain_id: int | None = None,
47
48
  ) -> tuple[bool, str | int]:
48
- """Get token balance for a wallet."""
49
+ """Get token or pool balance for a wallet.
50
+
51
+ query: token_id/address string or a dict with a "token_id" key.
52
+ """
53
+ resolved = query if isinstance(query, str) else (query or {}).get("token_id")
54
+ if not resolved:
55
+ return (False, "missing query")
49
56
  try:
50
- data = await self.wallet_client.get_token_balance_for_wallet(
51
- token_id=token_id,
57
+ if chain_id is None:
58
+ token_info = await self.token_client.get_token_details(resolved)
59
+ if not token_info:
60
+ return (False, f"Token not found: {resolved}")
61
+ resolved_chain_id = resolve_chain_id(token_info, self.logger)
62
+ if resolved_chain_id is None:
63
+ return (False, f"Token {resolved} is missing a chain id")
64
+ chain_id = resolved_chain_id
65
+
66
+ data = await self.wallet_client.get_token_balance_for_address(
52
67
  wallet_address=wallet_address,
68
+ query=resolved,
69
+ chain_id=int(chain_id),
53
70
  )
54
- return (True, data.get("balance"))
71
+ # Use _parse_balance for consistent parsing (handles balance_raw or balance)
72
+ raw = (
73
+ data.get("balance_raw") or data.get("balance")
74
+ if isinstance(data, dict)
75
+ else None
76
+ )
77
+ return (True, self._parse_balance(raw))
55
78
  except Exception as e:
56
79
  return (False, str(e))
57
80
 
@@ -93,6 +116,46 @@ class BalanceAdapter(BaseAdapter):
93
116
  skip_ledger=skip_ledger,
94
117
  )
95
118
 
119
+ async def send_to_address(
120
+ self,
121
+ token_id: str,
122
+ amount: float,
123
+ from_wallet: dict[str, Any] | None,
124
+ to_address: str,
125
+ skip_ledger: bool = True,
126
+ ) -> tuple[bool, Any]:
127
+ """Send tokens from a wallet to an arbitrary address (e.g., bridge contract)."""
128
+ if self.token_transactions is None:
129
+ return False, "Token transaction service not configured"
130
+
131
+ from_address = self._wallet_address(from_wallet)
132
+ if not from_address:
133
+ return False, "from_wallet missing or invalid"
134
+
135
+ if not to_address:
136
+ return False, "to_address is required"
137
+
138
+ token_info = await self.token_client.get_token_details(token_id)
139
+ if not token_info:
140
+ return False, f"Token not found: {token_id}"
141
+
142
+ build_success, tx_data = await self.token_transactions.build_send(
143
+ token_id=token_id,
144
+ amount=amount,
145
+ from_address=from_address,
146
+ to_address=to_address,
147
+ token_info=token_info,
148
+ )
149
+ if not build_success:
150
+ return False, tx_data
151
+
152
+ tx = tx_data
153
+ broadcast_result = await self.wallet_provider.broadcast_transaction(
154
+ tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
155
+ )
156
+
157
+ return broadcast_result
158
+
96
159
  async def _move_between_wallets(
97
160
  self,
98
161
  *,
@@ -159,6 +222,7 @@ class BalanceAdapter(BaseAdapter):
159
222
 
160
223
  usd_value = await self._token_amount_usd(token_info, amount)
161
224
  try:
225
+ token_id = token_info.get("token_id") or token_info.get("id")
162
226
  success, response = await ledger_method(
163
227
  wallet_address=wallet_address,
164
228
  chain_id=chain_id,
@@ -166,7 +230,7 @@ class BalanceAdapter(BaseAdapter):
166
230
  token_amount=str(amount),
167
231
  usd_value=usd_value,
168
232
  data={
169
- "token_id": token_info.get("token_id"),
233
+ "token_id": token_id,
170
234
  "amount": str(amount),
171
235
  "usd_value": usd_value,
172
236
  },
@@ -176,15 +240,16 @@ class BalanceAdapter(BaseAdapter):
176
240
  self.logger.warning(
177
241
  "Ledger entry failed",
178
242
  wallet=wallet_address,
179
- token_id=token_info.get("token_id"),
243
+ token_id=token_id,
180
244
  amount=amount,
181
245
  error=response,
182
246
  )
183
247
  except Exception as exc: # noqa: BLE001
248
+ token_id = token_info.get("token_id") or token_info.get("id")
184
249
  self.logger.warning(
185
250
  f"Ledger entry raised: {exc}",
186
251
  wallet=wallet_address,
187
- token_id=token_info.get("token_id"),
252
+ token_id=token_id,
188
253
  )
189
254
 
190
255
  async def _token_amount_usd(
@@ -208,27 +273,3 @@ class BalanceAdapter(BaseAdapter):
208
273
  if isinstance(evm_wallet, dict):
209
274
  return evm_wallet.get("address")
210
275
  return None
211
-
212
- async def get_pool_balance(
213
- self,
214
- *,
215
- pool_address: str,
216
- chain_id: int,
217
- user_address: str,
218
- ) -> tuple[bool, Any]:
219
- """Get pool balance for a wallet."""
220
- try:
221
- data = await self.wallet_client.get_pool_balance_for_wallet(
222
- pool_address=pool_address,
223
- chain_id=chain_id,
224
- user_address=user_address,
225
- human_readable=False,
226
- )
227
- raw = (
228
- data.get("balance_raw") or data.get("balance")
229
- if isinstance(data, dict)
230
- else None
231
- )
232
- return (True, self._parse_balance(raw))
233
- except Exception as e:
234
- return (False, str(e))
@@ -57,3 +57,126 @@ class TestBalanceAdapter:
57
57
  def test_adapter_type(self, adapter):
58
58
  """Test adapter has adapter_type"""
59
59
  assert adapter.adapter_type == "BALANCE"
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_get_balance_with_query_string(
63
+ self, adapter, mock_token_client, mock_wallet_client
64
+ ):
65
+ """Test get_balance with query as string (auto-resolves chain_id)."""
66
+ mock_token_client.get_token_details = AsyncMock(
67
+ return_value={
68
+ "token_id": "usd-coin-base",
69
+ "address": "0x123",
70
+ "chain": {"id": 8453, "code": "base"},
71
+ }
72
+ )
73
+ mock_wallet_client.get_token_balance_for_address = AsyncMock(
74
+ return_value={"balance": 1000000}
75
+ )
76
+
77
+ success, balance = await adapter.get_balance(
78
+ query="usd-coin-base",
79
+ wallet_address="0xWallet",
80
+ )
81
+
82
+ assert success
83
+ assert balance == 1000000
84
+ mock_token_client.get_token_details.assert_called_once_with("usd-coin-base")
85
+ mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
86
+ wallet_address="0xWallet",
87
+ query="usd-coin-base",
88
+ chain_id=8453,
89
+ )
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_get_balance_with_query_dict(
93
+ self, adapter, mock_token_client, mock_wallet_client
94
+ ):
95
+ """get_balance accepts query= as dict with token_id key."""
96
+ mock_token_client.get_token_details = AsyncMock(
97
+ return_value={
98
+ "token_id": "wsteth-base",
99
+ "address": "0x456",
100
+ "chain": {"id": 8453, "code": "base"},
101
+ }
102
+ )
103
+ mock_wallet_client.get_token_balance_for_address = AsyncMock(
104
+ return_value={"balance": 3000000}
105
+ )
106
+
107
+ success, balance = await adapter.get_balance(
108
+ query={"token_id": "wsteth-base"},
109
+ wallet_address="0x123",
110
+ )
111
+ assert success
112
+ assert balance == 3000000
113
+ mock_token_client.get_token_details.assert_called_once_with("wsteth-base")
114
+ mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
115
+ wallet_address="0x123",
116
+ query="wsteth-base",
117
+ chain_id=8453,
118
+ )
119
+
120
+ @pytest.mark.asyncio
121
+ async def test_get_balance_missing_query(self, adapter):
122
+ """get_balance returns error when query is empty or missing token_id."""
123
+ success, result = await adapter.get_balance(query={}, wallet_address="0xabc")
124
+ assert success is False
125
+ assert "missing query" in str(result)
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_get_balance_with_pool_address(
129
+ self, adapter, mock_token_client, mock_wallet_client
130
+ ):
131
+ """Test get_balance with pool address (explicit chain_id)"""
132
+ mock_wallet_client.get_token_balance_for_address = AsyncMock(
133
+ return_value={"balance": 5000000}
134
+ )
135
+ mock_token_client.get_token_details = AsyncMock()
136
+
137
+ success, balance = await adapter.get_balance(
138
+ query="0xPoolAddress",
139
+ wallet_address="0xWallet",
140
+ chain_id=8453,
141
+ )
142
+
143
+ assert success
144
+ assert balance == 5000000
145
+ mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
146
+ wallet_address="0xWallet",
147
+ query="0xPoolAddress",
148
+ chain_id=8453,
149
+ )
150
+ mock_token_client.get_token_details.assert_not_called()
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_get_balance_token_not_found(self, adapter, mock_token_client):
154
+ """Test get_balance when token is not found"""
155
+ mock_token_client.get_token_details = AsyncMock(return_value=None)
156
+
157
+ success, error = await adapter.get_balance(
158
+ query="invalid-token",
159
+ wallet_address="0xWallet",
160
+ )
161
+
162
+ assert success is False
163
+ assert "Token not found" in str(error)
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_get_balance_missing_chain_id(self, adapter, mock_token_client):
167
+ """Test get_balance when chain_id cannot be resolved"""
168
+ mock_token_client.get_token_details = AsyncMock(
169
+ return_value={
170
+ "token_id": "token-without-chain",
171
+ "address": "0x123",
172
+ "chain": {},
173
+ }
174
+ )
175
+
176
+ success, error = await adapter.get_balance(
177
+ query="token-without-chain",
178
+ wallet_address="0xWallet",
179
+ )
180
+
181
+ assert success is False
182
+ assert "missing a chain id" in str(error)
@@ -46,10 +46,9 @@ success, data = await adapter.get_swap_quote(
46
46
  slippage=0.01 # 1% slippage
47
47
  )
48
48
  if success:
49
- quotes = data.get("quotes", {})
50
- best_quote = quotes.get("best_quote", {})
49
+ best_quote = data.get("best_quote", {})
51
50
  print(f"Output amount: {best_quote.get('output_amount')}")
52
- print(f"Total fee: {best_quote.get('total_fee')}")
51
+ print(f"Fee estimate: {best_quote.get('fee_estimate', {}).get('fee_total_usd')}")
53
52
  else:
54
53
  print(f"Error: {data}")
55
54
  ```
@@ -68,8 +67,7 @@ success, data = await adapter.get_best_quote(
68
67
  )
69
68
  if success:
70
69
  print(f"Best output: {data.get('output_amount')}")
71
- print(f"Gas fee: {data.get('gas_fee')}")
72
- print(f"Bridge fee: {data.get('bridge_fee')}")
70
+ print(f"Fee estimate (USD): {data.get('fee_estimate', {}).get('fee_total_usd')}")
73
71
  else:
74
72
  print(f"Error: {data}")
75
73
  ```
@@ -88,10 +86,8 @@ success, data = await adapter.calculate_swap_fees(
88
86
  if success:
89
87
  print(f"Input amount: {data.get('input_amount')}")
90
88
  print(f"Output amount: {data.get('output_amount')}")
91
- print(f"Gas fee: {data.get('gas_fee')}")
92
- print(f"Bridge fee: {data.get('bridge_fee')}")
93
- print(f"Protocol fee: {data.get('protocol_fee')}")
94
- print(f"Total fee: {data.get('total_fee')}")
89
+ print(f"Gas estimate: {data.get('gas_fee')}")
90
+ print(f"Total fee (USD): {data.get('total_fee')}")
95
91
  print(f"Price impact: {data.get('price_impact')}")
96
92
  else:
97
93
  print(f"Error: {data}")
@@ -112,7 +108,7 @@ if success:
112
108
  print(f"Best route output: {data.get('best_route', {}).get('output_amount')}")
113
109
 
114
110
  for i, route in enumerate(data.get('all_routes', [])):
115
- print(f"Route {i+1}: Output {route.get('output_amount')}, Fee {route.get('total_fee')}")
111
+ print(f\"Route {i+1}: Output {route.get('output_amount')}, Fee USD {route.get('fee_estimate', {}).get('fee_total_usd')}\")
116
112
  else:
117
113
  print(f"Error: {data}")
118
114
  ```
@@ -170,7 +166,7 @@ success, data = await adapter.get_bridge_quote(
170
166
  slippage=0.01
171
167
  )
172
168
  if success:
173
- print(f"Bridge quote received: {data.get('quotes', {}).get('best_quote', {}).get('output_amount')}")
169
+ print(f"Bridge quote received: {data.get('best_quote', {}).get('output_amount')}")
174
170
  else:
175
171
  print(f"Error: {data}")
176
172
  ```
@@ -196,8 +192,7 @@ if success:
196
192
  fastest = analysis.get("fastest")
197
193
 
198
194
  print(f"Highest output route: {highest_output.get('output_amount')}")
199
- print(f"Lowest fees route: {lowest_fees.get('total_fee')}")
200
- print(f"Fastest route: {fastest.get('estimated_time')} seconds")
195
+ print(f\"Lowest fees route (USD): {lowest_fees.get('fee_estimate', {}).get('fee_total_usd')}\")
201
196
  ```
202
197
 
203
198
  ### Fee Analysis
@@ -215,19 +210,19 @@ success, data = await adapter.calculate_swap_fees(
215
210
  if success:
216
211
  input_amount = int(data.get("input_amount", 0))
217
212
  output_amount = int(data.get("output_amount", 0))
218
- total_fee = int(data.get("total_fee", 0))
213
+ total_fee_usd = float(data.get("total_fee", 0))
219
214
 
220
215
  # Calculate effective rate
221
216
  effective_rate = (input_amount - output_amount) / input_amount
222
217
  print(f"Effective rate: {effective_rate:.4f} ({effective_rate * 100:.2f}%)")
223
- print(f"Total fees: {total_fee / 1e18:.6f} tokens")
218
+ print(f"Total fees: ${total_fee_usd:.4f}")
224
219
  ```
225
220
 
226
221
  ## API Endpoints
227
222
 
228
223
  The adapter uses the following Wayfinder API endpoints:
229
224
 
230
- - `POST /api/v1/public/quotes/` - Get swap/bridge quotes
225
+ - `GET /api/v1/blockchain/braps/quote/` - Get swap/bridge quotes
231
226
 
232
227
  ## Error Handling
233
228