wayfinder-paths 0.1.14__py3-none-any.whl → 0.1.16__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 (58) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +19 -20
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
  4. wayfinder_paths/adapters/brap_adapter/README.md +22 -19
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
  6. wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
  7. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
  8. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
  9. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  11. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  12. wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
  15. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  16. wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
  17. wayfinder_paths/core/config.py +8 -47
  18. wayfinder_paths/core/constants/base.py +0 -1
  19. wayfinder_paths/core/constants/erc20_abi.py +13 -24
  20. wayfinder_paths/core/engine/StrategyJob.py +3 -1
  21. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  22. wayfinder_paths/core/strategies/Strategy.py +22 -4
  23. wayfinder_paths/core/utils/erc20_service.py +100 -0
  24. wayfinder_paths/core/utils/evm_helpers.py +1 -8
  25. wayfinder_paths/core/utils/transaction.py +191 -0
  26. wayfinder_paths/core/utils/web3.py +66 -0
  27. wayfinder_paths/policies/erc20.py +1 -1
  28. wayfinder_paths/run_strategy.py +42 -6
  29. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
  30. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
  31. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
  32. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
  33. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
  34. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
  35. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
  37. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
  40. wayfinder_paths/templates/adapter/README.md +1 -1
  41. wayfinder_paths/templates/strategy/README.md +1 -5
  42. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
  43. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
  44. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
  45. wayfinder_paths/abis/generic/erc20.json +0 -383
  46. wayfinder_paths/core/clients/sdk_example.py +0 -125
  47. wayfinder_paths/core/engine/__init__.py +0 -5
  48. wayfinder_paths/core/services/__init__.py +0 -0
  49. wayfinder_paths/core/services/base.py +0 -130
  50. wayfinder_paths/core/services/local_evm_txn.py +0 -334
  51. wayfinder_paths/core/services/local_token_txn.py +0 -242
  52. wayfinder_paths/core/services/web3_service.py +0 -43
  53. wayfinder_paths/core/wallets/README.md +0 -88
  54. wayfinder_paths/core/wallets/WalletManager.py +0 -56
  55. wayfinder_paths/core/wallets/__init__.py +0 -7
  56. wayfinder_paths/scripts/run_strategy.py +0 -152
  57. wayfinder_paths/strategies/config.py +0 -85
  58. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
@@ -7,43 +7,42 @@ Adapter that exposes wallet, token, and pool balances backed by `WalletClient`/`
7
7
 
8
8
  ## Capabilities
9
9
 
10
- The adapter provides both wallet read and wallet transfer capabilities. Transfers are executed by leveraging the shared `DefaultWeb3Service.token_transactions` helper, but ledger recording + wallet selection now live inside the adapter.
10
+ The adapter provides both wallet read and wallet transfer capabilities. Ledger recording + wallet selection now live inside the adapter.
11
11
 
12
12
  ## Construction
13
13
 
14
14
  ```python
15
- from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
16
15
  from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
17
16
 
18
- web3_service = DefaultWeb3Service(config)
19
- balance = BalanceAdapter(config, web3_service=web3_service)
20
17
  ```
21
18
 
22
- `web3_service` is required so the adapter can share the same wallet provider (and `TokenTxn` helper) as the rest of the strategy.
23
-
24
19
  ## API surface
25
20
 
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.
21
+ ### `get_balance(token_id: str, wallet_address: str)`
28
22
 
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.
23
+ Returns the raw balance (as an integer) for a specific token on a wallet.
30
24
 
31
25
  ```python
32
- # Token balance (chain_id auto-resolved)
33
26
  success, balance = await balance.get_balance(
34
- query="usd-coin-base",
27
+ token_id="usd-coin-base",
35
28
  wallet_address=config["main_wallet"]["address"],
36
29
  )
