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.
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
@@ -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()
@@ -1,4 +1,4 @@
1
- from wayfinder_paths.core.utils.evm_helpers import ERC20_ABI
1
+ from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
2
2
 
3
3
 
4
4
  def any_erc20_function(token_address: str) -> dict:
@@ -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
 
@@ -197,6 +228,10 @@ async def run_strategy(
197
228
  result = await strategy_job.execute_strategy("update")
198
229
  logger.info(f"Update result: {result}")
199
230
 
231
+ elif action == "exit":
232
+ result = await strategy_job.execute_strategy("exit")
233
+ logger.info(f"Exit result: {result}")
234
+
200
235
  elif action == "partial-liquidate":
201
236
  usd_value = kwargs.get("amount")
202
237
  if not usd_value:
@@ -292,6 +327,7 @@ def main():
292
327
  "withdraw",
293
328
  "status",
294
329
  "update",
330
+ "exit",
295
331
  "policy",
296
332
  "script",
297
333
  "partial-liquidate",