wayfinder-paths 0.1.15__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 (47) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +19 -20
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +66 -37
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +2 -8
  4. wayfinder_paths/adapters/brap_adapter/README.md +22 -19
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +33 -34
  6. wayfinder_paths/adapters/brap_adapter/test_adapter.py +2 -18
  7. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -56
  8. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +1 -8
  9. wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
  10. wayfinder_paths/adapters/moonwell_adapter/adapter.py +301 -662
  11. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +275 -179
  12. wayfinder_paths/core/config.py +8 -47
  13. wayfinder_paths/core/constants/base.py +0 -1
  14. wayfinder_paths/core/constants/erc20_abi.py +13 -13
  15. wayfinder_paths/core/strategies/Strategy.py +6 -2
  16. wayfinder_paths/core/utils/erc20_service.py +100 -0
  17. wayfinder_paths/core/utils/evm_helpers.py +1 -1
  18. wayfinder_paths/core/utils/transaction.py +191 -0
  19. wayfinder_paths/core/utils/web3.py +66 -0
  20. wayfinder_paths/run_strategy.py +37 -6
  21. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +200 -224
  22. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +128 -151
  23. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
  24. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +52 -78
  25. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
  26. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +0 -1
  27. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +39 -64
  28. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
  29. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +42 -85
  30. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
  31. wayfinder_paths/templates/strategy/README.md +1 -5
  32. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
  33. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +35 -44
  34. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
  35. wayfinder_paths/core/clients/sdk_example.py +0 -125
  36. wayfinder_paths/core/engine/__init__.py +0 -5
  37. wayfinder_paths/core/services/__init__.py +0 -0
  38. wayfinder_paths/core/services/base.py +0 -131
  39. wayfinder_paths/core/services/local_evm_txn.py +0 -350
  40. wayfinder_paths/core/services/local_token_txn.py +0 -238
  41. wayfinder_paths/core/services/web3_service.py +0 -43
  42. wayfinder_paths/core/wallets/README.md +0 -88
  43. wayfinder_paths/core/wallets/WalletManager.py +0 -56
  44. wayfinder_paths/core/wallets/__init__.py +0 -7
  45. wayfinder_paths/scripts/run_strategy.py +0 -152
  46. wayfinder_paths/strategies/config.py +0 -85
  47. {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
@@ -1,350 +0,0 @@
1
- import asyncio
2
- from typing import Any
3
-
4
- from eth_account import Account
5
- from eth_utils import to_checksum_address
6
- from loguru import logger
7
- from web3 import AsyncHTTPProvider, AsyncWeb3
8
- from web3.middleware import ExtraDataToPOAMiddleware
9
- from web3.module import Module
10
-
11
- from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
12
- from wayfinder_paths.core.services.base import EvmTxn
13
- from wayfinder_paths.core.utils.evm_helpers import (
14
- resolve_private_key_for_from_address,
15
- resolve_rpc_url,
16
- )
17
-
18
- SUGGESTED_GAS_PRICE_MULTIPLIER = 1.5
19
- SUGGESTED_PRIORITY_FEE_MULTIPLIER = 1.5
20
- MAX_BASE_FEE_GROWTH_MULTIPLIER = 2
21
- GAS_LIMIT_BUFFER_MULTIPLIER = 1.5
22
-
23
- # Base chain ID (Base mainnet)
24
- BASE_CHAIN_ID = 8453
25
-
26
- # Chains that don't support EIP-1559 (London) and need legacy gas pricing
27
- PRE_LONDON_GAS_CHAIN_IDS: set[int] = {56, 42161}
28
- POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
29
-
30
-
31
- def _looks_like_revert_error(error: Any) -> bool:
32
- msg = str(error).lower()
33
- return any(
34
- needle in msg
35
- for needle in (
36
- "execution reverted",
37
- "revert",
38
- "always failing transaction",
39
- "gas required exceeds",
40
- "out of gas",
41
- "insufficient funds",
42
- "transfer amount exceeds balance",
43
- "insufficient balance",
44
- "insufficient allowance",
45
- )
46
- )
47
-
48
-
49
- class HyperModule(Module):
50
- def __init__(self, w3):
51
- super().__init__(w3)
52
-
53
- async def big_block_gas_price(self):
54
- big_block_gas_price = await self.w3.manager.coro_request(
55
- "eth_bigBlockGasPrice", []
56
- )
57
- return int(big_block_gas_price, 16)
58
-
59
-
60
- class LocalEvmTxn(EvmTxn):
61
- """
62
- Local wallet provider using private keys stored in config.json or config.json.
63
-
64
- This provider implements the current default behavior:
65
- - Resolves private keys from config.json or config.json
66
- - Signs transactions using eth_account
67
- - Broadcasts transactions via RPC
68
- """
69
-
70
- def __init__(self, config: dict[str, Any] | None = None):
71
- self.config = config or {}
72
- self.logger = logger.bind(provider="LocalWalletProvider")
73
-
74
- def get_web3(self, chain_id: int) -> AsyncWeb3:
75
- rpc_url = self._resolve_rpc_url(chain_id)
76
- web3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
77
- if chain_id in POA_MIDDLEWARE_CHAIN_IDS:
78
- web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
79
- if chain_id == 999:
80
- web3.attach_modules({"hype": (HyperModule)})
81
- return web3
82
-
83
- def _validate_transaction(self, transaction: dict[str, Any]) -> dict[str, Any]:
84
- tx = dict(transaction)
85
-
86
- assert "from" in tx, "Transaction missing 'from' address"
87
- assert "to" in tx, "Transaction missing 'to' address"
88
- assert "chainId" in tx, "Transaction missing 'chainId'"
89
-
90
- tx["from"] = to_checksum_address(tx["from"])
91
- tx["to"] = to_checksum_address(tx["to"])
92
- if "value" in tx:
93
- tx["value"] = self._normalize_int(tx["value"])
94
-
95
- tx.pop("gas", None)
96
- tx.pop("gasPrice", None)
97
- tx.pop("maxFeePerGas", None)
98
- tx.pop("maxPriorityFeePerGas", None)
99
- tx.pop("nonce", None)
100
-
101
- return tx
102
-
103
- async def _nonce_transaction(
104
- self, transaction: dict[str, Any], w3: AsyncWeb3
105
- ) -> dict[str, Any]:
106
- transaction["nonce"] = await w3.eth.get_transaction_count(
107
- transaction["from"], "pending"
108
- )
109
- return transaction
110
-
111
- async def _gas_limit_transaction(
112
- self, transaction: dict[str, Any], w3: AsyncWeb3
113
- ) -> dict[str, Any]:
114
- # Pop existing gas limit before estimating - if present, the node uses it as
115
- # a ceiling and fails with "out of gas" instead of returning actual estimate
116
- existing_gas = transaction.pop("gas", None)
117
- try:
118
- transaction.pop("gas", None) # Remove any existing gas limit
119
- estimated = await w3.eth.estimate_gas(transaction)
120
- transaction["gas"] = int(estimated * GAS_LIMIT_BUFFER_MULTIPLIER)
121
- self.logger.debug(
122
- f"Estimated gas with buffer: {estimated} -> {transaction['gas']}"
123
- )
124
- except Exception as exc: # noqa: BLE001
125
- if _looks_like_revert_error(exc):
126
- raise ValueError(
127
- f"Gas estimation failed (tx likely to revert): {exc}"
128
- ) from exc
129
- self.logger.warning(f"Gas estimation failed. Reason: {exc}")
130
- # Restore existing gas limit if estimation failed, otherwise error
131
- if existing_gas is not None:
132
- transaction["gas"] = existing_gas
133
- else:
134
- raise ValueError(
135
- f"Gas estimation failed and no gas limit set: {exc}"
136
- ) from exc
137
-
138
- return transaction
139
-
140
- async def _get_gas_price(self, w3: AsyncWeb3) -> int:
141
- return await w3.eth.gas_price
142
-
143
- async def _get_base_fee(self, w3: AsyncWeb3) -> int:
144
- latest_block = await w3.eth.get_block("latest")
145
- return latest_block.baseFeePerGas
146
-
147
- async def _get_priority_fee(self, w3: AsyncWeb3) -> int:
148
- lookback_blocks = 10
149
- percentile = 80
150
- fee_history = await w3.eth.fee_history(lookback_blocks, "latest", [percentile])
151
- historical_priority_fees = [i[0] for i in fee_history.reward]
152
- return sum(historical_priority_fees) // len(historical_priority_fees)
153
-
154
- async def _gas_price_transaction(
155
- self, transaction: dict[str, Any], chain_id: int, w3: AsyncWeb3
156
- ) -> dict[str, Any]:
157
- if chain_id in PRE_LONDON_GAS_CHAIN_IDS:
158
- gas_price = await self._get_gas_price(w3)
159
-
160
- transaction["gasPrice"] = int(gas_price * SUGGESTED_GAS_PRICE_MULTIPLIER)
161
- # elif chain_id == 999:
162
- # big_block_gas_price = await w3.hype.big_block_gas_price()
163
-
164
- # transaction["maxFeePerGas"] = int(
165
- # big_block_gas_price * SUGGESTED_PRIORITY_FEE_MULTIPLIER
166
- # )
167
- # transaction["maxPriorityFeePerGas"] = 0
168
- else:
169
- base_fee = await self._get_base_fee(w3)
170
- priority_fee = await self._get_priority_fee(w3)
171
-
172
- transaction["maxFeePerGas"] = int(
173
- base_fee * MAX_BASE_FEE_GROWTH_MULTIPLIER
174
- + priority_fee * SUGGESTED_PRIORITY_FEE_MULTIPLIER
175
- )
176
- transaction["maxPriorityFeePerGas"] = int(
177
- priority_fee * SUGGESTED_PRIORITY_FEE_MULTIPLIER
178
- )
179
-
180
- return transaction
181
-
182
- async def broadcast_transaction(
183
- self,
184
- transaction: dict[str, Any],
185
- *,
186
- wait_for_receipt: bool = True,
187
- timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
188
- confirmations: int | None = None,
189
- ) -> tuple[bool, Any]:
190
- try:
191
- chain_id = transaction["chainId"]
192
- from_address = transaction["from"]
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
-
202
- web3 = self.get_web3(chain_id)
203
- try:
204
- transaction = self._validate_transaction(transaction)
205
- transaction = await self._nonce_transaction(transaction, web3)
206
- transaction = await self._gas_limit_transaction(transaction, web3)
207
- transaction = await self._gas_price_transaction(
208
- transaction, chain_id, web3
209
- )
210
-
211
- signed_tx = self._sign_transaction(transaction, from_address)
212
-
213
- tx_hash = await web3.eth.send_raw_transaction(signed_tx)
214
- tx_hash_hex = tx_hash.hex()
215
-
216
- result: dict[str, Any] = {"tx_hash": tx_hash_hex}
217
- if wait_for_receipt:
218
- receipt = await web3.eth.wait_for_transaction_receipt(
219
- tx_hash, timeout=timeout
220
- )
221
- result["receipt"] = self._format_receipt(receipt)
222
- # Add block_number at top level for convenience
223
- result["block_number"] = result["receipt"].get("blockNumber")
224
- result["confirmations"] = effective_confirmations
225
- result["confirmed_block_number"] = result["block_number"]
226
-
227
- receipt_status = result["receipt"].get("status")
228
- if receipt_status is not None and int(receipt_status) != 1:
229
- return (
230
- False,
231
- f"Transaction reverted (status={receipt_status}): {tx_hash_hex}",
232
- )
233
-
234
- # Wait for additional confirmations if requested
235
- if effective_confirmations > 0:
236
- tx_block = result["receipt"].get("blockNumber")
237
- if tx_block:
238
- await self._wait_for_confirmations(
239
- web3, tx_block, effective_confirmations
240
- )
241
- result["confirmed_block_number"] = int(tx_block) + int(
242
- effective_confirmations
243
- )
244
-
245
- return (True, result)
246
-
247
- finally:
248
- await self._close_web3(web3)
249
-
250
- except Exception as exc: # noqa: BLE001
251
- self.logger.error(f"Transaction broadcast failed: {exc}")
252
- return (False, f"Transaction broadcast failed: {exc}")
253
-
254
- async def transaction_succeeded(
255
- self, tx_hash: str, chain_id: int, timeout: int = 120
256
- ) -> bool:
257
- w3 = self.get_web3(chain_id)
258
- try:
259
- receipt = await w3.eth.wait_for_transaction_receipt(
260
- tx_hash, timeout=timeout
261
- )
262
- status = getattr(receipt, "status", None)
263
- if status is None and isinstance(receipt, dict):
264
- status = receipt.get("status")
265
- return status == 1
266
- except Exception as exc: # noqa: BLE001
267
- self.logger.warning(
268
- f"Failed to confirm transaction {tx_hash} on chain {chain_id}: {exc}"
269
- )
270
- return False
271
- finally:
272
- await self._close_web3(w3)
273
-
274
- def _sign_transaction(
275
- self, transaction: dict[str, Any], from_address: str
276
- ) -> bytes:
277
- private_key = resolve_private_key_for_from_address(from_address, self.config)
278
- if not private_key:
279
- raise ValueError(f"No private key available for address {from_address}")
280
- signed = Account.sign_transaction(transaction, private_key)
281
- return signed.raw_transaction
282
-
283
- def _resolve_rpc_url(self, chain_id: int) -> str:
284
- return resolve_rpc_url(chain_id, self.config or {}, None)
285
-
286
- async def _close_web3(self, web3: AsyncWeb3) -> None:
287
- try:
288
- if hasattr(web3.provider, "disconnect"):
289
- await web3.provider.disconnect()
290
- except Exception as e: # noqa: BLE001
291
- self.logger.debug(f"Error disconnecting provider: {e}")
292
-
293
- async def _wait_for_confirmations(
294
- self, w3: AsyncWeb3, tx_block: int, confirmations: int
295
- ) -> None:
296
- """Wait until the transaction has the specified number of confirmations."""
297
- target_block = tx_block + confirmations
298
- while True:
299
- current_block = await w3.eth.block_number
300
- if current_block >= target_block:
301
- break
302
- await asyncio.sleep(1)
303
-
304
- def _format_receipt(self, receipt: Any) -> dict[str, Any]:
305
- tx_hash = getattr(receipt, "transactionHash", None)
306
- if hasattr(tx_hash, "hex"):
307
- tx_hash = tx_hash.hex()
308
-
309
- return {
310
- "transactionHash": tx_hash,
311
- "status": (
312
- getattr(receipt, "status", None)
313
- if not isinstance(receipt, dict)
314
- else receipt.get("status")
315
- ),
316
- "blockNumber": (
317
- getattr(receipt, "blockNumber", None)
318
- if not isinstance(receipt, dict)
319
- else receipt.get("blockNumber")
320
- ),
321
- "gasUsed": (
322
- getattr(receipt, "gasUsed", None)
323
- if not isinstance(receipt, dict)
324
- else receipt.get("gasUsed")
325
- ),
326
- "logs": (
327
- [
328
- dict(log_entry) if not isinstance(log_entry, dict) else log_entry
329
- for log_entry in getattr(receipt, "logs", [])
330
- ]
331
- if hasattr(receipt, "logs")
332
- else receipt.get("logs")
333
- if isinstance(receipt, dict)
334
- else []
335
- ),
336
- }
337
-
338
- def _normalize_int(self, value: Any) -> int:
339
- if isinstance(value, int):
340
- return value
341
- if isinstance(value, float):
342
- return int(value)
343
- if isinstance(value, str):
344
- if value.startswith("0x"):
345
- return int(value, 16)
346
- try:
347
- return int(value)
348
- except ValueError:
349
- return int(float(value))
350
- raise ValueError(f"Unable to convert value '{value}' to int")
@@ -1,238 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from decimal import ROUND_DOWN, Decimal
5
- from typing import Any
6
-
7
- from eth_utils import to_checksum_address
8
- from loguru import logger
9
- from web3 import Web3
10
-
11
- from wayfinder_paths.core.clients.TokenClient import TokenClient
12
- from wayfinder_paths.core.constants import ZERO_ADDRESS
13
- from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI, ERC20_APPROVAL_ABI
14
- from wayfinder_paths.core.services.base import EvmTxn, TokenTxn
15
- from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
16
-
17
-
18
- class LocalTokenTxnService(TokenTxn):
19
- """Default transaction builder used by adapters."""
20
-
21
- def __init__(
22
- self,
23
- config: dict[str, Any] | None,
24
- *,
25
- wallet_provider: EvmTxn,
26
- ) -> None:
27
- del config
28
- self.wallet_provider = wallet_provider
29
- self.logger = logger.bind(service="DefaultEvmTransactionService")
30
- self.token_client = TokenClient()
31
-
32
- async def build_send(
33
- self,
34
- *,
35
- token_id: str,
36
- amount: float,
37
- from_address: str,
38
- to_address: str,
39
- token_info: dict[str, Any] | None = None,
40
- ) -> tuple[bool, dict[str, Any] | str]:
41
- """Build the transaction dict for sending tokens between wallets."""
42
- token_meta = token_info
43
- if token_meta is None:
44
- token_meta = await self.token_client.get_token_details(token_id)
45
- if not token_meta:
46
- return False, f"Token not found: {token_id}"
47
-
48
- chain_id = resolve_chain_id(token_meta, self.logger)
49
- if chain_id is None:
50
- return False, f"Token {token_id} is missing a chain id"
51
-
52
- token_address = (token_meta or {}).get("address") or ZERO_ADDRESS
53
- is_native = not token_address or token_address.lower() == ZERO_ADDRESS.lower()
54
-
55
- if is_native:
56
- amount_wei = self._to_base_units(
57
- amount, 18
58
- ) # Native tokens use 18 decimals
59
- else:
60
- decimals = int((token_meta or {}).get("decimals") or 18)
61
- amount_wei = self._to_base_units(amount, decimals)
62
-
63
- try:
64
- tx = await self.build_send_transaction(
65
- from_address=from_address,
66
- to_address=to_address,
67
- token_address=token_address,
68
- amount=amount_wei,
69
- chain_id=int(chain_id),
70
- is_native=is_native,
71
- )
72
- except Exception as exc: # noqa: BLE001
73
- return False, f"Failed to build send transaction: {exc}"
74
-
75
- return True, tx
76
-
77
- def build_erc20_approve(
78
- self,
79
- *,
80
- chain_id: int,
81
- token_address: str,
82
- from_address: str,
83
- spender: str,
84
- amount: int,
85
- ) -> tuple[bool, dict[str, Any] | str]:
86
- """Build the transaction dictionary for an ERC20 approval."""
87
- try:
88
- token_checksum = to_checksum_address(token_address)
89
- from_checksum = to_checksum_address(from_address)
90
- spender_checksum = to_checksum_address(spender)
91
- amount_int = int(amount)
92
- except (TypeError, ValueError) as exc:
93
- return False, str(exc)
94
-
95
- approve_tx = self.build_erc20_approval_transaction(
96
- chain_id=chain_id,
97
- token_address=token_checksum,
98
- from_address=from_checksum,
99
- spender=spender_checksum,
100
- amount=amount_int,
101
- )
102
- return True, approve_tx
103
-
104
- async def read_erc20_allowance(
105
- self,
106
- chain: Any,
107
- token_address: str,
108
- from_address: str,
109
- spender_address: str,
110
- max_retries: int = 3,
111
- ) -> dict[str, Any]:
112
- try:
113
- chain_id = self._chain_id(chain)
114
- except (TypeError, ValueError) as exc:
115
- return {"error": str(exc), "allowance": 0}
116
-
117
- last_error = None
118
- for attempt in range(max_retries):
119
- w3 = self.wallet_provider.get_web3(chain_id)
120
- try:
121
- contract = w3.eth.contract(
122
- address=to_checksum_address(token_address), abi=ERC20_APPROVAL_ABI
123
- )
124
- allowance = await contract.functions.allowance(
125
- to_checksum_address(from_address),
126
- to_checksum_address(spender_address),
127
- ).call()
128
- return {"allowance": int(allowance)}
129
- except Exception as exc: # noqa: BLE001
130
- last_error = exc
131
- error_str = str(exc)
132
- if "429" in error_str or "Too Many Requests" in error_str:
133
- if attempt < max_retries - 1:
134
- wait_time = 3.0 * (2**attempt) # 3, 6, 12 seconds
135
- self.logger.warning(
136
- f"Rate limited reading allowance, retrying in {wait_time}s..."
137
- )
138
- await asyncio.sleep(wait_time)
139
- continue
140
- self.logger.error(f"Failed to read allowance: {exc}")
141
- return {"error": f"Allowance query failed: {exc}", "allowance": 0}
142
- finally:
143
- await self.wallet_provider._close_web3(w3)
144
-
145
- self.logger.error(
146
- f"Failed to read allowance after {max_retries} retries: {last_error}"
147
- )
148
- return {
149
- "error": f"Allowance query failed after retries: {last_error}",
150
- "allowance": 0,
151
- }
152
-
153
- def _chain_id(self, chain: Any) -> int:
154
- if isinstance(chain, dict):
155
- chain_id = chain.get("id")
156
- else:
157
- chain_id = getattr(chain, "id", None)
158
- if chain_id is None:
159
- raise ValueError("Chain ID is required")
160
- return int(chain_id)
161
-
162
- def _to_base_units(self, amount: float, decimals: int) -> int:
163
- """Convert human-readable amount to base units (wei for native, token units for ERC20)."""
164
- scale = Decimal(10) ** int(decimals)
165
- quantized = (Decimal(str(amount)) * scale).to_integral_value(
166
- rounding=ROUND_DOWN
167
- )
168
- return int(quantized)
169
-
170
- async def build_send_transaction(
171
- self,
172
- *,
173
- from_address: str,
174
- to_address: str,
175
- token_address: str | None,
176
- amount: int,
177
- chain_id: int,
178
- is_native: bool,
179
- ) -> dict[str, Any]:
180
- """Build the transaction dict for sending native or ERC20 tokens."""
181
- from_checksum = to_checksum_address(from_address)
182
- to_checksum = to_checksum_address(to_address)
183
- chain_id_int = int(chain_id)
184
-
185
- if is_native:
186
- return {
187
- "chainId": chain_id_int,
188
- "from": from_checksum,
189
- "to": to_checksum,
190
- "value": int(amount),
191
- }
192
-
193
- token_checksum = to_checksum_address(token_address or ZERO_ADDRESS)
194
- w3_sync = Web3()
195
- contract = w3_sync.eth.contract(address=token_checksum, abi=ERC20_ABI)
196
- data = contract.functions.transfer(
197
- to_checksum, int(amount)
198
- )._encode_transaction_data()
199
-
200
- return {
201
- "chainId": chain_id_int,
202
- "from": from_checksum,
203
- "to": token_checksum,
204
- "data": data,
205
- "value": 0,
206
- }
207
-
208
- def build_erc20_approval_transaction(
209
- self,
210
- *,
211
- chain_id: int,
212
- token_address: str,
213
- from_address: str,
214
- spender: str,
215
- amount: int,
216
- ) -> dict[str, Any]:
217
- """Build an ERC20 approval transaction dict."""
218
- token_checksum = to_checksum_address(token_address)
219
- spender_checksum = to_checksum_address(spender)
220
- from_checksum = to_checksum_address(from_address)
221
- amount_int = int(amount)
222
-
223
- # Use synchronous Web3 for encoding (encodeABI doesn't exist in web3.py v7)
224
- w3_sync = Web3()
225
- contract = w3_sync.eth.contract(address=token_checksum, abi=ERC20_APPROVAL_ABI)
226
-
227
- # In web3.py v7, use _encode_transaction_data to encode without network calls
228
- data = contract.functions.approve(
229
- spender_checksum, amount_int
230
- )._encode_transaction_data()
231
-
232
- return {
233
- "chainId": int(chain_id),
234
- "from": from_checksum,
235
- "to": token_checksum,
236
- "data": data,
237
- "value": 0,
238
- }
@@ -1,43 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from wayfinder_paths.core.services.base import EvmTxn, TokenTxn, Web3Service
4
- from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
5
- from wayfinder_paths.core.services.local_token_txn import (
6
- LocalTokenTxnService,
7
- )
8
-
9
-
10
- class DefaultWeb3Service(Web3Service):
11
- """Default implementation that simply wires the provided dependencies together."""
12
-
13
- def __init__(
14
- self,
15
- config: dict | None = None,
16
- *,
17
- wallet_provider: EvmTxn | None = None,
18
- evm_transactions: TokenTxn | None = None,
19
- ) -> None:
20
- """
21
- Initialize the service with optional dependency injection.
22
-
23
- Strategies that already constructed wallet providers or transaction helpers
24
- can pass them in directly. Otherwise we fall back to the legacy behavior of
25
- building a LocalWalletProvider + DefaultEvmTransactionService from config.
26
- """
27
- cfg = config or {}
28
- self._wallet_provider = wallet_provider or LocalEvmTxn(cfg)
29
- if evm_transactions is not None:
30
- self._evm_transactions = evm_transactions
31
- else:
32
- self._evm_transactions = LocalTokenTxnService(
33
- config=cfg,
34
- wallet_provider=self._wallet_provider,
35
- )
36
-
37
- @property
38
- def evm_transactions(self) -> EvmTxn:
39
- return self._wallet_provider
40
-
41
- @property
42
- def token_transactions(self) -> TokenTxn:
43
- return self._evm_transactions