30
+ ```
31
+
32
+ ### `get_pool_balance(pool_address: str, chain_id: int, user_address: str)`
37
33
 
38
- # Pool balance (chain_id required)
39
- success, pool_balance = await balance.get_balance(
40
- query="0xPool...",
41
- wallet_address=config["strategy_wallet"]["address"],
34
+ Fetches the amount supplied to a specific pool, using the `/wallets/pool-balance` endpoint.
35
+
36
+ ```python
37
+ success, amount = await balance.get_pool_balance(
38
+ pool_address="0xPool",
42
39
  chain_id=8453,
40
+ user_address=config["strategy_wallet"]["address"],
43
41
  )
44
42
  ```
45
43
 
46
44
  ### `move_from_main_wallet_to_strategy_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
45
+
47
46
  Sends the specified token from the configured `main_wallet` to the strategy wallet, records the ledger deposit (unless `skip_ledger=True`), and returns the `(success, tx_result)` tuple from the underlying send helper.
48
47
 
49
48
  ```python
@@ -55,6 +54,7 @@ success, tx = await balance.move_from_main_wallet_to_strategy_wallet(
55
54
  ```
56
55
 
57
56
  ### `move_from_strategy_wallet_to_main_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
57
+
58
58
  Mirrors the previous method but withdraws from the strategy wallet back to the main wallet while recording a ledger withdrawal entry.
59
59
 
60
60
  ```python
@@ -73,16 +73,15 @@ All methods return `(success: bool, payload: Any)` tuples. On failure the payloa
73
73
  class MyStrategy(Strategy):
74
74
  def __init__(self, config):
75
75
  super().__init__()
76
- web3_service = DefaultWeb3Service(config)
77
- balance_adapter = BalanceAdapter(config, web3_service=web3_service)
76
+ balance_adapter = BalanceAdapter(config)
78
77
  self.register_adapters([balance_adapter])
79
78
  self.balance_adapter = balance_adapter
80
79
 
81
80
  async def _status(self):
82
- success, pool_balance = await self.balance_adapter.get_balance(
83
- query=self.current_pool["address"],
84
- wallet_address=self.config["strategy_wallet"]["address"],
81
+ success, pool_balance = await self.balance_adapter.get_pool_balance(
82
+ pool_address=self.current_pool["address"],
85
83
  chain_id=self.current_pool["chain"]["id"],
84
+ user_address=self.config["strategy_wallet"]["address"],
86
85
  )
87
86
  return {"portfolio_value": float(pool_balance or 0), ...}
88
87
  ```
@@ -5,9 +5,9 @@ from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
5
5
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
6
6
  from wayfinder_paths.core.clients.TokenClient import TokenClient
7
7
  from wayfinder_paths.core.clients.WalletClient import WalletClient
8
- from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
9
- from wayfinder_paths.core.services.base import Web3Service
8
+ from wayfinder_paths.core.utils.erc20_service import build_send_transaction
10
9
  from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
10
+ from wayfinder_paths.core.utils.transaction import send_transaction
11
11
 
12
12
 
13
13
  class BalanceAdapter(BaseAdapter):
@@ -16,17 +16,19 @@ class BalanceAdapter(BaseAdapter):
16
16
  def __init__(
17
17
  self,
18
18
  config: dict[str, Any],
19
- web3_service: Web3Service,
19
+ simulation: bool = False,
20
+ main_wallet_signing_callback=None,
21
+ strategy_wallet_signing_callback=None,
20
22
  ):
21
23
  super().__init__("balance", config)
24
+ self.simulation = simulation
25
+ self.main_wallet_signing_callback = main_wallet_signing_callback
26
+ self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
22
27
  self.wallet_client = WalletClient()
23
28
  self.token_client = TokenClient()
24
29
  self.token_adapter = TokenAdapter()
25
30
  self.ledger_adapter = LedgerAdapter()
26
31
 
27
- self.wallet_provider = web3_service.evm_transactions
28
- self.token_transactions = web3_service.token_transactions
29
-
30
32
  def _parse_balance(self, raw: Any) -> int:
