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
@@ -5,13 +5,6 @@ from typing import Any
5
5
 
6
6
  from loguru import logger
7
7
 
8
- from wayfinder_paths.core.constants.base import (
9
- ADAPTER_BALANCE,
10
- ADAPTER_BRAP,
11
- ADAPTER_HYPERLIQUID,
12
- ADAPTER_MOONWELL,
13
- )
14
-
15
8
 
16
9
  def _load_config_file() -> dict[str, Any]:
17
10
  path = Path("config.json")
@@ -25,6 +18,14 @@ def _load_config_file() -> dict[str, Any]:
25
18
 
26
19
 
27
20
  CONFIG = _load_config_file()
21
+ SUPPORTED_CHAINS = [
22
+ 1,
23
+ 8453,
24
+ 56,
25
+ 42161,
26
+ 137,
27
+ 999,
28
+ ]
28
29
 
29
30
 
30
31
  @dataclass
@@ -193,46 +194,6 @@ class StrategyJobConfig:
193
194
  strategy_config=strategy_config,
194
195
  )
195
196
 
196
- def get_adapter_config(self, adapter_name: str) -> dict[str, Any]:
197
- config = {
198
- "api_base_url": self.system.api_base_url,
199
- }
200
-
201
- if adapter_name in [
202
- ADAPTER_BALANCE,
203
- ADAPTER_BRAP,
204
- ADAPTER_MOONWELL,
205
- ADAPTER_HYPERLIQUID,
206
- ]:
207
- strategy_wallet = self.strategy_config.get("strategy_wallet")
208
- main_wallet = self.strategy_config.get("main_wallet")
209
- config["strategy_wallet"] = (
210
- {"address": strategy_wallet["address"]}
211
- if strategy_wallet
212
- and isinstance(strategy_wallet, dict)
213
- and strategy_wallet.get("address")
214
- else {}
215
- )
216
- config["main_wallet"] = (
217
- {"address": main_wallet["address"]}
218
- if main_wallet
219
- and isinstance(main_wallet, dict)
220
- and main_wallet.get("address")
221
- else {}
222
- )
223
- config["user_wallet"] = (
224
- config.get("strategy_wallet") or config.get("main_wallet") or {}
225
- )
226
-
227
- if adapter_name == ADAPTER_BRAP:
228
- config["default_slippage"] = self.user.default_slippage
229
- config["gas_multiplier"] = self.user.gas_multiplier
230
-
231
- if adapter_name in self.strategy_config.get("adapters", {}):
232
- config.update(self.strategy_config["adapters"][adapter_name])
233
-
234
- return config
235
-
236
197
 
237
198
  def get_api_base_url() -> str:
238
199
  system = CONFIG.get("system", {}) if isinstance(CONFIG, dict) else {}
@@ -29,7 +29,6 @@ DEFAULT_SLIPPAGE = 0.005
29
29
  # Base L2 (and some RPC providers) can occasionally take >2 minutes to index/return receipts,
30
30
  # even if the transaction is eventually mined. A longer timeout reduces false negatives that
31
31
  # can lead to unsafe retry behavior (nonce gaps, duplicate swaps, etc.).
32
- DEFAULT_TRANSACTION_TIMEOUT = 300 # Transaction receipt wait timeout
33
32
  DEFAULT_HTTP_TIMEOUT = 30.0 # HTTP client timeout
34
33
 
35
34
  # Adapter type identifiers
