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
@@ -4,7 +4,7 @@ Protocol definitions for API clients.
4
4
  These protocols define the interface that all client implementations must satisfy.
5
5
  When used as an SDK, users can provide custom implementations that match these protocols.
6
6
 
7
- Note: AuthClient is excluded as SDK users handle their own authentication.
7
+ Note: Authentication is handled via X-API-KEY header in WayfinderClient base class.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -12,19 +12,19 @@ from __future__ import annotations
12
12
  from typing import TYPE_CHECKING, Any, Protocol
13
13
 
14
14
  if TYPE_CHECKING:
15
- from wayfinder_paths.core.clients.BRAPClient import BRAPQuote
15
+ from wayfinder_paths.core.clients.BRAPClient import BRAPQuoteResponse
16
16
  from wayfinder_paths.core.clients.HyperlendClient import (
17
17
  AssetsView,
18
18
  LendRateHistory,
19
19
  MarketEntry,
20
- StableMarket,
20
+ StableMarketsHeadroomResponse,
21
21
  )
22
22
  from wayfinder_paths.core.clients.LedgerClient import (
23
23
  StrategyTransactionList,
24
24
  TransactionRecord,
25
25
  )
26
26
  from wayfinder_paths.core.clients.PoolClient import (
27
- LlamaMatch,
27
+ LlamaMatchesResponse,
28
28
  PoolList,
29
29
  )
30
30
  from wayfinder_paths.core.clients.TokenClient import (
@@ -32,8 +32,7 @@ if TYPE_CHECKING:
32
32
  TokenDetails,
33
33
  )
34
34
  from wayfinder_paths.core.clients.WalletClient import (
35
- PoolBalance,
36
- TokenBalance,
35
+ AddressBalance,
37
36
  )
38
37
 
39
38
 
@@ -41,7 +40,10 @@ class TokenClientProtocol(Protocol):
41
40
  """Protocol for token-related operations"""
42
41
 
43
42
  async def get_token_details(
44
- self, token_id: str, force_refresh: bool = False
43
+ self,
44
+ query: str,
45
+ market_data: bool = True,
46
+ chain_id: int | None = None,
45
47
  ) -> TokenDetails:
46
48
  """Get token data including price from the token-details endpoint"""
47
49
  ...
@@ -57,19 +59,16 @@ class HyperlendClientProtocol(Protocol):
57
59
  async def get_stable_markets(
58
60
  self,
59
61
  *,
60
- chain_id: int,
61
62
  required_underlying_tokens: float | None = None,
62
63
  buffer_bps: int | None = None,
63
64
  min_buffer_tokens: float | None = None,
64
- is_stable_symbol: bool | None = None,
65
- ) -> list[StableMarket]:
66
- """Fetch stable markets from Hyperlend"""
65
+ ) -> StableMarketsHeadroomResponse:
66
+ """Fetch stable markets headroom from Hyperlend"""
67
67
  ...
68
68
 
69
69
  async def get_assets_view(
70
70
  self,
71
71
  *,
72
- chain_id: int,
73
72
  user_address: str,
74
73
  ) -> AssetsView:
75
74
  """Fetch assets view for a user address from Hyperlend"""
@@ -78,8 +77,7 @@ class HyperlendClientProtocol(Protocol):
78
77
  async def get_market_entry(
79
78
  self,
80
79
  *,
81
- chain_id: int,
82
- token_address: str,
80
+ token: str,
83
81
  ) -> MarketEntry:
84
82
  """Fetch market entry from Hyperlend"""
85
83
  ...
@@ -87,9 +85,9 @@ class HyperlendClientProtocol(Protocol):
87
85
  async def get_lend_rate_history(
88
86
  self,
89
87
  *,
90
- chain_id: int,
91
- token_address: str,
88
+ token: str,
92
89
  lookback_hours: int,
90
+ force_refresh: bool | None = None,
93
91
  ) -> LendRateHistory:
94
92
  """Fetch lend rate history from Hyperlend"""
95
93
  ...
@@ -161,25 +159,14 @@ class LedgerClientProtocol(Protocol):
161
159
  class WalletClientProtocol(Protocol):
162
160
  """Protocol for wallet-related operations"""
163
161
 