31
33
  """Parse balance value to integer, handling various formats."""
32
34
  if raw is None:
@@ -42,15 +44,23 @@ class BalanceAdapter(BaseAdapter):
42
44
  async def get_balance(
43
45
  self,
44
46
  *,
45
- query: str | dict[str, Any],
47
+ query: str | dict[str, Any] | None = None,
48
+ token_id: str | None = None,
46
49
  wallet_address: str,
47
50
  chain_id: int | None = None,
48
51
  ) -> tuple[bool, str | int]:
49
52
  """Get token or pool balance for a wallet.
50
53
 
51
54
  query: token_id/address string or a dict with a "token_id" key.
55
+ token_id: alternative to query for convenience.
52
56
  """
53
- resolved = query if isinstance(query, str) else (query or {}).get("token_id")
57
+ # Support both query= and token_id= for caller convenience
58
+ effective_query = query if query is not None else token_id
59
+ resolved = (
60
+ effective_query
61
+ if isinstance(effective_query, str)
62
+ else (effective_query or {}).get("token_id")
63
+ )
54
64
  if not resolved:
55
65
  return (False, "missing query")
56
66
  try:
@@ -116,6 +126,50 @@ class BalanceAdapter(BaseAdapter):
116
126
  skip_ledger=skip_ledger,
117
127
  )
118
128
 
129
+ async def send_to_address(
130
+ self,
131
+ token_id: str,
132
+ amount: int,
133
+ from_wallet: dict[str, Any] | None,
134
+ to_address: str,
135
+ signing_callback=None,
136
+ skip_ledger: bool = True,
137
+ ) -> tuple[bool, Any]:
138
+ """Send tokens from a wallet to an arbitrary address (e.g., bridge contract)."""
139
+ from_address = self._wallet_address(from_wallet)
140
+ if not from_address:
141
+ return False, "from_wallet missing or invalid"
142
+
143
+ if not to_address:
144
+ return False, "to_address is required"
145
+
146
+ token_info = await self.token_client.get_token_details(token_id)
147
+ if not token_info:
148
+ return False, f"Token not found: {token_id}"
149
+
150
+ chain_id = resolve_chain_id(token_info, self.logger)
151
+ if chain_id is None:
152
+ return False, f"Token {token_id} is missing chain_id"
153
+
154
+ token_address = token_info.get("address")
155
+
156
+ tx = await build_send_transaction(
157
+ from_address=from_address,
158
+ to_address=to_address,
159
+ token_address=token_address,
160
+ chain_id=chain_id,
161
+ amount=int(amount),
162
+ )
163
+
164
+ if self.simulation:
165
+ return True, {"simulation": tx}
166
+
167
+ if not signing_callback:
168
+ return False, "signing_callback is required"
169
+
170
+ tx_hash = await send_transaction(tx, signing_callback)
171
+ return True, tx_hash
172
+
119
173
  async def _move_between_wallets(
120
174
  self,
121
175
  *,
@@ -128,9 +182,6 @@ class BalanceAdapter(BaseAdapter):
128
182
  strategy_name: str,
129
183
  skip_ledger: bool,
130
184
  ) -> tuple[bool, Any]:
131
- if self.token_transactions is None:
132
- return False, "Token transaction service not configured"
133
-
134
185
  from_address = self._wallet_address(from_wallet)
135
186
  to_address = self._wallet_address(to_wallet)
136
187
  if not from_address or not to_address:
@@ -140,20 +191,21 @@ class BalanceAdapter(BaseAdapter):
140
191
  if not token_info:
141
192
  return False, f"Token not found: {token_id}"
142
193
 
