wayfinder-paths 0.1.10__py3-none-any.whl → 0.1.13__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.
- wayfinder_paths/adapters/balance_adapter/adapter.py +3 -7
- wayfinder_paths/adapters/brap_adapter/adapter.py +10 -13
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +6 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
- wayfinder_paths/adapters/pool_adapter/README.md +4 -19
- wayfinder_paths/adapters/pool_adapter/adapter.py +4 -29
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/core/clients/AuthClient.py +2 -2
- wayfinder_paths/core/clients/BRAPClient.py +2 -2
- wayfinder_paths/core/clients/HyperlendClient.py +2 -2
- wayfinder_paths/core/clients/PoolClient.py +18 -54
- wayfinder_paths/core/clients/TokenClient.py +3 -3
- wayfinder_paths/core/clients/WalletClient.py +2 -2
- wayfinder_paths/core/clients/WayfinderClient.py +9 -10
- wayfinder_paths/core/clients/protocols.py +1 -7
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/services/base.py +0 -55
- wayfinder_paths/core/services/local_evm_txn.py +37 -134
- wayfinder_paths/core/strategies/Strategy.py +3 -3
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -28
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +10 -8
- wayfinder_paths/scripts/create_strategy.py +5 -5
- wayfinder_paths/scripts/make_wallets.py +5 -5
- wayfinder_paths/scripts/run_strategy.py +3 -3
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
- wayfinder_paths/templates/adapter/README.md +1 -1
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +15 -18
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +46 -48
- wayfinder_paths/CONFIG_GUIDE.md +0 -390
- wayfinder_paths/config.example.json +0 -22
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/WHEEL +0 -0
|
@@ -4,16 +4,11 @@ from typing import Any
|
|
|
4
4
|
from eth_account import Account
|
|
5
5
|
from eth_utils import to_checksum_address
|
|
6
6
|
from loguru import logger
|
|
7
|
-
from web3 import AsyncHTTPProvider, AsyncWeb3
|
|
7
|
+
from web3 import AsyncHTTPProvider, AsyncWeb3
|
|
8
|
+
from web3.middleware import ExtraDataToPOAMiddleware
|
|
9
|
+
from web3.module import Module
|
|
8
10
|
|
|
9
|
-
from wayfinder_paths.core.constants import (
|
|
10
|
-
ZERO_ADDRESS,
|
|
11
|
-
)
|
|
12
11
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
13
|
-
from wayfinder_paths.core.constants.erc20_abi import (
|
|
14
|
-
ERC20_APPROVAL_ABI,
|
|
15
|
-
ERC20_MINIMAL_ABI,
|
|
16
|
-
)
|
|
17
12
|
from wayfinder_paths.core.services.base import EvmTxn
|
|
18
13
|
from wayfinder_paths.core.utils.evm_helpers import (
|
|
19
14
|
resolve_private_key_for_from_address,
|
|
@@ -27,6 +22,7 @@ GAS_LIMIT_BUFFER_MULTIPLIER = 1.5
|
|
|
27
22
|
|
|
28
23
|
# Chains that don't support EIP-1559 (London) and need legacy gas pricing
|
|
29
24
|
PRE_LONDON_GAS_CHAIN_IDS: set[int] = {56, 42161}
|
|
25
|
+
POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
|
|
30
26
|
|
|
31
27
|
|
|
32
28
|
def _looks_like_revert_error(error: Any) -> bool:
|
|
@@ -47,12 +43,23 @@ def _looks_like_revert_error(error: Any) -> bool:
|
|
|
47
43
|
)
|
|
48
44
|
|
|
49
45
|
|
|
46
|
+
class HyperModule(Module):
|
|
47
|
+
def __init__(self, w3):
|
|
48
|
+
super().__init__(w3)
|
|
49
|
+
|
|
50
|
+
async def big_block_gas_price(self):
|
|
51
|
+
big_block_gas_price = await self.w3.manager.coro_request(
|
|
52
|
+
"eth_bigBlockGasPrice", []
|
|
53
|
+
)
|
|
54
|
+
return int(big_block_gas_price, 16)
|
|
55
|
+
|
|
56
|
+
|
|
50
57
|
class LocalEvmTxn(EvmTxn):
|
|
51
58
|
"""
|
|
52
|
-
Local wallet provider using private keys stored in config.json or
|
|
59
|
+
Local wallet provider using private keys stored in config.json or config.json.
|
|
53
60
|
|
|
54
61
|
This provider implements the current default behavior:
|
|
55
|
-
- Resolves private keys from config.json or
|
|
62
|
+
- Resolves private keys from config.json or config.json
|
|
56
63
|
- Signs transactions using eth_account
|
|
57
64
|
- Broadcasts transactions via RPC
|
|
58
65
|
"""
|
|
@@ -60,104 +67,15 @@ class LocalEvmTxn(EvmTxn):
|
|
|
60
67
|
def __init__(self, config: dict[str, Any] | None = None):
|
|
61
68
|
self.config = config or {}
|
|
62
69
|
self.logger = logger.bind(provider="LocalWalletProvider")
|
|
63
|
-
# Cache web3 instances per chain to avoid load balancer inconsistency
|
|
64
|
-
self._web3_cache: dict[int, AsyncWeb3] = {}
|
|
65
70
|
|
|
66
71
|
def get_web3(self, chain_id: int) -> AsyncWeb3:
|
|
67
|
-
# Reuse cached instance to ensure consistent RPC node for reads after writes
|
|
68
|
-
if chain_id in self._web3_cache:
|
|
69
|
-
return self._web3_cache[chain_id]
|
|
70
72
|
rpc_url = self._resolve_rpc_url(chain_id)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
address: str,
|
|
78
|
-
token_address: str | None,
|
|
79
|
-
chain_id: int,
|
|
80
|
-
block_identifier: int | str | None = None,
|
|
81
|
-
) -> tuple[bool, Any]:
|
|
82
|
-
"""
|
|
83
|
-
Get balance for an address at a specific block.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
address: Address to query balance for
|
|
87
|
-
token_address: ERC20 token address, or None for native token
|
|
88
|
-
chain_id: Chain ID
|
|
89
|
-
block_identifier: Block to query at. Can be:
|
|
90
|
-
- int: specific block number (for pinning to tx block)
|
|
91
|
-
- "safe": OP Stack safe block (data posted to L1)
|
|
92
|
-
- "finalized": fully finalized block
|
|
93
|
-
- None/"latest": current head (default, but avoid after txs)
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Tuple of (success, balance_integer_or_error_message)
|
|
97
|
-
"""
|
|
98
|
-
w3 = self.get_web3(chain_id)
|
|
99
|
-
try:
|
|
100
|
-
checksum_addr = to_checksum_address(address)
|
|
101
|
-
block_id = block_identifier if block_identifier is not None else "latest"
|
|
102
|
-
|
|
103
|
-
if not token_address or token_address.lower() == ZERO_ADDRESS:
|
|
104
|
-
balance = await w3.eth.get_balance(
|
|
105
|
-
checksum_addr, block_identifier=block_id
|
|
106
|
-
)
|
|
107
|
-
return (True, int(balance))
|
|
108
|
-
|
|
109
|
-
token_checksum = to_checksum_address(token_address)
|
|
110
|
-
contract = w3.eth.contract(address=token_checksum, abi=ERC20_MINIMAL_ABI)
|
|
111
|
-
balance = await contract.functions.balanceOf(checksum_addr).call(
|
|
112
|
-
block_identifier=block_id
|
|
113
|
-
)
|
|
114
|
-
return (True, int(balance))
|
|
115
|
-
|
|
116
|
-
except Exception as exc: # noqa: BLE001
|
|
117
|
-
self.logger.error(f"Failed to get balance: {exc}")
|
|
118
|
-
return (False, f"Balance query failed: {exc}")
|
|
119
|
-
finally:
|
|
120
|
-
await self._close_web3(w3)
|
|
121
|
-
|
|
122
|
-
async def approve_token(
|
|
123
|
-
self,
|
|
124
|
-
token_address: str,
|
|
125
|
-
spender: str,
|
|
126
|
-
amount: int,
|
|
127
|
-
from_address: str,
|
|
128
|
-
chain_id: int,
|
|
129
|
-
wait_for_receipt: bool = True,
|
|
130
|
-
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
131
|
-
) -> tuple[bool, Any]:
|
|
132
|
-
try:
|
|
133
|
-
token_checksum = to_checksum_address(token_address)
|
|
134
|
-
spender_checksum = to_checksum_address(spender)
|
|
135
|
-
from_checksum = to_checksum_address(from_address)
|
|
136
|
-
amount_int = int(amount)
|
|
137
|
-
|
|
138
|
-
w3_sync = Web3()
|
|
139
|
-
contract = w3_sync.eth.contract(
|
|
140
|
-
address=token_checksum, abi=ERC20_APPROVAL_ABI
|
|
141
|
-
)
|
|
142
|
-
transaction_data = contract.encode_abi(
|
|
143
|
-
"approve", args=[spender_checksum, amount_int]
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
approve_txn = {
|
|
147
|
-
"from": from_checksum,
|
|
148
|
-
"chainId": int(chain_id),
|
|
149
|
-
"to": token_checksum,
|
|
150
|
-
"data": transaction_data,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return await self.broadcast_transaction(
|
|
154
|
-
approve_txn,
|
|
155
|
-
wait_for_receipt=wait_for_receipt,
|
|
156
|
-
timeout=timeout,
|
|
157
|
-
)
|
|
158
|
-
except Exception as exc: # noqa: BLE001
|
|
159
|
-
self.logger.error(f"ERC20 approval failed: {exc}")
|
|
160
|
-
return (False, f"ERC20 approval failed: {exc}")
|
|
73
|
+
web3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
|
|
74
|
+
if chain_id in POA_MIDDLEWARE_CHAIN_IDS:
|
|
75
|
+
web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
76
|
+
if chain_id == 999:
|
|
77
|
+
web3.attach_modules({"hype": (HyperModule)})
|
|
78
|
+
return web3
|
|
161
79
|
|
|
162
80
|
def _validate_transaction(self, transaction: dict[str, Any]) -> dict[str, Any]:
|
|
163
81
|
tx = dict(transaction)
|
|
@@ -264,29 +182,29 @@ class LocalEvmTxn(EvmTxn):
|
|
|
264
182
|
*,
|
|
265
183
|
wait_for_receipt: bool = True,
|
|
266
184
|
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
267
|
-
confirmations: int =
|
|
185
|
+
confirmations: int = 1,
|
|
268
186
|
) -> tuple[bool, Any]:
|
|
269
187
|
try:
|
|
270
188
|
chain_id = transaction["chainId"]
|
|
271
189
|
from_address = transaction["from"]
|
|
272
190
|
|
|
273
|
-
|
|
191
|
+
web3 = self.get_web3(chain_id)
|
|
274
192
|
try:
|
|
275
193
|
transaction = self._validate_transaction(transaction)
|
|
276
|
-
transaction = await self._nonce_transaction(transaction,
|
|
277
|
-
transaction = await self._gas_limit_transaction(transaction,
|
|
194
|
+
transaction = await self._nonce_transaction(transaction, web3)
|
|
195
|
+
transaction = await self._gas_limit_transaction(transaction, web3)
|
|
278
196
|
transaction = await self._gas_price_transaction(
|
|
279
|
-
transaction, chain_id,
|
|
197
|
+
transaction, chain_id, web3
|
|
280
198
|
)
|
|
281
199
|
|
|
282
200
|
signed_tx = self._sign_transaction(transaction, from_address)
|
|
283
201
|
|
|
284
|
-
tx_hash = await
|
|
202
|
+
tx_hash = await web3.eth.send_raw_transaction(signed_tx)
|
|
285
203
|
tx_hash_hex = tx_hash.hex()
|
|
286
204
|
|
|
287
205
|
result: dict[str, Any] = {"tx_hash": tx_hash_hex}
|
|
288
206
|
if wait_for_receipt:
|
|
289
|
-
receipt = await
|
|
207
|
+
receipt = await web3.eth.wait_for_transaction_receipt(
|
|
290
208
|
tx_hash, timeout=timeout
|
|
291
209
|
)
|
|
292
210
|
result["receipt"] = self._format_receipt(receipt)
|
|
@@ -299,32 +217,19 @@ class LocalEvmTxn(EvmTxn):
|
|
|
299
217
|
False,
|
|
300
218
|
f"Transaction reverted (status={receipt_status}): {tx_hash_hex}",
|
|
301
219
|
)
|
|
302
|
-
# Check if transaction reverted (status=0)
|
|
303
|
-
# Handle both dict-like and attribute access for web3.py receipts
|
|
304
|
-
receipt_status = (
|
|
305
|
-
receipt.get("status")
|
|
306
|
-
if hasattr(receipt, "get")
|
|
307
|
-
else getattr(receipt, "status", None)
|
|
308
|
-
)
|
|
309
|
-
self.logger.debug(
|
|
310
|
-
f"Transaction {tx_hash_hex} receipt status: {receipt_status} (type: {type(receipt_status).__name__})"
|
|
311
|
-
)
|
|
312
|
-
if receipt_status == 0:
|
|
313
|
-
self.logger.error(f"Transaction reverted: {tx_hash_hex}")
|
|
314
|
-
return (False, f"Transaction reverted: {tx_hash_hex}")
|
|
315
220
|
|
|
316
221
|
# Wait for additional confirmations if requested
|
|
317
222
|
if confirmations > 0:
|
|
318
223
|
tx_block = result["receipt"].get("blockNumber")
|
|
319
224
|
if tx_block:
|
|
320
225
|
await self._wait_for_confirmations(
|
|
321
|
-
|
|
226
|
+
web3, tx_block, confirmations
|
|
322
227
|
)
|
|
323
228
|
|
|
324
229
|
return (True, result)
|
|
325
230
|
|
|
326
231
|
finally:
|
|
327
|
-
await self._close_web3(
|
|
232
|
+
await self._close_web3(web3)
|
|
328
233
|
|
|
329
234
|
except Exception as exc: # noqa: BLE001
|
|
330
235
|
self.logger.error(f"Transaction broadcast failed: {exc}")
|
|
@@ -362,14 +267,12 @@ class LocalEvmTxn(EvmTxn):
|
|
|
362
267
|
def _resolve_rpc_url(self, chain_id: int) -> str:
|
|
363
268
|
return resolve_rpc_url(chain_id, self.config or {}, None)
|
|
364
269
|
|
|
365
|
-
async def _close_web3(self,
|
|
366
|
-
# Don't close cached connections - we want to reuse them for consistency
|
|
367
|
-
if w3 in self._web3_cache.values():
|
|
368
|
-
return
|
|
270
|
+
async def _close_web3(self, web3: AsyncWeb3) -> None:
|
|
369
271
|
try:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
272
|
+
if hasattr(web3.provider, "disconnect"):
|
|
273
|
+
await web3.provider.disconnect()
|
|
274
|
+
except Exception as e: # noqa: BLE001
|
|
275
|
+
self.logger.debug(f"Error disconnecting provider: {e}")
|
|
373
276
|
|
|
374
277
|
async def _wait_for_confirmations(
|
|
375
278
|
self, w3: AsyncWeb3, tx_block: int, confirmations: int
|
|
@@ -152,21 +152,21 @@ class Strategy(ABC):
|
|
|
152
152
|
|
|
153
153
|
@staticmethod
|
|
154
154
|
async def policies() -> list[str]:
|
|
155
|
-
"""Return policy strings for this strategy
|
|
155
|
+
"""Return policy strings for this strategy."""
|
|
156
156
|
raise NotImplementedError
|
|
157
157
|
|
|
158
158
|
@abstractmethod
|
|
159
159
|
async def _status(self) -> StatusDict:
|
|
160
160
|
"""
|
|
161
161
|
Return status payload. Subclasses should implement this.
|
|
162
|
-
Should include
|
|
162
|
+
Should include keys (portfolio_value, net_deposit, strategy_status).
|
|
163
163
|
Backward-compatible keys (active_amount, total_earned) may also be included.
|
|
164
164
|
"""
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
167
|
async def status(self) -> StatusDict:
|
|
168
168
|
"""
|
|
169
|
-
Wrapper to compute and return strategy status
|
|
169
|
+
Wrapper to compute and return strategy status and record a snapshot.
|
|
170
170
|
Here we simply delegate to _status for compatibility.
|
|
171
171
|
"""
|
|
172
172
|
|
|
@@ -46,12 +46,19 @@ DEFAULT_TOKEN_REWARDS = [
|
|
|
46
46
|
},
|
|
47
47
|
]
|
|
48
48
|
|
|
49
|
+
DEFAULT_FEE_DESCRIPTION = """Wayfinder deducts a 10% performance fee on profits generated by this vault. Fees are collected from the assets in the vault.
|
|
50
|
+
|
|
51
|
+
If fees remain unpaid, Wayfinder may pause automated management of this vault."""
|
|
52
|
+
|
|
49
53
|
|
|
50
54
|
class StratDescriptor(BaseModel):
|
|
51
55
|
description: str
|
|
52
56
|
|
|
53
57
|
summary: str
|
|
54
58
|
|
|
59
|
+
risk_description: str
|
|
60
|
+
fee_description: str = DEFAULT_FEE_DESCRIPTION
|
|
61
|
+
|
|
55
62
|
gas_token_symbol: str
|
|
56
63
|
gas_token_id: str
|
|
57
64
|
deposit_token_id: str
|
|
@@ -11,7 +11,6 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from loguru import logger
|
|
14
|
-
from web3 import AsyncHTTPProvider, AsyncWeb3
|
|
15
14
|
|
|
16
15
|
from wayfinder_paths.core.constants.base import CHAIN_CODE_TO_ID
|
|
17
16
|
|
|
@@ -79,39 +78,17 @@ def resolve_rpc_url(
|
|
|
79
78
|
if chain_id is not None and isinstance(mapping, dict):
|
|
80
79
|
by_int = mapping.get(chain_id)
|
|
81
80
|
if by_int:
|
|
81
|
+
if isinstance(by_int, list):
|
|
82
|
+
return str(by_int[0])
|
|
82
83
|
return str(by_int)
|
|
83
84
|
by_str = mapping.get(str(chain_id))
|
|
84
85
|
if by_str:
|
|
86
|
+
if isinstance(by_str, list):
|
|
87
|
+
return str(by_str[0])
|
|
85
88
|
return str(by_str)
|
|
86
89
|
raise ValueError("RPC URL not provided. Set strategy.rpc_urls in config.json.")
|
|
87
90
|
|
|
88
91
|
|
|
89
|
-
async def get_next_nonce(
|
|
90
|
-
from_address: str, rpc_url: str, use_latest: bool = False
|
|
91
|
-
) -> int:
|
|
92
|
-
"""
|
|
93
|
-
Get the next nonce for the given address.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
from_address: Address to get nonce for
|
|
97
|
-
rpc_url: RPC URL to connect to
|
|
98
|
-
use_latest: If True, use 'latest' block instead of 'pending'
|
|
99
|
-
|
|
100
|
-
Returns:
|
|
101
|
-
Next nonce as integer
|
|
102
|
-
"""
|
|
103
|
-
w3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
|
|
104
|
-
try:
|
|
105
|
-
if use_latest:
|
|
106
|
-
return await w3.eth.get_transaction_count(from_address, "latest")
|
|
107
|
-
return await w3.eth.get_transaction_count(from_address)
|
|
108
|
-
finally:
|
|
109
|
-
try:
|
|
110
|
-
await w3.provider.session.close()
|
|
111
|
-
except Exception:
|
|
112
|
-
pass
|
|
113
|
-
|
|
114
|
-
|
|
115
92
|
def resolve_private_key_for_from_address(
|
|
116
93
|
from_address: str, config: dict[str, Any]
|
|
117
94
|
) -> str | None:
|
|
@@ -160,7 +137,7 @@ def resolve_private_key_for_from_address(
|
|
|
160
137
|
if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
|
|
161
138
|
return strategy_pk
|
|
162
139
|
|
|
163
|
-
# No fallback - private keys must be in config or
|
|
140
|
+
# No fallback - private keys must be in config or config.json
|
|
164
141
|
return None
|
|
165
142
|
|
|
166
143
|
|
|
@@ -6,11 +6,7 @@ from eth_account import Account
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def make_random_wallet() -> dict[str, str]:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Returns a mapping with keys: "address" and "private_key_hex" (0x-prefixed).
|
|
12
|
-
"""
|
|
13
|
-
acct = Account.create() # uses os.urandom
|
|
9
|
+
acct = Account.create()
|
|
14
10
|
return {
|
|
15
11
|
"address": acct.address,
|
|
16
12
|
"private_key_hex": acct.key.hex(),
|
|
@@ -22,33 +18,31 @@ def _load_existing_wallets(file_path: Path) -> list[dict[str, Any]]:
|
|
|
22
18
|
return []
|
|
23
19
|
try:
|
|
24
20
|
parsed = json.loads(file_path.read_text())
|
|
25
|
-
if isinstance(parsed, list):
|
|
26
|
-
return parsed
|
|
27
21
|
if isinstance(parsed, dict):
|
|
28
22
|
wallets = parsed.get("wallets")
|
|
29
23
|
if isinstance(wallets, list):
|
|
30
24
|
return wallets
|
|
31
25
|
return []
|
|
32
26
|
except Exception:
|
|
33
|
-
# If the file is malformed, start fresh rather than raising.
|
|
34
27
|
return []
|
|
35
28
|
|
|
36
29
|
|
|
37
30
|
def _save_wallets(file_path: Path, wallets: list[dict[str, Any]]) -> None:
|
|
38
|
-
|
|
31
|
+
config = {}
|
|
32
|
+
if file_path.exists():
|
|
33
|
+
try:
|
|
34
|
+
config = json.loads(file_path.read_text())
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
39
38
|
sorted_wallets = sorted(wallets, key=lambda w: w.get("address", ""))
|
|
40
|
-
|
|
39
|
+
config["wallets"] = sorted_wallets
|
|
40
|
+
file_path.write_text(json.dumps(config, indent=2))
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def write_wallet_to_json(
|
|
44
|
-
wallet: dict[str, str], out_dir: str | Path = ".", filename: str = "
|
|
44
|
+
wallet: dict[str, str], out_dir: str | Path = ".", filename: str = "config.json"
|
|
45
45
|
) -> Path:
|
|
46
|
-
"""Create or update a wallets.json with the provided wallet.
|
|
47
|
-
|
|
48
|
-
- Ensures the output directory exists.
|
|
49
|
-
- Merges with existing entries keyed by address (updates if present, appends otherwise).
|
|
50
|
-
- Writes a pretty-printed JSON list of wallet objects.
|
|
51
|
-
"""
|
|
52
46
|
out_dir_path = Path(out_dir)
|
|
53
47
|
out_dir_path.mkdir(parents=True, exist_ok=True)
|
|
54
48
|
file_path = out_dir_path / filename
|
|
@@ -71,7 +65,6 @@ def write_wallet_to_json(
|
|
|
71
65
|
|
|
72
66
|
|
|
73
67
|
def load_wallets(
|
|
74
|
-
out_dir: str | Path = ".", filename: str = "
|
|
68
|
+
out_dir: str | Path = ".", filename: str = "config.json"
|
|
75
69
|
) -> list[dict[str, Any]]:
|
|
76
|
-
"""Public helper to read wallets.json as a list of wallet dicts."""
|
|
77
70
|
return _load_existing_wallets(Path(out_dir) / filename)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Wallet Abstraction Layer
|
|
2
2
|
|
|
3
|
-
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or
|
|
3
|
+
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or config.json, while `WalletManager` resolves which provider to use at runtime.
|
|
4
4
|
|
|
5
5
|
## Pieces
|
|
6
6
|
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -135,14 +135,16 @@ async def run_strategy(
|
|
|
135
135
|
# Load configuration with strategy name for wallet lookup
|
|
136
136
|
logger.debug(f"Config path provided: {config_path}")
|
|
137
137
|
config = load_config(config_path, strategy_name=strategy_name)
|
|
138
|
-
|
|
139
|
-
"Loaded config: creds=%s wallets(main=%s strategy=%s)",
|
|
138
|
+
creds = (
|
|
140
139
|
"yes"
|
|
141
140
|
if (config.user.username and config.user.password)
|
|
142
141
|
or config.user.refresh_token
|
|
143
|
-
else "no"
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
else "no"
|
|
143
|
+
)
|
|
144
|
+
main_wallet = config.user.main_wallet_address or "none"
|
|
145
|
+
strategy_wallet = config.user.strategy_wallet_address or "none"
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Loaded config: creds={creds} wallets(main={main_wallet} strategy={strategy_wallet})"
|
|
146
148
|
)
|
|
147
149
|
|
|
148
150
|
# Load strategy with the enriched config
|
|
@@ -157,13 +159,13 @@ async def run_strategy(
|
|
|
157
159
|
|
|
158
160
|
# Setup strategy job
|
|
159
161
|
logger.info("Setting up strategy job...")
|
|
160
|
-
|
|
161
|
-
"Auth mode: %s",
|
|
162
|
+
auth_mode = (
|
|
162
163
|
"credentials"
|
|
163
164
|
if (config.user.username and config.user.password)
|
|
164
165
|
or config.user.refresh_token
|
|
165
|
-
else "missing"
|
|
166
|
+
else "missing"
|
|
166
167
|
)
|
|
168
|
+
logger.debug(f"Auth mode: {auth_mode}")
|
|
167
169
|
await strategy_job.setup()
|
|
168
170
|
|
|
169
171
|
# Execute action
|
|
@@ -62,8 +62,8 @@ def main():
|
|
|
62
62
|
parser.add_argument(
|
|
63
63
|
"--wallets-file",
|
|
64
64
|
type=Path,
|
|
65
|
-
default=Path(__file__).parent.parent.parent / "
|
|
66
|
-
help="Path to
|
|
65
|
+
default=Path(__file__).parent.parent.parent / "config.json",
|
|
66
|
+
help="Path to config.json file",
|
|
67
67
|
)
|
|
68
68
|
parser.add_argument(
|
|
69
69
|
"--override",
|
|
@@ -121,9 +121,9 @@ def main():
|
|
|
121
121
|
print(f" Updated strategy.py with class name: {class_name}")
|
|
122
122
|
|
|
123
123
|
# Generate wallet with label matching directory name (strategy identifier)
|
|
124
|
-
# If
|
|
124
|
+
# If config.json doesn't exist, create it with a main wallet first
|
|
125
125
|
if not args.wallets_file.exists():
|
|
126
|
-
print(" Creating new
|
|
126
|
+
print(" Creating new config.json with main wallet...")
|
|
127
127
|
main_wallet = make_random_wallet()
|
|
128
128
|
main_wallet["label"] = "main"
|
|
129
129
|
write_wallet_to_json(
|
|
@@ -133,7 +133,7 @@ def main():
|
|
|
133
133
|
)
|
|
134
134
|
print(f" Generated main wallet: {main_wallet['address']}")
|
|
135
135
|
|
|
136
|
-
# Generate strategy wallet (will append to existing
|
|
136
|
+
# Generate strategy wallet (will append to existing config.json)
|
|
137
137
|
wallet = make_random_wallet()
|
|
138
138
|
wallet["label"] = dir_name
|
|
139
139
|
write_wallet_to_json(
|
|
@@ -27,7 +27,7 @@ def main():
|
|
|
27
27
|
"--out-dir",
|
|
28
28
|
type=Path,
|
|
29
29
|
default=Path("."),
|
|
30
|
-
help="Output directory for
|
|
30
|
+
help="Output directory for config.json (and keystore files)",
|
|
31
31
|
)
|
|
32
32
|
parser.add_argument(
|
|
33
33
|
"--keystore-password",
|
|
@@ -55,7 +55,7 @@ def main():
|
|
|
55
55
|
args.out_dir.mkdir(parents=True, exist_ok=True)
|
|
56
56
|
|
|
57
57
|
# Load existing wallets
|
|
58
|
-
existing = load_wallets(args.out_dir, "
|
|
58
|
+
existing = load_wallets(args.out_dir, "config.json")
|
|
59
59
|
has_main = any(w.get("label") in ("main", "default") for w in existing)
|
|
60
60
|
|
|
61
61
|
rows: list[dict[str, str]] = []
|
|
@@ -72,7 +72,7 @@ def main():
|
|
|
72
72
|
w["label"] = args.label
|
|
73
73
|
rows.append(w)
|
|
74
74
|
print(f"[{index}] {w['address']} (label: {args.label})")
|
|
75
|
-
write_wallet_to_json(w, out_dir=args.out_dir, filename="
|
|
75
|
+
write_wallet_to_json(w, out_dir=args.out_dir, filename="config.json")
|
|
76
76
|
if args.keystore_password:
|
|
77
77
|
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
78
78
|
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
@@ -86,7 +86,7 @@ def main():
|
|
|
86
86
|
rows.append(main_w)
|
|
87
87
|
print(f"[{index}] {main_w['address']} (main)")
|
|
88
88
|
write_wallet_to_json(
|
|
89
|
-
main_w, out_dir=args.out_dir, filename="
|
|
89
|
+
main_w, out_dir=args.out_dir, filename="config.json"
|
|
90
90
|
)
|
|
91
91
|
if args.keystore_password:
|
|
92
92
|
ks = to_keystore_json(
|
|
@@ -133,7 +133,7 @@ def main():
|
|
|
133
133
|
rows.append(w)
|
|
134
134
|
print(f"[{index}] {w['address']} (label: temporary_{next_temp_num})")
|
|
135
135
|
|
|
136
|
-
write_wallet_to_json(w, out_dir=args.out_dir, filename="
|
|
136
|
+
write_wallet_to_json(w, out_dir=args.out_dir, filename="config.json")
|
|
137
137
|
if args.keystore_password:
|
|
138
138
|
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
139
139
|
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
@@ -27,7 +27,7 @@ def _find_wallet(wallets: list[dict[str, Any]], label: str) -> dict[str, Any]:
|
|
|
27
27
|
for w in wallets:
|
|
28
28
|
if w.get("label") == label:
|
|
29
29
|
return w
|
|
30
|
-
raise ValueError(f"Wallet label not found in
|
|
30
|
+
raise ValueError(f"Wallet label not found in config.json: {label}")
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _get_strategy_class(strategy: str):
|
|
@@ -58,7 +58,7 @@ def _get_strategy_class(strategy: str):
|
|
|
58
58
|
async def _run(args: argparse.Namespace) -> int:
|
|
59
59
|
repo_root = Path(__file__).resolve().parents[2]
|
|
60
60
|
wallets_path = (
|
|
61
|
-
Path(args.wallets).resolve() if args.wallets else repo_root / "
|
|
61
|
+
Path(args.wallets).resolve() if args.wallets else repo_root / "config.json"
|
|
62
62
|
)
|
|
63
63
|
config_path = (
|
|
64
64
|
Path(args.config).resolve() if args.config else repo_root / "config.json"
|
|
@@ -123,7 +123,7 @@ def main() -> int:
|
|
|
123
123
|
],
|
|
124
124
|
)
|
|
125
125
|
p.add_argument(
|
|
126
|
-
"--wallets", default=None, help="Path to
|
|
126
|
+
"--wallets", default=None, help="Path to config.json (default: repo root)"
|
|
127
127
|
)
|
|
128
128
|
p.add_argument(
|
|
129
129
|
"--config", default=None, help="Path to config.json (default: repo root)"
|
|
@@ -38,7 +38,7 @@ class BasisSnapshotMixin:
|
|
|
38
38
|
entry_cost_usd: float,
|
|
39
39
|
exit_cost_usd: float,
|
|
40
40
|
) -> dict[str, Any]:
|
|
41
|
-
"""Build the `safe[horizon]` entry matching
|
|
41
|
+
"""Build the `safe[horizon]` entry matching the expected output shape."""
|
|
42
42
|
L = max(1, int(leverage))
|
|
43
43
|
|
|
44
44
|
depth_checks = depth_checks or {}
|