164
- async def get_token_balance_for_wallet(
162
+ async def get_token_balance_for_address(
165
163
  self,
166
164
  *,
167
- token_id: str,
168
165
  wallet_address: str,
169
- human_readable: bool = True,
170
- ) -> TokenBalance:
171
- """Fetch a single token balance for an explicit wallet address"""
172
- ...
173
-
174
- async def get_pool_balance_for_wallet(
175
- self,
176
- *,
177
- pool_address: str,
178
- chain_id: int,
179
- user_address: str,
180
- human_readable: bool = True,
181
- ) -> PoolBalance:
182
- """Fetch a wallet's LP/share balance for a given pool address and chain"""
166
+ query: str,
167
+ chain_id: int | None = None,
168
+ ) -> AddressBalance:
169
+ """Fetch a balance for an address + chain + query (supports compound query formats)"""
183
170
  ...
184
171
 
185
172
 
@@ -189,13 +176,18 @@ class PoolClientProtocol(Protocol):
189
176
  async def get_pools_by_ids(
190
177
  self,
191
178
  *,
192
- pool_ids: str,
179
+ pool_ids: list[str] | str,
193
180
  ) -> PoolList:
194
- """Fetch pools by comma-separated pool ids"""
181
+ """Fetch pools by pool IDs (list or comma-separated string)"""
195
182
  ...
196
183
 
197
- async def get_pools(self) -> dict[str, LlamaMatch]:
198
- """Fetch Llama matches for pools"""
184
+ async def get_pools(
185
+ self,
186
+ *,
187
+ chain_id: int | None = None,
188
+ project: str | None = None,
189
+ ) -> LlamaMatchesResponse:
190
+ """Fetch pools (optionally filtered by chain_id and project)"""
199
191
  ...
200
192
 
201
193
 
@@ -205,16 +197,13 @@ class BRAPClientProtocol(Protocol):
205
197
  async def get_quote(
206
198
  self,
207
199
  *,
208
- from_token_address: str,
209
- to_token_address: str,
210
- from_chain_id: int,
211
- to_chain_id: int,
212
- from_address: str,
213
- to_address: str,
214
- amount1: str,
215
- slippage: float | None = None,
216
- wayfinder_fee: float | None = None,
217
- ) -> BRAPQuote:
200
+ from_token: str,
201
+ to_token: str,
202
+ from_chain: int,
203
+ to_chain: int,
204
+ from_wallet: str,
205
+ from_amount: str,
206
+ ) -> BRAPQuoteResponse:
218
207
  """Get a quote for a bridge/swap operation"""
219
208
  ...
220
209
 
@@ -38,59 +38,74 @@ class MockHyperlendClient:
38
38
  async def get_stable_markets(
39
39
  self,
40
40
  *,
41
- chain_id: int,
42
41
  required_underlying_tokens: float | None = None,
43
42
  buffer_bps: int | None = None,
44
43
  min_buffer_tokens: float | None = None,
45
- is_stable_symbol: bool | None = None,
46
44
  ) -> dict[str, Any]:
47
45
  return {
48
- "markets": [
49
- {
50
- "chain_id": chain_id,
51
- "token_address": "0xMockToken",
46
+ "markets": {
47
+ "0xMockToken": {
52
48
  "symbol": "USDC",
53
- "lend_rate": 0.05,
54
- "available_liquidity": 1000000.0,
49
+ "symbol_canonical": "usdc",
50
+ "display_symbol": "USDC",
51
+ "reserve": {},
52
+ "decimals": 6,
53
+ "headroom": 1000000000000,
54
+ "supply_cap": 5000000000000,
55
55
  }
56
- ]
56
+ },
57
+ "notes": [],
57
58
  }
58
59
 
59
60
  async def get_assets_view(
60
61
  self,
61
62
  *,
62
- chain_id: int,
63
63
  user_address: str,
64
64
  ) -> dict[str, Any]:
65
65
  return {
66
- "user_address": user_address,
67
- "chain_id": chain_id,
66
+ "block_number": 12345,
67
+ "user": user_address,
68
+ "native_balance_wei": 0,
69
+ "native_balance": 0.0,
68
70
  "assets": [],
71
+ "account_data": {
72
+ "total_collateral_base": 0,
73
+ "total_debt_base": 0,
74
+ "available_borrows_base": 0,
75
+ "current_liquidation_threshold": 0,
76
+ "ltv": 0,
77
+ "health_factor_wad": 0,
78
+ "health_factor": 0.0,
79
+ },
80
+ "base_currency_info": {
81
+ "marketReferenceCurrencyUnit": 100000000,
82
+ "marketReferenceCurrencyPriceInUsd": 100000000,
83
+ "networkBaseTokenPriceInUsd": 0,
84
+ "networkBaseTokenPriceDecimals": 8,
85
+ },
69
86
  }
70
87
 
71
88
  async def get_market_entry(
72
89
  self,
73
90
  *,
74
- chain_id: int,
75
- token_address: str,
91
+ token: str,
76
92
  ) -> dict[str, Any]:
77
93
  return {
78
- "chain_id": chain_id,
79
- "token_address": token_address,
80
- "market_data": {},
94
+ "symbol": "USDC",
95
+ "symbol_canonical": "usdc",
96
+ "display_symbol": "USDC",
97
+ "reserve": {},
81
98
  }
82
99
 
83
100
  async def get_lend_rate_history(
84
101
  self,
85
102
  *,
86
- chain_id: int,
87
- token_address: str,
103
+ token: str,
88
104
  lookback_hours: int,
105
+ force_refresh: bool | None = None,
89
106
  ) -> dict[str, Any]:
90
107
  return {
91
- "chain_id": chain_id,
92
- "token_address": token_address,
93
- "rates": [],
108
+ "history": [],
94
109
  }
95
110
 
96
111
 
@@ -83,17 +83,6 @@ ERC20_ABI = [
83
83
  },
84
84
  ]