143
- build_success, tx_data = await self.token_transactions.build_send(
144
- token_id=token_id,
145
- amount=amount,
194
+ chain_id = resolve_chain_id(token_info, self.logger)
195
+ if chain_id is None:
196
+ return False, f"Token {token_id} is missing chain_id"
197
+
198
+ decimals = token_info.get("decimals", 18)
199
+ raw_amount = int(amount * (10**decimals))
200
+
201
+ tx = await build_send_transaction(
146
202
  from_address=from_address,
147
203
  to_address=to_address,
148
- token_info=token_info,
149
- )
150
- if not build_success:
151
- return False, tx_data
152
-
153
- tx = tx_data
154
- broadcast_result = await self.wallet_provider.broadcast_transaction(
155
- tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
204
+ token_address=token_info.get("address"),
205
+ chain_id=chain_id,
206
+ amount=raw_amount,
156
207
  )
208
+ broadcast_result = await self._send_tx(tx, from_address)
157
209
 
158
210
  if broadcast_result[0] and not skip_ledger and ledger_method is not None:
159
211
  wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
@@ -167,6 +219,23 @@ class BalanceAdapter(BaseAdapter):
167
219
 
168
220
  return broadcast_result
169
221
 
222
+ async def _send_tx(self, tx: dict[str, Any], from_address: str) -> tuple[bool, Any]:
223
+ """Send transaction with simulation check, using appropriate signing callback."""
224
+ if self.simulation:
225
+ return True, {"simulation": tx}
226
+
227
+ # Choose callback based on which wallet is sending
228
+ main_wallet = self.config.get("main_wallet") or {}
229
+ main_addr = main_wallet.get("address", "").lower()
230
+
231
+ if from_address.lower() == main_addr:
232
+ callback = self.main_wallet_signing_callback
233
+ else:
234
+ callback = self.strategy_wallet_signing_callback
235
+
236
+ txn_hash = await send_transaction(tx, callback)
237
+ return True, txn_hash
238
+
170
239
  async def _record_ledger_entry(
171
240
  self,
172
241
  *,
@@ -21,13 +21,7 @@ class TestBalanceAdapter:
21
21
  return mock_client
22
22
 
23
23
  @pytest.fixture
24
- def mock_web3_service(self):
25
- """Mock TokenClient for testing"""
26
- mock_client = AsyncMock()
27
- return mock_client
28
-
29
- @pytest.fixture
30
- def adapter(self, mock_wallet_client, mock_token_client, mock_web3_service):
24
+ def adapter(self, mock_wallet_client, mock_token_client):
31
25
  """Create a BalanceAdapter instance with mocked clients for testing"""
32
26
  with (
33
27
  patch(
@@ -39,7 +33,7 @@ class TestBalanceAdapter:
39
33
  return_value=mock_token_client,
40
34
  ),
41
35
  ):
42
- return BalanceAdapter(config={}, web3_service=mock_web3_service)
36
+ return BalanceAdapter(config={})
43
37
 
44
38
  @pytest.mark.asyncio
45
39
  async def test_health_check(self, adapter):
@@ -79,7 +73,7 @@ class TestBalanceAdapter:
79
73
  wallet_address="0xWallet",
80
74
  )
81
75
 
82
- assert success is True
76
+ assert success
83
77
  assert balance == 1000000
84
78
  mock_token_client.get_token_details.assert_called_once_with("usd-coin-base")
85
79
  mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
@@ -108,7 +102,7 @@ class TestBalanceAdapter:
108
102
  query={"token_id": "wsteth-base"},
109
103
  wallet_address="0x123",
110
104
  )
111
- assert success is True
105
+ assert success
112
106
  assert balance == 3000000
113
107
  mock_token_client.get_token_details.assert_called_once_with("wsteth-base")
114
108
  mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
@@ -140,7 +134,7 @@ class TestBalanceAdapter:
140
134
  chain_id=8453,
141
135
  )
142
136
 
143
- assert success is True
137
+ assert success
144
138
  assert balance == 5000000
145
139
  mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
146
140
  wallet_address="0xWallet",
@@ -13,9 +13,8 @@ A Wayfinder adapter that provides high-level operations for cross-chain swaps an
13
13
 