@@ -8,8 +8,8 @@ ERC20_ABI = [
8
8
  {
9
9
  "constant": True,
10
10
  "inputs": [
11
- {"name": "_owner", "type": "address"},
12
- {"name": "_spender", "type": "address"},
11
+ {"name": "owner", "type": "address"},
12
+ {"name": "spender", "type": "address"},
13
13
  ],
14
14
  "name": "allowance",
15
15
  "outputs": [{"name": "", "type": "uint256"}],
@@ -18,8 +18,8 @@ ERC20_ABI = [
18
18
  {
19
19
  "constant": False,
20
20
  "inputs": [
21
- {"name": "_spender", "type": "address"},
22
- {"name": "_value", "type": "uint256"},
21
+ {"name": "spender", "type": "address"},
22
+ {"name": "value", "type": "uint256"},
23
23
  ],
24
24
  "name": "approve",
25
25
  "outputs": [{"name": "", "type": "bool"}],
@@ -63,8 +63,8 @@ ERC20_ABI = [
63
63
  {
64
64
  "constant": False,
65
65
  "inputs": [
66
- {"name": "_to", "type": "address"},
67
- {"name": "_value", "type": "uint256"},
66
+ {"name": "to", "type": "address"},
67
+ {"name": "value", "type": "uint256"},
68
68
  ],
69
69
  "name": "transfer",
70
70
  "outputs": [{"name": "", "type": "bool"}],
@@ -73,9 +73,9 @@ ERC20_ABI = [
73
73
  {
74
74
  "constant": False,
75
75
  "inputs": [
76
- {"name": "_from", "type": "address"},
77
- {"name": "_to", "type": "address"},
78
- {"name": "_value", "type": "uint256"},
76
+ {"name": "from", "type": "address"},
77
+ {"name": "to", "type": "address"},
78
+ {"name": "value", "type": "uint256"},
79
79
  ],
80
80
  "name": "transferFrom",
81
81
  "outputs": [{"name": "", "type": "bool"}],
@@ -87,8 +87,8 @@ ERC20_APPROVAL_ABI = [
87
87
  {
88
88
  "constant": True,
89
89
  "inputs": [
90
- {"name": "_owner", "type": "address"},
91
- {"name": "_spender", "type": "address"},
90
+ {"name": "owner", "type": "address"},
91
+ {"name": "spender", "type": "address"},
92
92
  ],
93
93
  "name": "allowance",
94
94
  "outputs": [{"name": "", "type": "uint256"}],
@@ -97,8 +97,8 @@ ERC20_APPROVAL_ABI = [
97
97
  {
98
98
  "constant": False,
99
99
  "inputs": [
100
- {"name": "_spender", "type": "address"},
101
- {"name": "_value", "type": "uint256"},
100
+ {"name": "spender", "type": "address"},
101
+ {"name": "value", "type": "uint256"},
102
102
  ],
103
103
  "name": "approve",
104
104
  "outputs": [{"name": "", "type": "bool"}],
@@ -8,7 +8,6 @@ from typing import Any, TypedDict
8
8
  from loguru import logger
9
9
 
10
10
  from wayfinder_paths.core.clients.TokenClient import TokenDetails
11
- from wayfinder_paths.core.services.base import Web3Service
12
11
  from wayfinder_paths.core.strategies.descriptors import StratDescriptor
13
12
 
14
13
 
@@ -56,12 +55,17 @@ class Strategy(ABC):
56
55
  *,
57
56
  main_wallet: WalletConfig | dict[str, Any] | None = None,
58
57
  strategy_wallet: WalletConfig | dict[str, Any] | None = None,
59
- web3_service: Web3Service | None = None,
58
+ api_key: str | None = None,
59
+ main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
60
+ strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
61
+ | None = None,
60
62
  ):
61
63
  self.adapters = {}
62
64
  self.ledger_adapter = None
63
65
  self.logger = logger.bind(strategy=self.__class__.__name__)
64
66
  self.config = config
67
+ self.main_wallet_signing_callback = main_wallet_signing_callback
68
+ self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
65
69
 
66
70
  async def setup(self) -> None:
67
71
  """Initialize strategy-specific setup after construction"""
@@ -0,0 +1,100 @@
1
+ from web3 import AsyncWeb3
2
+
3
+ from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
4
+ from wayfinder_paths.core.utils.web3 import web3_from_chain_id
5
+
6
+ NATIVE_CURRENCY_ADDRESSES: set = {
7
+ "0x0000000000000000000000000000000000000000",
8
+ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
9
+ # TODO: This is not a proper SOL address, this short form is for LIFI only, fix this after fixing lifi
10
+ "11111111111111111111111111111111",
11
+ "0x0000000000000000000000000000000000001010",
12
+ }
13
+
14
+
15
+ async def get_token_balance(
16
+ token_address: str, chain_id: int, wallet_address: str
17
+ ) -> int:
18
+ async with web3_from_chain_id(chain_id) as web3:
19
+ checksum_wallet = AsyncWeb3.to_checksum_address(wallet_address)
20
+ if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
21
+ balance = await web3.eth.get_balance(checksum_wallet)
22
+ return int(balance)
23
+
24
+ checksum_token = AsyncWeb3.to_checksum_address(token_address)
25
+ contract = web3.eth.contract(address=checksum_token, abi=ERC20_ABI)
26
+ balance = await contract.functions.balanceOf(checksum_wallet).call(
27
+ block_identifier="pending"
28
+ )
29
+ return int(balance)
30
+
31
+
32
+ async def get_token_allowance(
33
+ token_address: str, chain_id: int, owner_address: str, spender_address: str
34
+ ):
35
+ async with web3_from_chain_id(chain_id) as web3:
36
+ contract = web3.eth.contract(
37
+ address=web3.to_checksum_address(token_address), abi=ERC20_ABI
38
+ )
39
+ return await contract.functions.allowance(
40
+ web3.to_checksum_address(owner_address),
41
+ web3.to_checksum_address(spender_address),
42
+ ).call(block_identifier="pending")
43
+
44
+
45
+ async def build_approve_transaction(
46
+ from_address: str,
47
+ chain_id: int,
48
+ token_address: str,
49
+ spender_address: str,
50
+ amount: int,
51
+ ) -> dict:
52
+ """Build an ERC20 approve transaction."""
53
+ async with web3_from_chain_id(chain_id) as web3:
54
+ contract = web3.eth.contract(
55
+ address=web3.to_checksum_address(token_address), abi=ERC20_ABI
56
+ )
57
+ data = contract.encode_abi(
58
+ "approve",
59
+ [
60
+ web3.to_checksum_address(spender_address),
61
+ amount,
62
+ ],
63
+ )
64
+ return {
65
+ "to": web3.to_checksum_address(token_address),
66
+ "from": web3.to_checksum_address(from_address),
67
+ "data": data,
68
+ "chainId": chain_id,
69
+ }
70
+
71
+
72
+ async def build_send_transaction(
73
+ from_address: str,
74
+ to_address: str,
75
+ token_address: str | None,
76
+ chain_id: int,
77
+ amount: int,
78
+ ) -> dict:
79
+ async with web3_from_chain_id(chain_id) as web3:
80
+ from_checksum = web3.to_checksum_address(from_address)
81
+ to_checksum = web3.to_checksum_address(to_address)
82
+
83
+ if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
84
+ return {
85
+ "to": to_checksum,
86
+ "from": from_checksum,
87
+ "value": amount,
88
+ "chainId": chain_id,
89
+ }
90
+
91
+ token_checksum = web3.to_checksum_address(token_address)
92
+ contract = web3.eth.contract(address=token_checksum, abi=ERC20_ABI)
93
+ data = contract.encode_abi("transfer", [to_checksum, amount])
94
+
95
+ return {
96
+ "to": token_checksum,
97
+ "from": from_checksum,
98
+ "data": data,
99
+ "chainId": chain_id,
100
+ }
@@ -2,7 +2,7 @@
2
2
  EVM helper utilities for common blockchain operations.
3
3
 
4
4
  This module provides reusable functions for EVM-related operations that are shared
5
- across multiple adapters, extracted from evm_transaction_adapter.
5
+ across multiple adapters.
6
6
  """
7
7
 
8
8
  import json
@@ -0,0 +1,191 @@
1
+ import asyncio
2
+ from collections.abc import Callable
3
+
4
+ from loguru import logger
5
+ from web3 import AsyncWeb3
6
+
7
+ from wayfinder_paths.core.utils.web3 import (
8
+ get_transaction_chain_id,
9
+ web3_from_chain_id,
10
+ web3s_from_chain_id,
11
+ )
12
+
13
+ PRE_EIP_1559_CHAIN_IDS: set = {56, 42161}
14
+
15
+ SUGGESTED_PRIORITY_FEE_MULTIPLIER = 1.5
16
+ SUGGESTED_GAS_PRICE_MULTIPLIER = 1.5
17
+
18
+
19
+ def _get_transaction_from_address(transaction: dict) -> str:
20
+ if "from" not in transaction:
21
+ raise ValueError("Transaction does not contain from address")
22
+ return AsyncWeb3.to_checksum_address(transaction["from"])
23
+
24
+
25
+ async def nonce_transaction(transaction: dict):
26
+ transaction = transaction.copy()
27
+
28
+ from_address = _get_transaction_from_address(transaction)
29
+
30
+ async def _get_nonce(web3: AsyncWeb3, from_address: str) -> int:
31
+ return await web3.eth.get_transaction_count(from_address)
32
+
33
+ async with web3s_from_chain_id(get_transaction_chain_id(transaction)) as web3s:
34
+ nonces = await asyncio.gather(
35
+ *[_get_nonce(web3, from_address) for web3 in web3s]
36
+ )
37
+
38
+ nonce = max(nonces)
39
+ transaction["nonce"] = nonce
40
+
41
+ return transaction
42
+
43
+
44
+ async def gas_price_transaction(
45
+ transaction: dict,
46
+ gas_price_multiplier: float = SUGGESTED_GAS_PRICE_MULTIPLIER,
47
+ priority_fee_multiplier: float = SUGGESTED_PRIORITY_FEE_MULTIPLIER,
48
+ ):
49
+ transaction = transaction.copy()
50
+
51
+ async def _get_gas_price(web3: AsyncWeb3) -> int:
52
+ return await web3.eth.gas_price
53
+
54
+ async def _get_base_fee(web3: AsyncWeb3) -> int:
55
+ latest_block = await web3.eth.get_block("latest")
56
+ return latest_block.baseFeePerGas
57
+
58
+ async def _get_priority_fee(web3: AsyncWeb3) -> int:
59
+ lookback_blocks = 10
60
+ percentile = 80
61
+ fee_history = await web3.eth.fee_history(
62
+ lookback_blocks, "latest", [percentile]
63
+ )
64
+ historical_priority_fees = [i[0] for i in fee_history.reward]
65
+ return sum(historical_priority_fees) // len(historical_priority_fees)
66
+
67
+ chain_id = get_transaction_chain_id(transaction)
68
+ async with web3s_from_chain_id(chain_id) as web3s:
69
+ if chain_id in PRE_EIP_1559_CHAIN_IDS:
70
+ gas_prices = await asyncio.gather(*[_get_gas_price(web3) for web3 in web3s])
71
+ gas_price = max(gas_prices)
72
+
73
+ transaction["gasPrice"] = int(gas_price * gas_price_multiplier)
74
+ elif chain_id == 999:
75
+ # HyperEVM big blocks fetch base gas price from a different RPC method. Priority fee = 0 is # grandfathered in from Django, not sure what's right here.
76
+ big_block_gas_prices = await asyncio.gather(
77
+ *[web3.hype.big_block_gas_price() for web3 in web3s]
78
+ )
79
+ big_block_gas_price = max(big_block_gas_prices)
80
+
81
+ transaction["maxFeePerGas"] = int(
82
+ big_block_gas_price * priority_fee_multiplier
83
+ )
84
+ transaction["maxPriorityFeePerGas"] = 0
85
+ else:
86
+ base_fees = await asyncio.gather(*[_get_base_fee(web3) for web3 in web3s])
87
+ priority_fees = await asyncio.gather(
88
+ *[_get_priority_fee(web3) for web3 in web3s]
89
+ )
90
+
91
+ base_fee = max(base_fees)
92
+ priority_fee = max(priority_fees)
93
+
94
+ # The next block can grow base fee by up to 12.5%, we give a flew blocks of landing room. log_1.125(2) ~ 6 blocks of landing room. GPT says this is also what Metamask does.
95
+ max_base_fee_growth_multiplier = 2
96
+ transaction["maxFeePerGas"] = int(
97
+ base_fee * max_base_fee_growth_multiplier
98
+ + priority_fee * priority_fee_multiplier
99
+ )
100
+ transaction["maxPriorityFeePerGas"] = int(
101
+ priority_fee * priority_fee_multiplier
102
+ )
103
+
104
+ return transaction
105
+
106
+
107
+ async def gas_limit_transaction(transaction: dict):
108
+ transaction = transaction.copy()
109
+
110
+ # prevents RPCs from taking this as a serious limit
111
+ transaction.pop("gas", None)
112
+
113
+ async def _estimate_gas(web3: AsyncWeb3, transaction: dict) -> int:
114
+ try:
115
+ return await web3.eth.estimate_gas(transaction, block_identifier="pending")
116
+ except Exception as e:
117
+ logger.info(
118
+ f"Failed to estimate gas using {web3.provider.endpoint_uri}. Error: {e}"
119
+ )
120
+ return 0
121
+
122
+ async with web3s_from_chain_id(get_transaction_chain_id(transaction)) as web3s:
123
+ gas_limits = await asyncio.gather(
124
+ *[_estimate_gas(web3, transaction) for web3 in web3s]
125
+ )
126
+
127
+ gas_limit = max(gas_limits)
128
+ if gas_limit == 0:
129
+ logger.error("Gas estimation failed on all RPCs")
130
+ raise Exception("Gas estimation failed on all RPCs")
131
+ transaction["gas"] = gas_limit
132
+
133
+ return transaction
134
+
135
+
136
+ async def broadcast_transaction(chain_id, signed_transaction: bytes) -> str:
137
+ async with web3_from_chain_id(chain_id) as web3:
138
+ tx_hash = await web3.eth.send_raw_transaction(signed_transaction)
139
+ return tx_hash.hex()
140
+
141
+
142
+ async def wait_for_transaction_receipt(
143
+ chain_id: int,
144
+ txn_hash: str,
145
+ poll_interval: float = 0.1,
146
+ timeout: int = 300,
147
+ confirmations: int = 3,
148
+ ) -> dict:
149
+ async def _wait_for_receipt(web3: AsyncWeb3, tx_hash: str) -> dict:
150
+ return await web3.eth.wait_for_transaction_receipt(
151
+ tx_hash, poll_latency=poll_interval, timeout=timeout
152
+ )
153
+
154
+ async def _get_block_number(web3: AsyncWeb3) -> int:
155
+ return await web3.eth.block_number
156
+
157
+ async with web3s_from_chain_id(chain_id) as web3s:
158
+ tasks = [
159
+ asyncio.create_task(_wait_for_receipt(web3, txn_hash)) for web3 in web3s
160
+ ]
161
+ done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
162
+ for task in pending:
163
+ task.cancel()
164
+ receipt = done.pop().result()
165
+
166
+ target_block = receipt["blockNumber"] + confirmations - 1
167
+ while (
168
+ max(await asyncio.gather(*[_get_block_number(w) for w in web3s]))
169
+ < target_block
170
+ ):
171
+ await asyncio.sleep(poll_interval)
172
+ return receipt
173
+
174
+
175
+ async def send_transaction(
176
+ transaction: dict, sign_callback: Callable, wait_for_receipt=True
177
+ ) -> str:
178
+ logger.info(f"Broadcasting transaction {transaction.get('to', 'unknown')[:10]}...")
179
+ chain_id = get_transaction_chain_id(transaction)
180
+ transaction = await gas_limit_transaction(transaction)
181
+ transaction = await nonce_transaction(transaction)
182
+ transaction = await gas_price_transaction(transaction)
183
+ signed_transaction = await sign_callback(transaction)
184
+ txn_hash = await broadcast_transaction(chain_id, signed_transaction)
185
+ logger.info(f"Transaction broadcasted: {txn_hash}")
186
+ if wait_for_receipt:
187
+ await wait_for_transaction_receipt(chain_id, txn_hash)
188
+ return txn_hash
189
+
190
+
191
+ # TODO: HypeEVM Big Blocks: Setting and detecting
@@ -0,0 +1,66 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from web3 import AsyncHTTPProvider, AsyncWeb3
4
+ from web3.middleware import ExtraDataToPOAMiddleware
5
+ from web3.module import Module
6
+
7
+ from wayfinder_paths.core.config import CONFIG
8
+
9
+ POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
10
+
11
+
12
+ class HyperModule(Module):
13
+ def __init__(self, w3):
14
+ super().__init__(w3)
15
+
16
+ async def big_block_gas_price(self):
17
+ big_block_gas_price = await self.w3.manager.coro_request(
18
+ "eth_bigBlockGasPrice", []
19
+ )
20
+ return int(big_block_gas_price, 16)
21
+
22
+
23
+ def _get_rpcs_for_chain_id(chain_id: int) -> list:
24
+ rpcs = CONFIG.get("strategy", {}).get("rpc_urls", {}).get(str(chain_id))
25
+ if rpcs is None:
26
+ raise ValueError(f"No RPCs configured for chain ID {chain_id}")
27
+ return rpcs
28
+
29
+
30
+ def _get_web3(rpc: str, chain_id: int) -> AsyncWeb3:
31
+ web3 = AsyncWeb3(AsyncHTTPProvider(rpc))
32
+ if chain_id in POA_MIDDLEWARE_CHAIN_IDS:
33
+ web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
34
+ if chain_id == 999:
35
+ web3.attach_modules({"hype": (HyperModule)})
36
+ return web3
37
+
38
+
39
+ def get_transaction_chain_id(transaction: dict) -> int:
40
+ if "chainId" not in transaction:
41
+ raise ValueError("Transaction does not contain chainId")
42
+ return int(transaction["chainId"])
43
+
44
+
45
+ def get_web3s_from_chain_id(chain_id: int) -> list[AsyncWeb3]:
46
+ rpcs = _get_rpcs_for_chain_id(chain_id)
47
+ return [_get_web3(rpc, chain_id) for rpc in rpcs]
48
+
49
+
50
+ @asynccontextmanager
51
+ async def web3s_from_chain_id(chain_id: int):
52
+ web3s = get_web3s_from_chain_id(chain_id)
53
+ try:
54
+ yield web3s
55
+ finally:
56
+ for web3 in web3s:
57
+ await web3.provider.disconnect()
58
+
59
+
60
+ @asynccontextmanager
61
+ async def web3_from_chain_id(chain_id: int):
62
+ web3s = get_web3s_from_chain_id(chain_id)
63
+ try:
64
+ yield web3s[0]
65
+ finally:
66
+ await web3s[0].provider.disconnect()
@@ -14,19 +14,21 @@ from loguru import logger
14
14
 
15
15
  from wayfinder_paths.core.config import StrategyJobConfig
16
16
  from wayfinder_paths.core.engine.StrategyJob import StrategyJob
17
+ from wayfinder_paths.core.utils.evm_helpers import resolve_private_key_for_from_address
18
+ from wayfinder_paths.core.utils.web3 import get_transaction_chain_id, web3_from_chain_id
17
19
 
18
20
 
19
21
  def load_strategy(
20
22
  strategy_name: str,
21
23
  *,
22
- strategy_config: dict | None = None,
24
+ config: StrategyJobConfig,
23
25
  ):
24
26
  """
25
27
  Dynamically load a strategy by name
26
28
 
27
29
  Args:
28
30
  strategy_name: Name of the strategy to load (directory name in strategies/)
29
- strategy_config: Configuration dict for the strategy
31
+ config: StrategyJobConfig instance containing user and strategy configuration
30
32
 
31
33
  Returns:
32
34
  Strategy instance
@@ -68,7 +70,34 @@ def load_strategy(
68
70
  if strategy_class is None:
69
71
  raise ValueError(f"No Strategy class found in {module_path}")
70
72
 
71
- return strategy_class(config=strategy_config)
73
+ # Get wallet addresses from strategy_config (enriched from wallets array in config.json)
74
+ main_wallet = config.strategy_config.get("main_wallet") or {}
75
+ strategy_wallet = config.strategy_config.get("strategy_wallet") or {}
76
+ main_wallet_address = main_wallet.get("address")
77
+ strategy_wallet_address = strategy_wallet.get("address")
78
+
79
+ async def main_wallet_signing_callback(transaction):
80
+ private_key = resolve_private_key_for_from_address(
81
+ main_wallet_address, config.strategy_config
82
+ )
83
+ async with web3_from_chain_id(get_transaction_chain_id(transaction)) as web3:
84
+ signed = web3.eth.account.sign_transaction(transaction, private_key)
85
+ return signed.raw_transaction.hex()
86
+
87
+ async def strategy_wallet_signing_callback(transaction):
88
+ private_key = resolve_private_key_for_from_address(
89
+ strategy_wallet_address,
90
+ config.strategy_config,
91
+ )
92
+ async with web3_from_chain_id(get_transaction_chain_id(transaction)) as web3:
93
+ signed = web3.eth.account.sign_transaction(transaction, private_key)
94
+ return signed.raw_transaction.hex()
95
+
96
+ return strategy_class(
97
+ config=config.strategy_config,
98
+ main_wallet_signing_callback=main_wallet_signing_callback,
99
+ strategy_wallet_signing_callback=strategy_wallet_signing_callback,
100
+ )
72
101
 
73
102
 
74
103
  def load_config(
@@ -133,10 +162,12 @@ async def run_strategy(
133
162
  # Load configuration with strategy name for wallet lookup
134
163
  logger.debug(f"Config path provided: {config_path}")
135
164
  config = load_config(config_path, strategy_name=strategy_name)
165
+ main_wallet_cfg = config.strategy_config.get("main_wallet") or {}
166
+ strategy_wallet_cfg = config.strategy_config.get("strategy_wallet") or {}
136
167
  logger.debug(
137
168
  "Loaded config: wallets(main={} strategy={})",
138
- config.user.main_wallet_address or "none",
139
- config.user.strategy_wallet_address or "none",
169
+ main_wallet_cfg.get("address") or "none",
170
+ strategy_wallet_cfg.get("address") or "none",
140
171
  )
141
172
 
142
173
  # Validate required configuration
@@ -145,7 +176,7 @@ async def run_strategy(
145
176
  # Load strategy with the enriched config
146
177
  strategy = load_strategy(
147
178
  strategy_name,
148
- strategy_config=config.strategy_config,
179
+ config=config,
149
180
  )
150
181
  logger.info(f"Loaded strategy: {strategy.name}")
151
182