85
85
 
86
- # Minimal ABI for specific use cases (e.g., when you only need certain functions)
87
- ERC20_MINIMAL_ABI = [
88
- {
89
- "constant": True,
90
- "inputs": [{"name": "account", "type": "address"}],
91
- "name": "balanceOf",
92
- "outputs": [{"name": "", "type": "uint256"}],
93
- "type": "function",
94
- }
95
- ]
96
-
97
86
  ERC20_APPROVAL_ABI = [
98
87
  {
99
88
  "constant": True,
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import os
3
2
  from typing import Any
4
3
 
5
4
  from loguru import logger
@@ -16,7 +15,6 @@ class StrategyJob:
16
15
  config: StrategyJobConfig,
17
16
  clients: dict[str, Any] | None = None,
18
17
  skip_auth: bool = False,
19
- api_key: str | None = None,
20
18
  ):
21
19
  """
22
20
  Initialize a StrategyJob.
@@ -26,16 +24,12 @@ class StrategyJob:
26
24
  config: Strategy job configuration.
27
25
  clients: Optional dict of pre-instantiated clients to inject directly.
28
26
  skip_auth: If True, skips authentication (for SDK usage).
29
- api_key: Optional API key for service account authentication.
30
- If provided, will be passed to ClientManager and strategy.
31
27
  """
32
28
  self.strategy = strategy
33
29
  self.config = config
34
30
 
35
31
  self.job_id = strategy.name or "unknown"
36
- self.clients = ClientManager(
37
- clients=clients, skip_auth=skip_auth, api_key=api_key
38
- )
32
+ self.clients = ClientManager(clients=clients, skip_auth=skip_auth)
39
33
 
40
34
  def _setup_strategy(self):
41
35
  """Setup the strategy instance"""
@@ -44,23 +38,6 @@ class StrategyJob:
44
38
 
45
39
  self.strategy.log = self.log
46
40
 
47
- def _is_using_api_key(self) -> bool:
48
- """Check if API key authentication is being used."""
49
- if self.clients._api_key:
50
- return True
51
-
52
- if self.clients.auth:
53
- try:
54
- creds = self.clients.auth._load_config_credentials()
55
- if creds.get("api_key"):
56
- return True
57
- if os.getenv("WAYFINDER_API_KEY"):
58
- return True
59
- except Exception:
60
- pass
61
-
62
- return False
63
-
64
41
  async def setup(self):
65
42
  """
66
43
  Initialize the strategy job and strategy.
@@ -69,38 +46,13 @@ class StrategyJob:
69
46
  """
70
47
  self._setup_strategy()
71
48
 
72
- # Ensure auth token is set for API calls
49
+ # Ensure API key is set for API calls
50
+ # All clients inherit from WayfinderClient and have _ensure_api_key()
73
51
  if not self.clients._skip_auth:
74
- is_api_key_auth = self._is_using_api_key()
75
-
76
- if is_api_key_auth:
77
- logger.debug("Using API key authentication")
78
- if self.clients.auth:
79
- await self.clients.auth._ensure_bearer_token()
80
- else:
81
- # Try to ensure bearer token is set, authenticate if needed
82
- try:
83
- if self.clients.auth:
84
- await self.clients.auth._ensure_bearer_token()
85
- except (PermissionError, Exception) as e:
86
- if not isinstance(e, PermissionError):
87
- logger.warning(
88
- f"Authentication failed: {e}, trying OAuth fallback"
89
- )
90
- username = self.config.user.username
91
- password = self.config.user.password
92
- refresh_token = self.config.user.refresh_token
93
- if refresh_token or (username and password):
94
- await self.clients.authenticate(
95
- username=username,
96
- password=password,
97
- refresh_token=refresh_token,
98
- )
99
- else:
100
- raise ValueError(
101
- "Authentication required: provide api_key parameter for service account auth, "
102
- "or username+password/refresh_token in config.json for personal access"
103
- ) from e
52
+ # Ensure API key on any client (they all share the same method)
53
+ token_client = self.clients.token
54
+ if token_client:
55
+ token_client._ensure_api_key()
104
56
 
105
57
  existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
106
58
  strategy_cfg = dict(self.config.strategy_config or {})
@@ -110,7 +62,7 @@ class StrategyJob:
110
62
  await self.strategy.setup()
111
63
 
112
64
  async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
113
- """Execute a strategy action (deposit, withdraw, update, status, partial_liquidate)"""
65
+ """Execute a strategy action (deposit, withdraw, update, status, exit, partial_liquidate)"""
114
66
  try:
115
67
  if action == "deposit":
116
68
  result = await self.strategy.deposit(**kwargs)
@@ -120,6 +72,8 @@ class StrategyJob:
120
72
  result = await self.strategy.update()
121
73
  elif action == "status":
122
74
  result = await self.strategy.status()
75
+ elif action == "exit":
76
+ result = await self.strategy.exit(**kwargs)
123
77
  elif action == "partial_liquidate":
124
78
  usd_value = kwargs.get("usd_value")
125
79
  if usd_value is None:
@@ -57,6 +57,7 @@ class EvmTxn(ABC):
57
57
  *,
58
58
  wait_for_receipt: bool = True,
59
59
  timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
60
+ confirmations: int = 1,
60
61
  ) -> tuple[bool, Any]:
61
62
  """
62
63
  Sign and broadcast a transaction dict.
@@ -20,6 +20,9 @@ SUGGESTED_PRIORITY_FEE_MULTIPLIER = 1.5
20
20
  MAX_BASE_FEE_GROWTH_MULTIPLIER = 2
21
21
  GAS_LIMIT_BUFFER_MULTIPLIER = 1.5
22
22
 
23
+ # Base chain ID (Base mainnet)
24
+ BASE_CHAIN_ID = 8453
25
+
23
26
  # Chains that don't support EIP-1559 (London) and need legacy gas pricing
24
27
  PRE_LONDON_GAS_CHAIN_IDS: set[int] = {56, 42161}
25
28
  POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
@@ -155,13 +158,13 @@ class LocalEvmTxn(EvmTxn):
155
158
  gas_price = await self._get_gas_price(w3)
156
159
 
157
160
  transaction["gasPrice"] = int(gas_price * SUGGESTED_GAS_PRICE_MULTIPLIER)
158
- elif chain_id == 999:
159
- big_block_gas_price = await w3.hype.big_block_gas_price()
161
+ # elif chain_id == 999:
162
+ # big_block_gas_price = await w3.hype.big_block_gas_price()
160
163
 
161
- transaction["maxFeePerGas"] = int(
162
- big_block_gas_price * SUGGESTED_PRIORITY_FEE_MULTIPLIER
163
- )
164
- transaction["maxPriorityFeePerGas"] = 0
164
+ # transaction["maxFeePerGas"] = int(
165
+ # big_block_gas_price * SUGGESTED_PRIORITY_FEE_MULTIPLIER
166
+ # )
167
+ # transaction["maxPriorityFeePerGas"] = 0
165
168
  else:
166
169
  base_fee = await self._get_base_fee(w3)
167
170
  priority_fee = await self._get_priority_fee(w3)
@@ -182,12 +185,20 @@ class LocalEvmTxn(EvmTxn):
182
185
  *,
183
186
  wait_for_receipt: bool = True,
184
187
  timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
185
- confirmations: int = 1,
188
+ confirmations: int | None = None,
186
189
  ) -> tuple[bool, Any]:
187
190
  try:
188
191
  chain_id = transaction["chainId"]
189
192
  from_address = transaction["from"]
190
193
 
194
+ # Default confirmation behavior:
195
+ # - Base: wait for 2 additional blocks after the receipt block
196
+ # - Others: do not wait for additional confirmations
197
+ effective_confirmations = confirmations
198
+ if effective_confirmations is None:
199
+ effective_confirmations = 2 if int(chain_id) == BASE_CHAIN_ID else 0
200
+ effective_confirmations = max(0, int(effective_confirmations))
201
+
191
202
  web3 = self.get_web3(chain_id)
192
203
  try:
193
204
  transaction = self._validate_transaction(transaction)
@@ -210,6 +221,8 @@ class LocalEvmTxn(EvmTxn):
210
221
  result["receipt"] = self._format_receipt(receipt)
211
222
  # Add block_number at top level for convenience
212
223
  result["block_number"] = result["receipt"].get("blockNumber")
224
+ result["confirmations"] = effective_confirmations
225
+ result["confirmed_block_number"] = result["block_number"]
213
226
 
214
227
  receipt_status = result["receipt"].get("status")
215
228
  if receipt_status is not None and int(receipt_status) != 1:
@@ -219,11 +232,14 @@ class LocalEvmTxn(EvmTxn):
219
232
  )
220
233
 
221
234
  # Wait for additional confirmations if requested
222
- if confirmations > 0:
235
+ if effective_confirmations > 0:
223
236
  tx_block = result["receipt"].get("blockNumber")
224
237
  if tx_block:
225
238
  await self._wait_for_confirmations(
226
- web3, tx_block, confirmations
239
+ web3, tx_block, effective_confirmations
240
+ )
241
+ result["confirmed_block_number"] = int(tx_block) + int(
242
+ effective_confirmations
227
243
  )
228
244
 
229
245
  return (True, result)
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  from eth_utils import to_checksum_address
8
8
  from loguru import logger
9
- from web3 import AsyncWeb3, Web3
9
+ from web3 import Web3
10
10
 
11
11
  from wayfinder_paths.core.clients.TokenClient import TokenClient
12
12
  from wayfinder_paths.core.constants import ZERO_ADDRESS
@@ -85,7 +85,6 @@ class LocalTokenTxnService(TokenTxn):
85
85
  ) -> tuple[bool, dict[str, Any] | str]:
86
86
  """Build the transaction dictionary for an ERC20 approval."""
87
87
  try:
88
- web3 = self.wallet_provider.get_web3(chain_id)
89
88
  token_checksum = to_checksum_address(token_address)
90
89
  from_checksum = to_checksum_address(from_address)
91
90
  spender_checksum = to_checksum_address(spender)
@@ -99,7 +98,6 @@ class LocalTokenTxnService(TokenTxn):
99
98
  from_address=from_checksum,
100
99
  spender=spender_checksum,
101
100
  amount=amount_int,
102
- web3=web3,
103
101
  )
104
102
  return True, approve_tx
105
103
 
@@ -154,7 +152,7 @@ class LocalTokenTxnService(TokenTxn):
154
152
 
155
153
  def _chain_id(self, chain: Any) -> int:
156
154
  if isinstance(chain, dict):
157
- chain_id = chain.get("id") or chain.get("chain_id")
155
+ chain_id = chain.get("id")
158
156
  else:
159
157
  chain_id = getattr(chain, "id", None)
160
158
  if chain_id is None:
@@ -215,10 +213,8 @@ class LocalTokenTxnService(TokenTxn):
215
213
  from_address: str,
216
214
  spender: str,
217
215
  amount: int,
218
- web3: AsyncWeb3,
219
216
  ) -> dict[str, Any]:
220
217
  """Build an ERC20 approval transaction dict."""
221
- del web3 # Use sync Web3 for encoding (AsyncContract doesn't have encodeABI)
222
218
  token_checksum = to_checksum_address(token_address)
223
219
  spender_checksum = to_checksum_address(spender)
224
220
  from_checksum = to_checksum_address(from_address)
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ import pytest
6
+
7
+ from wayfinder_paths.core.services.local_evm_txn import BASE_CHAIN_ID, LocalEvmTxn
8
+
9
+
10
+ class _FakeTxHash:
11
+ def __init__(self, value: str):
12
+ self._value = value
13
+
14
+ def hex(self) -> str:
15
+ return self._value
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_base_defaults_to_two_confirmations():
20
+ txn = LocalEvmTxn(config={})
21
+
22
+ fake_web3 = MagicMock()
23
+ fake_web3.eth = MagicMock()
24
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
25
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
26
+ return_value={
27
+ "status": 1,
28
+ "blockNumber": 100,
29
+ "transactionHash": "0x1",
30
+ "gasUsed": 21_000,
31
+ "logs": [],
32
+ }
33
+ )
34
+
35
+ txn.get_web3 = MagicMock(return_value=fake_web3)
36
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
37
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
38
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
39
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
40
+ txn._sign_transaction = MagicMock(return_value=b"signed")
41
+ txn._close_web3 = AsyncMock()
42
+ txn._wait_for_confirmations = AsyncMock()
43
+
44
+ ok, result = await txn.broadcast_transaction(
45
+ {
46
+ "chainId": BASE_CHAIN_ID,
47
+ "from": "0x0000000000000000000000000000000000000001",
48
+ "to": "0x0000000000000000000000000000000000000002",
49
+ "value": 0,
50
+ },
51
+ wait_for_receipt=True,
52
+ timeout=1,
53
+ )
54
+
55
+ assert ok is True
56
+ txn._wait_for_confirmations.assert_awaited_once_with(fake_web3, 100, 2)
57
+ assert result["confirmations"] == 2
58
+ assert result["confirmed_block_number"] == 102
59
+
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_non_base_defaults_to_zero_confirmations():
63
+ txn = LocalEvmTxn(config={})
64
+
65
+ fake_web3 = MagicMock()
66
+ fake_web3.eth = MagicMock()
67
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
68
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
69
+ return_value={
70
+ "status": 1,
71
+ "blockNumber": 100,
72
+ "transactionHash": "0x1",
73
+ "gasUsed": 21_000,
74
+ "logs": [],
75
+ }
76
+ )
77
+
78
+ txn.get_web3 = MagicMock(return_value=fake_web3)
79
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
80
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
81
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
82
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
83
+ txn._sign_transaction = MagicMock(return_value=b"signed")
84
+ txn._close_web3 = AsyncMock()
85
+ txn._wait_for_confirmations = AsyncMock()
86
+
87
+ ok, result = await txn.broadcast_transaction(
88
+ {
89
+ "chainId": 1,
90
+ "from": "0x0000000000000000000000000000000000000001",
91
+ "to": "0x0000000000000000000000000000000000000002",
92
+ "value": 0,
93
+ },
94
+ wait_for_receipt=True,
95
+ timeout=1,
96
+ )
97
+
98
+ assert ok is True
99
+ txn._wait_for_confirmations.assert_not_awaited()
100
+ assert result["confirmations"] == 0
101
+ assert result["confirmed_block_number"] == 100
102
+
103
+
104
+ @pytest.mark.asyncio
105
+ async def test_explicit_confirmations_override_defaults():
106
+ txn = LocalEvmTxn(config={})
107
+
108
+ fake_web3 = MagicMock()
109
+ fake_web3.eth = MagicMock()
110
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
111
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
112
+ return_value={
113
+ "status": 1,
114
+ "blockNumber": 100,
115
+ "transactionHash": "0x1",
116
+ "gasUsed": 21_000,
117
+ "logs": [],
118
+ }
119
+ )
120
+
121
+ txn.get_web3 = MagicMock(return_value=fake_web3)
122
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
123
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
124
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
125
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
126
+ txn._sign_transaction = MagicMock(return_value=b"signed")
127
+ txn._close_web3 = AsyncMock()
128
+ txn._wait_for_confirmations = AsyncMock()
129
+
130
+ ok, result = await txn.broadcast_transaction(
131
+ {
132
+ "chainId": BASE_CHAIN_ID,
133
+ "from": "0x0000000000000000000000000000000000000001",
134
+ "to": "0x0000000000000000000000000000000000000002",
135
+ "value": 0,
136
+ },
137
+ wait_for_receipt=True,
138
+ timeout=1,
139
+ confirmations=0,
140
+ )
141
+
142
+ assert ok is True
143
+ txn._wait_for_confirmations.assert_not_awaited()
144
+ assert result["confirmations"] == 0
145
+ assert result["confirmed_block_number"] == 100