14
14
  ## Configuration
15
15
 
16
- The adapter uses the BRAPClient which automatically handles authentication and API configuration through the Wayfinder settings. Pass a `Web3Service` instance so it can broadcast transactions and reuse the shared `LocalTokenTxnService` for approvals.
17
-
18
16
  The BRAPClient will automatically:
17
+
19
18
  - Use the WAYFINDER_API_URL from settings
20
19
  - Handle authentication via config.json
21
20
  - Manage token refresh and retry logic
@@ -25,11 +24,9 @@ The BRAPClient will automatically:
25
24
  ### Initialize the Adapter
26
25
 
27
26
  ```python
28
- from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
29
27
  from wayfinder_paths.adapters.brap_adapter.adapter import BRAPAdapter
30
28
 
31
- web3_service = DefaultWeb3Service(config)
32
- adapter = BRAPAdapter(web3_service=web3_service)
29
+ adapter = BRAPAdapter()
33
30
  ```
34
31
 
35
32
  ### Get Swap Quote
@@ -46,9 +43,10 @@ success, data = await adapter.get_swap_quote(
46
43
  slippage=0.01 # 1% slippage
47
44
  )
48
45
  if success:
49
- best_quote = data.get("best_quote", {})
46
+ quotes = data.get("quotes", {})
47
+ best_quote = quotes.get("best_quote", {})
50
48
  print(f"Output amount: {best_quote.get('output_amount')}")
51
- print(f"Fee estimate: {best_quote.get('fee_estimate', {}).get('fee_total_usd')}")
49
+ print(f"Total fee: {best_quote.get('total_fee')}")
52
50
  else:
53
51
  print(f"Error: {data}")
54
52
  ```
@@ -67,7 +65,8 @@ success, data = await adapter.get_best_quote(
67
65
  )
68
66
  if success:
69
67
  print(f"Best output: {data.get('output_amount')}")
70
- print(f"Fee estimate (USD): {data.get('fee_estimate', {}).get('fee_total_usd')}")
68
+ print(f"Gas fee: {data.get('gas_fee')}")
69
+ print(f"Bridge fee: {data.get('bridge_fee')}")
71
70
  else:
72
71
  print(f"Error: {data}")
73
72
  ```
