wayfinder-paths 0.1.7__py3-none-any.whl → 0.1.9__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.
- wayfinder_paths/CONFIG_GUIDE.md +5 -14
- wayfinder_paths/adapters/brap_adapter/README.md +1 -1
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -53
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +5 -7
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -7
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +0 -54
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/ledger_adapter/README.md +1 -1
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +3 -0
- wayfinder_paths/adapters/pool_adapter/README.md +3 -104
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -194
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -100
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -134
- wayfinder_paths/adapters/token_adapter/README.md +1 -1
- wayfinder_paths/core/clients/AuthClient.py +0 -3
- wayfinder_paths/core/clients/BRAPClient.py +1 -0
- wayfinder_paths/core/clients/ClientManager.py +1 -22
- wayfinder_paths/core/clients/PoolClient.py +0 -16
- wayfinder_paths/core/clients/WalletClient.py +0 -8
- wayfinder_paths/core/clients/WayfinderClient.py +9 -14
- wayfinder_paths/core/clients/__init__.py +0 -8
- wayfinder_paths/core/clients/protocols.py +0 -64
- wayfinder_paths/core/config.py +5 -45
- wayfinder_paths/core/engine/StrategyJob.py +0 -3
- wayfinder_paths/core/services/base.py +0 -49
- wayfinder_paths/core/services/local_evm_txn.py +3 -82
- wayfinder_paths/core/services/local_token_txn.py +61 -70
- wayfinder_paths/core/services/web3_service.py +0 -2
- wayfinder_paths/core/settings.py +8 -8
- wayfinder_paths/core/strategies/Strategy.py +1 -5
- wayfinder_paths/core/utils/evm_helpers.py +7 -12
- wayfinder_paths/core/wallets/README.md +3 -6
- wayfinder_paths/run_strategy.py +29 -32
- wayfinder_paths/scripts/make_wallets.py +1 -25
- wayfinder_paths/scripts/run_strategy.py +0 -2
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -3
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +86 -137
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +96 -58
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +106 -28
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +53 -14
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +40 -17
- wayfinder_paths/templates/strategy/test_strategy.py +0 -4
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/METADATA +33 -15
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/RECORD +49 -51
- wayfinder_paths/core/clients/SimulationClient.py +0 -192
- wayfinder_paths/core/clients/TransactionClient.py +0 -63
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/WHEEL +0 -0
|
@@ -13,14 +13,10 @@ from wayfinder_paths.core.clients.protocols import (
|
|
|
13
13
|
HyperlendClientProtocol,
|
|
14
14
|
LedgerClientProtocol,
|
|
15
15
|
PoolClientProtocol,
|
|
16
|
-
SimulationClientProtocol,
|
|
17
16
|
TokenClientProtocol,
|
|
18
|
-
TransactionClientProtocol,
|
|
19
17
|
WalletClientProtocol,
|
|
20
18
|
)
|
|
21
|
-
from wayfinder_paths.core.clients.SimulationClient import SimulationClient
|
|
22
19
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
23
|
-
from wayfinder_paths.core.clients.TransactionClient import TransactionClient
|
|
24
20
|
from wayfinder_paths.core.clients.WalletClient import WalletClient
|
|
25
21
|
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
26
22
|
|
|
@@ -30,19 +26,15 @@ __all__ = [
|
|
|
30
26
|
"AuthClient",
|
|
31
27
|
"TokenClient",
|
|
32
28
|
"WalletClient",
|
|
33
|
-
"TransactionClient",
|
|
34
29
|
"LedgerClient",
|
|
35
30
|
"PoolClient",
|
|
36
31
|
"BRAPClient",
|
|
37
|
-
"SimulationClient",
|
|
38
32
|
"HyperlendClient",
|
|
39
33
|
# Protocols for SDK usage
|
|
40
34
|
"TokenClientProtocol",
|
|
41
35
|
"HyperlendClientProtocol",
|
|
42
36
|
"LedgerClientProtocol",
|
|
43
37
|
"WalletClientProtocol",
|
|
44
|
-
"TransactionClientProtocol",
|
|
45
38
|
"PoolClientProtocol",
|
|
46
39
|
"BRAPClientProtocol",
|
|
47
|
-
"SimulationClientProtocol",
|
|
48
40
|
]
|
|
@@ -28,12 +28,10 @@ if TYPE_CHECKING:
|
|
|
28
28
|
LlamaReport,
|
|
29
29
|
PoolList,
|
|
30
30
|
)
|
|
31
|
-
from wayfinder_paths.core.clients.SimulationClient import SimulationResult
|
|
32
31
|
from wayfinder_paths.core.clients.TokenClient import (
|
|
33
32
|
GasToken,
|
|
34
33
|
TokenDetails,
|
|
35
34
|
)
|
|
36
|
-
from wayfinder_paths.core.clients.TransactionClient import TransactionPayload
|
|
37
35
|
from wayfinder_paths.core.clients.WalletClient import (
|
|
38
36
|
PoolBalance,
|
|
39
37
|
TokenBalance,
|
|
@@ -186,21 +184,6 @@ class WalletClientProtocol(Protocol):
|
|
|
186
184
|
...
|
|
187
185
|
|
|
188
186
|
|
|
189
|
-
class TransactionClientProtocol(Protocol):
|
|
190
|
-
"""Protocol for transaction operations"""
|
|
191
|
-
|
|
192
|
-
async def build_send(
|
|
193
|
-
self,
|
|
194
|
-
from_address: str,
|
|
195
|
-
to_address: str,
|
|
196
|
-
token_address: str,
|
|
197
|
-
amount: float,
|
|
198
|
-
chain_id: int,
|
|
199
|
-
) -> TransactionPayload:
|
|
200
|
-
"""Build a send transaction payload for EVM tokens/native transfers"""
|
|
201
|
-
...
|
|
202
|
-
|
|
203
|
-
|
|
204
187
|
class PoolClientProtocol(Protocol):
|
|
205
188
|
"""Protocol for pool-related read operations"""
|
|
206
189
|
|
|
@@ -213,10 +196,6 @@ class PoolClientProtocol(Protocol):
|
|
|
213
196
|
"""Fetch pools by comma-separated pool ids"""
|
|
214
197
|
...
|
|
215
198
|
|
|
216
|
-
async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
|
|
217
|
-
"""Fetch all pools"""
|
|
218
|
-
...
|
|
219
|
-
|
|
220
199
|
async def get_llama_matches(self) -> dict[str, LlamaMatch]:
|
|
221
200
|
"""Fetch Llama matches for pools"""
|
|
222
201
|
...
|
|
@@ -246,49 +225,6 @@ class BRAPClientProtocol(Protocol):
|
|
|
246
225
|
...
|
|
247
226
|
|
|
248
227
|
|
|
249
|
-
class SimulationClientProtocol(Protocol):
|
|
250
|
-
"""Protocol for blockchain transaction simulations"""
|
|
251
|
-
|
|
252
|
-
async def simulate_send(
|
|
253
|
-
self,
|
|
254
|
-
from_address: str,
|
|
255
|
-
to_address: str,
|
|
256
|
-
token_address: str,
|
|
257
|
-
amount: str,
|
|
258
|
-
chain_id: int,
|
|
259
|
-
initial_balances: dict[str, str],
|
|
260
|
-
) -> SimulationResult:
|
|
261
|
-
"""Simulate sending native ETH or ERC20 tokens"""
|
|
262
|
-
...
|
|
263
|
-
|
|
264
|
-
async def simulate_approve(
|
|
265
|
-
self,
|
|
266
|
-
from_address: str,
|
|
267
|
-
to_address: str,
|
|
268
|
-
token_address: str,
|
|
269
|
-
amount: str,
|
|
270
|
-
chain_id: int,
|
|
271
|
-
initial_balances: dict[str, str],
|
|
272
|
-
clear_approval_first: bool = False,
|
|
273
|
-
) -> SimulationResult:
|
|
274
|
-
"""Simulate ERC20 token approval"""
|
|
275
|
-
...
|
|
276
|
-
|
|
277
|
-
async def simulate_swap(
|
|
278
|
-
self,
|
|
279
|
-
from_token_address: str,
|
|
280
|
-
to_token_address: str,
|
|
281
|
-
from_chain_id: int,
|
|
282
|
-
to_chain_id: int,
|
|
283
|
-
amount: str,
|
|
284
|
-
from_address: str,
|
|
285
|
-
slippage: float,
|
|
286
|
-
initial_balances: dict[str, str],
|
|
287
|
-
) -> SimulationResult:
|
|
288
|
-
"""Simulate token swap operation"""
|
|
289
|
-
...
|
|
290
|
-
|
|
291
|
-
|
|
292
228
|
class HyperliquidExecutorProtocol(Protocol):
|
|
293
229
|
"""Protocol for Hyperliquid order execution operations."""
|
|
294
230
|
|
wayfinder_paths/core/config.py
CHANGED
|
@@ -4,7 +4,6 @@ Separates user-provided configuration from system configuration
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
-
import os
|
|
8
7
|
from dataclasses import dataclass, field
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Any
|
|
@@ -72,12 +71,8 @@ class SystemConfig:
|
|
|
72
71
|
These are values managed by the Wayfinder system
|
|
73
72
|
"""
|
|
74
73
|
|
|
75
|
-
# API endpoints (populated from
|
|
76
|
-
api_base_url: str = field(
|
|
77
|
-
default_factory=lambda: os.getenv(
|
|
78
|
-
"WAYFINDER_API_URL", "https://api.wayfinder.ai"
|
|
79
|
-
)
|
|
80
|
-
)
|
|
74
|
+
# API endpoints (populated from config.json or defaults)
|
|
75
|
+
api_base_url: str = field(default="https://api.wayfinder.ai")
|
|
81
76
|
|
|
82
77
|
# Job configuration
|
|
83
78
|
job_id: str | None = None
|
|
@@ -102,10 +97,7 @@ class SystemConfig:
|
|
|
102
97
|
def from_dict(cls, data: dict[str, Any]) -> "SystemConfig":
|
|
103
98
|
"""Create SystemConfig from dictionary"""
|
|
104
99
|
return cls(
|
|
105
|
-
api_base_url=data.get(
|
|
106
|
-
"api_base_url",
|
|
107
|
-
os.getenv("WAYFINDER_API_URL", "https://api.wayfinder.ai"),
|
|
108
|
-
),
|
|
100
|
+
api_base_url=data.get("api_base_url", "https://api.wayfinder.ai"),
|
|
109
101
|
job_id=data.get("job_id"),
|
|
110
102
|
job_type=data.get("job_type", "strategy"),
|
|
111
103
|
update_interval=data.get("update_interval", 60),
|
|
@@ -113,10 +105,8 @@ class SystemConfig:
|
|
|
113
105
|
retry_delay=data.get("retry_delay", 5),
|
|
114
106
|
log_path=data.get("log_path"),
|
|
115
107
|
data_path=data.get("data_path"),
|
|
116
|
-
wallets_path=data.get(
|
|
117
|
-
|
|
118
|
-
),
|
|
119
|
-
wallet_id=data.get("wallet_id") or os.getenv("WALLET_ID"),
|
|
108
|
+
wallets_path=data.get("wallets_path", "wallets.json"),
|
|
109
|
+
wallet_id=data.get("wallet_id"),
|
|
120
110
|
)
|
|
121
111
|
|
|
122
112
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -386,36 +376,6 @@ class StrategyJobConfig:
|
|
|
386
376
|
return config
|
|
387
377
|
|
|
388
378
|
|
|
389
|
-
def load_config_from_env() -> StrategyJobConfig:
|
|
390
|
-
"""
|
|
391
|
-
Load configuration from environment variables
|
|
392
|
-
This is the simplest way for users to provide configuration
|
|
393
|
-
"""
|
|
394
|
-
user_config = UserConfig(
|
|
395
|
-
username=os.getenv("WAYFINDER_USERNAME"),
|
|
396
|
-
password=os.getenv("WAYFINDER_PASSWORD"),
|
|
397
|
-
refresh_token=os.getenv("WAYFINDER_REFRESH_TOKEN"),
|
|
398
|
-
main_wallet_address=os.getenv("MAIN_WALLET_ADDRESS"),
|
|
399
|
-
strategy_wallet_address=os.getenv("STRATEGY_WALLET_ADDRESS"),
|
|
400
|
-
default_slippage=float(os.getenv("DEFAULT_SLIPPAGE", "0.005")),
|
|
401
|
-
gas_multiplier=float(os.getenv("GAS_MULTIPLIER", "1.2")),
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
system_config = SystemConfig(
|
|
405
|
-
api_base_url=os.getenv("WAYFINDER_API_URL", "https://api.wayfinder.ai"),
|
|
406
|
-
job_id=os.getenv("JOB_ID"),
|
|
407
|
-
update_interval=int(os.getenv("UPDATE_INTERVAL", "60")),
|
|
408
|
-
max_retries=int(os.getenv("MAX_RETRIES", "3")),
|
|
409
|
-
retry_delay=int(os.getenv("RETRY_DELAY", "5")),
|
|
410
|
-
wallets_path=os.getenv("WALLETS_PATH", "wallets.json"),
|
|
411
|
-
wallet_id=os.getenv("WALLET_ID"),
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
# No auto-population - wallets must be explicitly set in environment or matched by label
|
|
415
|
-
|
|
416
|
-
return StrategyJobConfig(user=user_config, system=system_config)
|
|
417
|
-
|
|
418
|
-
|
|
419
379
|
# --- Internal helpers -------------------------------------------------------
|
|
420
380
|
|
|
421
381
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import os
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
from loguru import logger
|
|
@@ -54,8 +53,6 @@ class StrategyJob:
|
|
|
54
53
|
creds = self.clients.auth._load_config_credentials()
|
|
55
54
|
if creds.get("api_key"):
|
|
56
55
|
return True
|
|
57
|
-
if os.getenv("WAYFINDER_API_KEY"):
|
|
58
|
-
return True
|
|
59
56
|
except Exception:
|
|
60
57
|
pass
|
|
61
58
|
|
|
@@ -50,55 +50,6 @@ class EvmTxn(ABC):
|
|
|
50
50
|
transaction confirmations.
|
|
51
51
|
"""
|
|
52
52
|
|
|
53
|
-
@abstractmethod
|
|
54
|
-
async def get_balance(
|
|
55
|
-
self,
|
|
56
|
-
address: str,
|
|
57
|
-
token_address: str | None,
|
|
58
|
-
chain_id: int,
|
|
59
|
-
) -> tuple[bool, Any]:
|
|
60
|
-
"""
|
|
61
|
-
Get balance for an address.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
address: Address to query balance for
|
|
65
|
-
token_address: ERC20 token address, or None for native token
|
|
66
|
-
chain_id: Chain ID
|
|
67
|
-
|
|
68
|
-
Returns:
|
|
69
|
-
Tuple of (success, balance_integer_or_error_message)
|
|
70
|
-
"""
|
|
71
|
-
pass
|
|
72
|
-
|
|
73
|
-
@abstractmethod
|
|
74
|
-
async def approve_token(
|
|
75
|
-
self,
|
|
76
|
-
token_address: str,
|
|
77
|
-
spender: str,
|
|
78
|
-
amount: int,
|
|
79
|
-
from_address: str,
|
|
80
|
-
chain_id: int,
|
|
81
|
-
wait_for_receipt: bool = True,
|
|
82
|
-
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
83
|
-
) -> tuple[bool, Any]:
|
|
84
|
-
"""
|
|
85
|
-
Approve a spender to spend tokens on behalf of from_address.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
token_address: ERC20 token contract address
|
|
89
|
-
spender: Address being approved to spend tokens
|
|
90
|
-
amount: Amount to approve (in token units, not human-readable)
|
|
91
|
-
from_address: Address approving the tokens
|
|
92
|
-
chain_id: Chain ID
|
|
93
|
-
wait_for_receipt: Whether to wait for the transaction receipt
|
|
94
|
-
timeout: Receipt timeout in seconds
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
Tuple of (success, transaction_result_dict_or_error_message)
|
|
98
|
-
Transaction result should include 'tx_hash' and optionally 'receipt'
|
|
99
|
-
"""
|
|
100
|
-
pass
|
|
101
|
-
|
|
102
53
|
@abstractmethod
|
|
103
54
|
async def broadcast_transaction(
|
|
104
55
|
self,
|
|
@@ -4,18 +4,13 @@ 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
8
|
|
|
9
9
|
from wayfinder_paths.core.constants import (
|
|
10
10
|
DEFAULT_GAS_ESTIMATE_FALLBACK,
|
|
11
11
|
ONE_GWEI,
|
|
12
|
-
ZERO_ADDRESS,
|
|
13
12
|
)
|
|
14
13
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
15
|
-
from wayfinder_paths.core.constants.erc20_abi import (
|
|
16
|
-
ERC20_APPROVAL_ABI,
|
|
17
|
-
ERC20_MINIMAL_ABI,
|
|
18
|
-
)
|
|
19
14
|
from wayfinder_paths.core.services.base import EvmTxn
|
|
20
15
|
from wayfinder_paths.core.utils.evm_helpers import (
|
|
21
16
|
resolve_private_key_for_from_address,
|
|
@@ -79,10 +74,10 @@ class NonceManager:
|
|
|
79
74
|
|
|
80
75
|
class LocalEvmTxn(EvmTxn):
|
|
81
76
|
"""
|
|
82
|
-
Local wallet provider using private keys stored in config or
|
|
77
|
+
Local wallet provider using private keys stored in config.json or wallets.json.
|
|
83
78
|
|
|
84
79
|
This provider implements the current default behavior:
|
|
85
|
-
- Resolves private keys from config or
|
|
80
|
+
- Resolves private keys from config.json or wallets.json
|
|
86
81
|
- Signs transactions using eth_account
|
|
87
82
|
- Broadcasts transactions via RPC
|
|
88
83
|
"""
|
|
@@ -107,80 +102,6 @@ class LocalEvmTxn(EvmTxn):
|
|
|
107
102
|
rpc_url = self._resolve_rpc_url(chain_id)
|
|
108
103
|
return AsyncWeb3(AsyncHTTPProvider(rpc_url))
|
|
109
104
|
|
|
110
|
-
async def get_balance(
|
|
111
|
-
self,
|
|
112
|
-
address: str,
|
|
113
|
-
token_address: str | None,
|
|
114
|
-
chain_id: int,
|
|
115
|
-
) -> tuple[bool, Any]:
|
|
116
|
-
"""
|
|
117
|
-
Get balance for an address (native or ERC20 token).
|
|
118
|
-
"""
|
|
119
|
-
w3 = self.get_web3(chain_id)
|
|
120
|
-
try:
|
|
121
|
-
checksum_addr = to_checksum_address(address)
|
|
122
|
-
|
|
123
|
-
if not token_address or token_address.lower() == ZERO_ADDRESS:
|
|
124
|
-
balance = await w3.eth.get_balance(checksum_addr)
|
|
125
|
-
return (True, int(balance))
|
|
126
|
-
|
|
127
|
-
token_checksum = to_checksum_address(token_address)
|
|
128
|
-
contract = w3.eth.contract(address=token_checksum, abi=ERC20_MINIMAL_ABI)
|
|
129
|
-
balance = await contract.functions.balanceOf(checksum_addr).call()
|
|
130
|
-
return (True, int(balance))
|
|
131
|
-
|
|
132
|
-
except Exception as exc: # noqa: BLE001
|
|
133
|
-
self.logger.error(f"Failed to get balance: {exc}")
|
|
134
|
-
return (False, f"Balance query failed: {exc}")
|
|
135
|
-
finally:
|
|
136
|
-
await self._close_web3(w3)
|
|
137
|
-
|
|
138
|
-
async def approve_token(
|
|
139
|
-
self,
|
|
140
|
-
token_address: str,
|
|
141
|
-
spender: str,
|
|
142
|
-
amount: int,
|
|
143
|
-
from_address: str,
|
|
144
|
-
chain_id: int,
|
|
145
|
-
wait_for_receipt: bool = True,
|
|
146
|
-
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
147
|
-
) -> tuple[bool, Any]:
|
|
148
|
-
"""
|
|
149
|
-
Approve a spender to spend tokens on behalf of from_address.
|
|
150
|
-
"""
|
|
151
|
-
try:
|
|
152
|
-
token_checksum = to_checksum_address(token_address)
|
|
153
|
-
spender_checksum = to_checksum_address(spender)
|
|
154
|
-
from_checksum = to_checksum_address(from_address)
|
|
155
|
-
amount_int = int(amount)
|
|
156
|
-
|
|
157
|
-
w3_sync = Web3()
|
|
158
|
-
contract = w3_sync.eth.contract(
|
|
159
|
-
address=token_checksum, abi=ERC20_APPROVAL_ABI
|
|
160
|
-
)
|
|
161
|
-
transaction_data = contract.encodeABI(
|
|
162
|
-
fn_name="approve",
|
|
163
|
-
args=[spender_checksum, amount_int],
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
approve_txn = {
|
|
167
|
-
"from": from_checksum,
|
|
168
|
-
"chainId": int(chain_id),
|
|
169
|
-
"to": token_checksum,
|
|
170
|
-
"data": transaction_data,
|
|
171
|
-
"value": 0,
|
|
172
|
-
"gas": ERC20_APPROVAL_GAS_LIMIT,
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return await self.broadcast_transaction(
|
|
176
|
-
approve_txn,
|
|
177
|
-
wait_for_receipt=wait_for_receipt,
|
|
178
|
-
timeout=timeout,
|
|
179
|
-
)
|
|
180
|
-
except Exception as exc: # noqa: BLE001
|
|
181
|
-
self.logger.error(f"ERC20 approval failed: {exc}")
|
|
182
|
-
return (False, f"ERC20 approval failed: {exc}")
|
|
183
|
-
|
|
184
105
|
async def broadcast_transaction(
|
|
185
106
|
self,
|
|
186
107
|
transaction: dict[str, Any],
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from decimal import ROUND_DOWN, Decimal
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
from eth_utils import to_checksum_address
|
|
6
7
|
from loguru import logger
|
|
7
|
-
from web3 import AsyncWeb3
|
|
8
|
+
from web3 import AsyncWeb3, Web3
|
|
8
9
|
|
|
9
10
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
10
|
-
from wayfinder_paths.core.clients.TransactionClient import TransactionClient
|
|
11
11
|
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
12
|
-
from wayfinder_paths.core.constants.erc20_abi import ERC20_APPROVAL_ABI
|
|
12
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI, ERC20_APPROVAL_ABI
|
|
13
13
|
from wayfinder_paths.core.services.base import EvmTxn, TokenTxn
|
|
14
14
|
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
15
15
|
|
|
@@ -22,13 +22,12 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
22
22
|
config: dict[str, Any] | None,
|
|
23
23
|
*,
|
|
24
24
|
wallet_provider: EvmTxn,
|
|
25
|
-
simulation: bool = False,
|
|
26
25
|
) -> None:
|
|
27
|
-
del config
|
|
26
|
+
del config
|
|
28
27
|
self.wallet_provider = wallet_provider
|
|
29
28
|
self.logger = logger.bind(service="DefaultEvmTransactionService")
|
|
30
29
|
self.token_client = TokenClient()
|
|
31
|
-
self.builder = _EvmTransactionBuilder()
|
|
30
|
+
self.builder = _EvmTransactionBuilder(wallet_provider)
|
|
32
31
|
|
|
33
32
|
async def build_send(
|
|
34
33
|
self,
|
|
@@ -51,14 +50,24 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
51
50
|
return False, f"Token {token_id} is missing a chain id"
|
|
52
51
|
|
|
53
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)
|
|
54
62
|
|
|
55
63
|
try:
|
|
56
64
|
tx = await self.builder.build_send_transaction(
|
|
57
65
|
from_address=from_address,
|
|
58
66
|
to_address=to_address,
|
|
59
67
|
token_address=token_address,
|
|
60
|
-
amount=
|
|
68
|
+
amount=amount_wei,
|
|
61
69
|
chain_id=int(chain_id),
|
|
70
|
+
is_native=is_native,
|
|
62
71
|
)
|
|
63
72
|
except Exception as exc: # noqa: BLE001
|
|
64
73
|
return False, f"Failed to build send transaction: {exc}"
|
|
@@ -127,12 +136,20 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
127
136
|
raise ValueError("Chain ID is required")
|
|
128
137
|
return int(chain_id)
|
|
129
138
|
|
|
139
|
+
def _to_base_units(self, amount: float, decimals: int) -> int:
|
|
140
|
+
"""Convert human-readable amount to base units (wei for native, token units for ERC20)."""
|
|
141
|
+
scale = Decimal(10) ** int(decimals)
|
|
142
|
+
quantized = (Decimal(str(amount)) * scale).to_integral_value(
|
|
143
|
+
rounding=ROUND_DOWN
|
|
144
|
+
)
|
|
145
|
+
return int(quantized)
|
|
146
|
+
|
|
130
147
|
|
|
131
148
|
class _EvmTransactionBuilder:
|
|
132
149
|
"""Helpers that only build transaction dictionaries for sends and approvals."""
|
|
133
150
|
|
|
134
|
-
def __init__(self) -> None:
|
|
135
|
-
self.
|
|
151
|
+
def __init__(self, wallet_provider: EvmTxn) -> None:
|
|
152
|
+
self.wallet_provider = wallet_provider
|
|
136
153
|
|
|
137
154
|
async def build_send_transaction(
|
|
138
155
|
self,
|
|
@@ -140,22 +157,37 @@ class _EvmTransactionBuilder:
|
|
|
140
157
|
from_address: str,
|
|
141
158
|
to_address: str,
|
|
142
159
|
token_address: str | None,
|
|
143
|
-
amount:
|
|
160
|
+
amount: int,
|
|
144
161
|
chain_id: int,
|
|
162
|
+
is_native: bool,
|
|
145
163
|
) -> dict[str, Any]:
|
|
146
164
|
"""Build the transaction dict for sending native or ERC20 tokens."""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
from_checksum = to_checksum_address(from_address)
|
|
166
|
+
to_checksum = to_checksum_address(to_address)
|
|
167
|
+
chain_id_int = int(chain_id)
|
|
168
|
+
|
|
169
|
+
if is_native:
|
|
170
|
+
return {
|
|
171
|
+
"chainId": chain_id_int,
|
|
172
|
+
"from": from_checksum,
|
|
173
|
+
"to": to_checksum,
|
|
174
|
+
"value": int(amount),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
token_checksum = to_checksum_address(token_address or ZERO_ADDRESS)
|
|
178
|
+
w3_sync = Web3()
|
|
179
|
+
contract = w3_sync.eth.contract(address=token_checksum, abi=ERC20_ABI)
|
|
180
|
+
data = contract.functions.transfer(
|
|
181
|
+
to_checksum, int(amount)
|
|
182
|
+
)._encode_transaction_data()
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"chainId": chain_id_int,
|
|
186
|
+
"from": from_checksum,
|
|
187
|
+
"to": token_checksum,
|
|
188
|
+
"data": data,
|
|
189
|
+
"value": 0,
|
|
190
|
+
}
|
|
159
191
|
|
|
160
192
|
def build_erc20_approval_transaction(
|
|
161
193
|
self,
|
|
@@ -173,10 +205,13 @@ class _EvmTransactionBuilder:
|
|
|
173
205
|
from_checksum = to_checksum_address(from_address)
|
|
174
206
|
amount_int = int(amount)
|
|
175
207
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
# Use synchronous Web3 for encoding (encodeABI doesn't exist in web3.py v7)
|
|
209
|
+
w3_sync = Web3()
|
|
210
|
+
contract = w3_sync.eth.contract(address=token_checksum, abi=ERC20_APPROVAL_ABI)
|
|
211
|
+
# In web3.py v7, use _encode_transaction_data to encode without network calls
|
|
212
|
+
data = contract.functions.approve(
|
|
213
|
+
spender_checksum, amount_int
|
|
214
|
+
)._encode_transaction_data()
|
|
180
215
|
|
|
181
216
|
return {
|
|
182
217
|
"chainId": int(chain_id),
|
|
@@ -185,47 +220,3 @@ class _EvmTransactionBuilder:
|
|
|
185
220
|
"data": data,
|
|
186
221
|
"value": 0,
|
|
187
222
|
}
|
|
188
|
-
|
|
189
|
-
def _payload_to_tx(
|
|
190
|
-
self, payload: dict[str, Any], from_address: str, is_native: bool
|
|
191
|
-
) -> dict[str, Any]:
|
|
192
|
-
data_root = payload.get("data", payload)
|
|
193
|
-
tx_src = data_root.get("transaction") or data_root
|
|
194
|
-
|
|
195
|
-
chain_id = tx_src.get("chainId") or data_root.get("chain_id")
|
|
196
|
-
if chain_id is None:
|
|
197
|
-
raise ValueError("Transaction payload missing chainId")
|
|
198
|
-
|
|
199
|
-
tx: dict[str, Any] = {"chainId": int(chain_id)}
|
|
200
|
-
tx["from"] = to_checksum_address(from_address)
|
|
201
|
-
|
|
202
|
-
if tx_src.get("to"):
|
|
203
|
-
tx["to"] = to_checksum_address(tx_src["to"])
|
|
204
|
-
if tx_src.get("data"):
|
|
205
|
-
data = tx_src["data"]
|
|
206
|
-
tx["data"] = data if str(data).startswith("0x") else f"0x{data}"
|
|
207
|
-
|
|
208
|
-
val = tx_src.get("value", 0)
|
|
209
|
-
tx["value"] = self._normalize_value(val) if is_native else 0
|
|
210
|
-
|
|
211
|
-
if tx_src.get("gas"):
|
|
212
|
-
tx["gas"] = int(tx_src["gas"])
|
|
213
|
-
if tx_src.get("maxFeePerGas"):
|
|
214
|
-
tx["maxFeePerGas"] = int(tx_src["maxFeePerGas"])
|
|
215
|
-
if tx_src.get("maxPriorityFeePerGas"):
|
|
216
|
-
tx["maxPriorityFeePerGas"] = int(tx_src["maxPriorityFeePerGas"])
|
|
217
|
-
if tx_src.get("gasPrice"):
|
|
218
|
-
tx["gasPrice"] = int(tx_src["gasPrice"])
|
|
219
|
-
if tx_src.get("nonce") is not None:
|
|
220
|
-
tx["nonce"] = int(tx_src["nonce"])
|
|
221
|
-
|
|
222
|
-
return tx
|
|
223
|
-
|
|
224
|
-
def _normalize_value(self, value: Any) -> int:
|
|
225
|
-
if isinstance(value, str):
|
|
226
|
-
if value.startswith("0x"):
|
|
227
|
-
return int(value, 16)
|
|
228
|
-
return int(float(value))
|
|
229
|
-
if isinstance(value, (int, float)):
|
|
230
|
-
return int(value)
|
|
231
|
-
return 0
|
|
@@ -16,7 +16,6 @@ class DefaultWeb3Service(Web3Service):
|
|
|
16
16
|
*,
|
|
17
17
|
wallet_provider: EvmTxn | None = None,
|
|
18
18
|
evm_transactions: TokenTxn | None = None,
|
|
19
|
-
simulation: bool = False,
|
|
20
19
|
) -> None:
|
|
21
20
|
"""
|
|
22
21
|
Initialize the service with optional dependency injection.
|
|
@@ -33,7 +32,6 @@ class DefaultWeb3Service(Web3Service):
|
|
|
33
32
|
self._evm_transactions = LocalTokenTxnService(
|
|
34
33
|
config=cfg,
|
|
35
34
|
wallet_provider=self._wallet_provider,
|
|
36
|
-
simulation=simulation,
|
|
37
35
|
)
|
|
38
36
|
|
|
39
37
|
@property
|
wayfinder_paths/core/settings.py
CHANGED
|
@@ -12,14 +12,14 @@ class CoreSettings(BaseSettings):
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
model_config = SettingsConfigDict(
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# Note: .env file is only used for publishing (REPOSITORY_NAME, PUBLISH_TOKEN)
|
|
16
|
+
# All other configuration should come from config.json
|
|
17
17
|
case_sensitive=False,
|
|
18
18
|
extra="ignore", # Ignore extra environment variables (e.g., from Django)
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
# Core API Configuration
|
|
22
|
-
API_ENV: str = Field("development"
|
|
22
|
+
API_ENV: str = Field(default="development")
|
|
23
23
|
|
|
24
24
|
def _compute_default_api_url() -> str:
|
|
25
25
|
"""
|
|
@@ -44,17 +44,17 @@ class CoreSettings(BaseSettings):
|
|
|
44
44
|
base = "https://wayfinder.ai/api/v1"
|
|
45
45
|
return base
|
|
46
46
|
|
|
47
|
-
WAYFINDER_API_URL: str = Field(_compute_default_api_url
|
|
47
|
+
WAYFINDER_API_URL: str = Field(default_factory=_compute_default_api_url)
|
|
48
48
|
|
|
49
49
|
# Network Configuration
|
|
50
|
-
NETWORK: str = Field("testnet"
|
|
50
|
+
NETWORK: str = Field(default="testnet") # mainnet, testnet, devnet
|
|
51
51
|
|
|
52
52
|
# Logging
|
|
53
|
-
LOG_LEVEL: str = Field("INFO"
|
|
54
|
-
LOG_FILE: str = Field("logs/strategy.log"
|
|
53
|
+
LOG_LEVEL: str = Field(default="INFO")
|
|
54
|
+
LOG_FILE: str = Field(default="logs/strategy.log")
|
|
55
55
|
|
|
56
56
|
# Safety
|
|
57
|
-
DRY_RUN: bool = Field(False
|
|
57
|
+
DRY_RUN: bool = Field(default=False)
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
# Core settings instance
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import traceback
|
|
5
4
|
from abc import ABC, abstractmethod
|
|
6
5
|
from collections.abc import Awaitable, Callable
|
|
@@ -57,16 +56,13 @@ class Strategy(ABC):
|
|
|
57
56
|
*,
|
|
58
57
|
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
58
|
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
60
|
-
simulation: bool = False,
|
|
61
59
|
web3_service: Web3Service | None = None,
|
|
62
60
|
api_key: str | None = None,
|
|
63
61
|
):
|
|
64
62
|
self.adapters = {}
|
|
65
63
|
self.ledger_adapter = None
|
|
66
64
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
67
|
-
|
|
68
|
-
os.environ["WAYFINDER_API_KEY"] = api_key
|
|
69
|
-
|
|
65
|
+
# Note: api_key is passed to ClientManager, not set in environment
|
|
70
66
|
self.config = config
|
|
71
67
|
|
|
72
68
|
async def setup(self) -> None:
|