@@ -86,8 +85,10 @@ success, data = await adapter.calculate_swap_fees(
86
85
  if success:
87
86
  print(f"Input amount: {data.get('input_amount')}")
88
87
  print(f"Output amount: {data.get('output_amount')}")
89
- print(f"Gas estimate: {data.get('gas_fee')}")
90
- print(f"Total fee (USD): {data.get('total_fee')}")
88
+ print(f"Gas fee: {data.get('gas_fee')}")
89
+ print(f"Bridge fee: {data.get('bridge_fee')}")
90
+ print(f"Protocol fee: {data.get('protocol_fee')}")
91
+ print(f"Total fee: {data.get('total_fee')}")
91
92
  print(f"Price impact: {data.get('price_impact')}")
92
93
  else:
93
94
  print(f"Error: {data}")
@@ -106,9 +107,9 @@ success, data = await adapter.compare_routes(
106
107
  if success:
107
108
  print(f"Total routes available: {data.get('total_routes')}")
108
109
  print(f"Best route output: {data.get('best_route', {}).get('output_amount')}")
109
-
110
+
110
111
  for i, route in enumerate(data.get('all_routes', [])):
111
- print(f\"Route {i+1}: Output {route.get('output_amount')}, Fee USD {route.get('fee_estimate', {}).get('fee_total_usd')}\")
112
+ print(f"Route {i+1}: Output {route.get('output_amount')}, Fee {route.get('total_fee')}")
112
113
  else:
113
114
  print(f"Error: {data}")
114
115
  ```
@@ -166,7 +167,7 @@ success, data = await adapter.get_bridge_quote(
166
167
  slippage=0.01
167
168
  )
168
169
  if success:
169
- print(f"Bridge quote received: {data.get('best_quote', {}).get('output_amount')}")
170
+ print(f"Bridge quote received: {data.get('quotes', {}).get('best_quote', {}).get('output_amount')}")
170
171
  else:
171
172
  print(f"Error: {data}")
172
173
  ```
@@ -190,9 +191,10 @@ if success:
190
191
  highest_output = analysis.get("highest_output")
191
192
  lowest_fees = analysis.get("lowest_fees")
192
193
  fastest = analysis.get("fastest")
193
-
194
+
194
195
  print(f"Highest output route: {highest_output.get('output_amount')}")
195
- print(f\"Lowest fees route (USD): {lowest_fees.get('fee_estimate', {}).get('fee_total_usd')}\")
196
+ print(f"Lowest fees route: {lowest_fees.get('total_fee')}")
197
+ print(f"Fastest route: {fastest.get('estimated_time')} seconds")
196
198
  ```
197
199
 
198
200
  ### Fee Analysis
@@ -210,23 +212,24 @@ success, data = await adapter.calculate_swap_fees(
210
212
  if success:
211
213
  input_amount = int(data.get("input_amount", 0))
212
214
  output_amount = int(data.get("output_amount", 0))
213
- total_fee_usd = float(data.get("total_fee", 0))
214
-
215
+ total_fee = int(data.get("total_fee", 0))
216
+
215
217
  # Calculate effective rate
216
218
  effective_rate = (input_amount - output_amount) / input_amount
217
219
  print(f"Effective rate: {effective_rate:.4f} ({effective_rate * 100:.2f}%)")
218
- print(f"Total fees: ${total_fee_usd:.4f}")
220
+ print(f"Total fees: {total_fee / 1e18:.6f} tokens")
219
221
  ```
220
222
 
221
223
  ## API Endpoints
222
224
 
223
225
  The adapter uses the following Wayfinder API endpoints:
224
226
 
225
- - `GET /api/v1/blockchain/braps/quote/` - Get swap/bridge quotes
227
+ - `POST /api/v1/public/quotes/` - Get swap/bridge quotes
226
228
 
227
229
  ## Error Handling
228
230
 
229
231
  All methods return a tuple of `(success: bool, data: Any)` where:
232
+
230
233
  - `success` is `True` if the operation succeeded
231
234
  - `data` contains the response data on success or error message on failure
232
235
 
@@ -15,8 +15,11 @@ from wayfinder_paths.core.clients.BRAPClient import (
15
15
  from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
16
16
  from wayfinder_paths.core.clients.TokenClient import TokenClient
17
17
  from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
18
- from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
19
- from wayfinder_paths.core.services.base import Web3Service
18
+ from wayfinder_paths.core.utils.erc20_service import (
19
+ build_approve_transaction,
20
+ get_token_allowance,
21
+ )
22
+ from wayfinder_paths.core.utils.transaction import send_transaction
20
23
 
21
24
  _NEEDS_CLEAR_APPROVAL = {
22
25
  (1, "0xdac17f958d2ee523a2206206994597c13d831ec7"),
@@ -41,17 +44,16 @@ class BRAPAdapter(BaseAdapter):
41
44
  def __init__(
42
45
  self,
43
46
  config: dict[str, Any] | None = None,
44
- *,
45
- web3_service: Web3Service,
47
+ simulation: bool = False,
48
+ strategy_wallet_signing_callback=None,
46
49
  ):
47
50
  super().__init__("brap_adapter", config)
51
+ self.simulation = simulation
52
+ self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
48
53
  self.brap_client = BRAPClient()
49
54
  self.token_client = TokenClient()
50
55
  self.token_adapter = TokenAdapter()
51
56
  self.ledger_adapter = LedgerAdapter()
52
- self.web3_service = web3_service
53
- self.wallet_provider = web3_service.evm_transactions
54
- self.token_transactions = web3_service.token_transactions
55
57
 
56
58
  async def get_swap_quote(
57
59
  self,
@@ -438,11 +440,37 @@ class BRAPAdapter(BaseAdapter):
438
440
  if not transaction or not transaction.get("data"):
439
441
  return (False, "Quote missing calldata")
440
442
  transaction["chainId"] = chain_id
443
+ if "value" in transaction:
444
+ transaction["value"] = int(transaction["value"])
441
445
  # Always set the sender to the strategy wallet for broadcast.
442
446
  # (Calldata may include either "from" or "from_address" depending on provider.)
443
447
  transaction["from"] = to_checksum_address(from_address)
444
448
 
445
- spender = transaction.get("to")
449
+ def _as_address(value: Any) -> str | None:
450
+ if not isinstance(value, str):
451
+ return None
452
+ v = value.strip()
453
+ if (
454
+ v.startswith("0x")
455
+ and len(v) == 42
456
+ and v.lower() != ZERO_ADDRESS.lower()
457
+ ):
458
+ return v
459
+ return None
460
+
461
+ spender = (
462
+ _as_address(transaction.get("allowanceTarget"))
463
+ or _as_address(transaction.get("allowance_target"))
464
+ or _as_address(transaction.get("approvalAddress"))
465
+ or _as_address(transaction.get("approval_address"))
466
+ or _as_address(transaction.get("spender"))
467
+ or _as_address(quote.get("allowanceTarget"))
468
+ or _as_address(quote.get("allowance_target"))
469
+ or _as_address(quote.get("approvalAddress"))
470
+ or _as_address(quote.get("approval_address"))
471
+ or _as_address(quote.get("spender"))
472
+ or _as_address(transaction.get("to"))
473
+ )
446
474
  approve_amount = (
447
475
  quote.get("input_amount")
448
476
  or quote.get("inputAmount")
@@ -466,16 +494,40 @@ class BRAPAdapter(BaseAdapter):
466
494
  if not approve_success:
467
495
  return (False, approve_response)
468
496
 
469
- broadcast_success, broadcast_response = await self._broadcast_transaction(
470
- transaction
471
- )
497
+ broadcast_success, broadcast_response = await self._send_tx(transaction)
472
498
  self.logger.info(
473
499
  f"Swap broadcast result: success={broadcast_success}, "
474
500
  f"response={broadcast_response}"
475
501
  )
502
+ # Log only key fields to avoid spamming raw HexBytes logs
503
+ if isinstance(broadcast_response, dict):
504
+ tx_hash_log = broadcast_response.get("tx_hash", "unknown")
505
+ block_log = broadcast_response.get("block_number", "unknown")
506
+ status_log = (
507
+ broadcast_response.get("receipt", {}).get("status", "unknown")
508
+ if isinstance(broadcast_response.get("receipt"), dict)
509
+ else "unknown"
510
+ )
511
+ self.logger.info(
512
+ f"Swap broadcast: success={broadcast_success}, tx={tx_hash_log}, block={block_log}, status={status_log}"
513
+ )
514
+ else:
515
+ self.logger.info(f"Swap broadcast: success={broadcast_success}")
476
516
  if not broadcast_success:
477
517
  return (False, broadcast_response)
478
518
 
519
+ tx_hash = None
520
+ block_number = None
521
+ confirmations = None
522
+ confirmed_block_number = None
523
+ if isinstance(broadcast_response, dict):
524
+ tx_hash = broadcast_response.get("tx_hash") or broadcast_response.get(
525
+ "transaction_hash"
526
+ )
527
+ block_number = broadcast_response.get("block_number")
528
+ confirmations = broadcast_response.get("confirmations")
529
+ confirmed_block_number = broadcast_response.get("confirmed_block_number")
530
+
479
531
  # Record the swap operation in ledger - but don't let ledger errors fail the swap
480
532
  # since the on-chain transaction already succeeded
481
533
  try:
@@ -491,15 +543,20 @@ class BRAPAdapter(BaseAdapter):
491
543
  self.logger.warning(
492
544
  f"Ledger recording failed (swap succeeded on-chain): {e}"
493
545
  )
494
- # Return the quote with output amount so caller can proceed
495
- ledger_record = {
496
- "to_amount": quote.get("output_amount"),
497
- "from_amount": quote.get("input_amount"),
498
- "tx_hash": broadcast_response.get("tx_hash")
499
- if isinstance(broadcast_response, dict)
500
- else None,
501
- }
502
- return (True, ledger_record)
546
+ ledger_record = {}
547
+
548
+ result_payload: dict[str, Any] = {
549
+ "from_amount": quote.get("input_amount"),
550
+ "to_amount": quote.get("output_amount"),
551
+ "tx_hash": tx_hash,
552
+ "block_number": block_number,
553
+ "confirmations": confirmations,
554
+ "confirmed_block_number": confirmed_block_number,
555
+ }
556
+ if isinstance(ledger_record, dict):
557
+ result_payload.update(ledger_record)
558
+
559
+ return (True, result_payload)
503
560
 
504
561
  async def get_bridge_quote(
505
562
  self,
@@ -678,46 +735,39 @@ class BRAPAdapter(BaseAdapter):
678
735
  spender_checksum = to_checksum_address(spender_address)
679
736
 
680
737
  if (chain_id, token_checksum.lower()) in _NEEDS_CLEAR_APPROVAL:
681
- allowance = await self.token_transactions.read_erc20_allowance(
682
- {"id": chain_id},
738
+ allowance = await get_token_allowance(
683
739
  token_checksum,
740
+ chain_id,
684
741
  owner_checksum,
685
742
  spender_checksum,
686
743
  )
687
- if allowance.get("allowance", 0) > 0:
688
- clear_success, clear_tx = self.token_transactions.build_erc20_approve(
744
+ if allowance > 0:
745
+ clear_tx = await build_approve_transaction(
746
+ from_address=owner_checksum,
689
747
  chain_id=chain_id,
690
748
  token_address=token_checksum,
691
- from_address=owner_checksum,
692
- spender=spender_checksum,
749
+ spender_address=spender_checksum,
693
750
  amount=0,
694
751
  )
695
- if not clear_success:
696
- return False, clear_tx
697
- clear_result = await self._broadcast_transaction(clear_tx)
752
+ clear_result = await self._send_tx(clear_tx)
698
753
  if not clear_result[0]:
699
754
  return clear_result
700
755
 
701
- build_success, approve_tx = self.token_transactions.build_erc20_approve(
756
+ approve_tx = await build_approve_transaction(
757
+ from_address=owner_checksum,
702
758
  chain_id=chain_id,
703
759
  token_address=token_checksum,
704
- from_address=owner_checksum,
705
- spender=spender_checksum,
760
+ spender_address=spender_checksum,
706
761
  amount=int(amount),
707
762
  )
708
- if not build_success:
709
- return False, approve_tx
710
- return await self._broadcast_transaction(approve_tx, confirmations=2)
711
-
712
- async def _broadcast_transaction(
713
- self, transaction: dict[str, Any], confirmations: int = 0
714
- ) -> tuple[bool, Any]:
715
- return await self.wallet_provider.broadcast_transaction(
716
- transaction,
717
- wait_for_receipt=True,
718
- timeout=DEFAULT_TRANSACTION_TIMEOUT,
719
- confirmations=confirmations,
720
- )
763
+ return await self._send_tx(approve_tx)
764
+
765
+ async def _send_tx(self, tx: dict[str, Any]) -> tuple[bool, Any]:
766
+ """Send transaction with simulation check."""
767
+ if self.simulation:
768
+ return True, {"simulation": tx}
769
+ txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
770
+ return True, txn_hash
721
771
 
722
772
  async def _record_swap_operation(
723
773
  self,