wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/constants/erc20_abi.py +0 -11
- wayfinder_paths/core/engine/StrategyJob.py +10 -56
- wayfinder_paths/core/services/base.py +1 -0
- wayfinder_paths/core/services/local_evm_txn.py +25 -9
- wayfinder_paths/core/services/local_token_txn.py +2 -6
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +16 -4
- wayfinder_paths/core/utils/evm_helpers.py +2 -9
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +13 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -4,7 +4,7 @@ Protocol definitions for API clients.
|
|
|
4
4
|
These protocols define the interface that all client implementations must satisfy.
|
|
5
5
|
When used as an SDK, users can provide custom implementations that match these protocols.
|
|
6
6
|
|
|
7
|
-
Note:
|
|
7
|
+
Note: Authentication is handled via X-API-KEY header in WayfinderClient base class.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -12,19 +12,19 @@ from __future__ import annotations
|
|
|
12
12
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
|
-
from wayfinder_paths.core.clients.BRAPClient import
|
|
15
|
+
from wayfinder_paths.core.clients.BRAPClient import BRAPQuoteResponse
|
|
16
16
|
from wayfinder_paths.core.clients.HyperlendClient import (
|
|
17
17
|
AssetsView,
|
|
18
18
|
LendRateHistory,
|
|
19
19
|
MarketEntry,
|
|
20
|
-
|
|
20
|
+
StableMarketsHeadroomResponse,
|
|
21
21
|
)
|
|
22
22
|
from wayfinder_paths.core.clients.LedgerClient import (
|
|
23
23
|
StrategyTransactionList,
|
|
24
24
|
TransactionRecord,
|
|
25
25
|
)
|
|
26
26
|
from wayfinder_paths.core.clients.PoolClient import (
|
|
27
|
-
|
|
27
|
+
LlamaMatchesResponse,
|
|
28
28
|
PoolList,
|
|
29
29
|
)
|
|
30
30
|
from wayfinder_paths.core.clients.TokenClient import (
|
|
@@ -32,8 +32,7 @@ if TYPE_CHECKING:
|
|
|
32
32
|
TokenDetails,
|
|
33
33
|
)
|
|
34
34
|
from wayfinder_paths.core.clients.WalletClient import (
|
|
35
|
-
|
|
36
|
-
TokenBalance,
|
|
35
|
+
AddressBalance,
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
|
|
@@ -41,7 +40,10 @@ class TokenClientProtocol(Protocol):
|
|
|
41
40
|
"""Protocol for token-related operations"""
|
|
42
41
|
|
|
43
42
|
async def get_token_details(
|
|
44
|
-
self,
|
|
43
|
+
self,
|
|
44
|
+
query: str,
|
|
45
|
+
market_data: bool = True,
|
|
46
|
+
chain_id: int | None = None,
|
|
45
47
|
) -> TokenDetails:
|
|
46
48
|
"""Get token data including price from the token-details endpoint"""
|
|
47
49
|
...
|
|
@@ -57,19 +59,16 @@ class HyperlendClientProtocol(Protocol):
|
|
|
57
59
|
async def get_stable_markets(
|
|
58
60
|
self,
|
|
59
61
|
*,
|
|
60
|
-
chain_id: int,
|
|
61
62
|
required_underlying_tokens: float | None = None,
|
|
62
63
|
buffer_bps: int | None = None,
|
|
63
64
|
min_buffer_tokens: float | None = None,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"""Fetch stable markets from Hyperlend"""
|
|
65
|
+
) -> StableMarketsHeadroomResponse:
|
|
66
|
+
"""Fetch stable markets headroom from Hyperlend"""
|
|
67
67
|
...
|
|
68
68
|
|
|
69
69
|
async def get_assets_view(
|
|
70
70
|
self,
|
|
71
71
|
*,
|
|
72
|
-
chain_id: int,
|
|
73
72
|
user_address: str,
|
|
74
73
|
) -> AssetsView:
|
|
75
74
|
"""Fetch assets view for a user address from Hyperlend"""
|
|
@@ -78,8 +77,7 @@ class HyperlendClientProtocol(Protocol):
|
|
|
78
77
|
async def get_market_entry(
|
|
79
78
|
self,
|
|
80
79
|
*,
|
|
81
|
-
|
|
82
|
-
token_address: str,
|
|
80
|
+
token: str,
|
|
83
81
|
) -> MarketEntry:
|
|
84
82
|
"""Fetch market entry from Hyperlend"""
|
|
85
83
|
...
|
|
@@ -87,9 +85,9 @@ class HyperlendClientProtocol(Protocol):
|
|
|
87
85
|
async def get_lend_rate_history(
|
|
88
86
|
self,
|
|
89
87
|
*,
|
|
90
|
-
|
|
91
|
-
token_address: str,
|
|
88
|
+
token: str,
|
|
92
89
|
lookback_hours: int,
|
|
90
|
+
force_refresh: bool | None = None,
|
|
93
91
|
) -> LendRateHistory:
|
|
94
92
|
"""Fetch lend rate history from Hyperlend"""
|
|
95
93
|
...
|
|
@@ -161,25 +159,14 @@ class LedgerClientProtocol(Protocol):
|
|
|
161
159
|
class WalletClientProtocol(Protocol):
|
|
162
160
|
"""Protocol for wallet-related operations"""
|
|
163
161
|
|
|
164
|
-
async def
|
|
162
|
+
async def get_token_balance_for_address(
|
|
165
163
|
self,
|
|
166
164
|
*,
|
|
167
|
-
token_id: str,
|
|
168
165
|
wallet_address: str,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
async def get_pool_balance_for_wallet(
|
|
175
|
-
self,
|
|
176
|
-
*,
|
|
177
|
-
pool_address: str,
|
|
178
|
-
chain_id: int,
|
|
179
|
-
user_address: str,
|
|
180
|
-
human_readable: bool = True,
|
|
181
|
-
) -> PoolBalance:
|
|
182
|
-
"""Fetch a wallet's LP/share balance for a given pool address and chain"""
|
|
166
|
+
query: str,
|
|
167
|
+
chain_id: int | None = None,
|
|
168
|
+
) -> AddressBalance:
|
|
169
|
+
"""Fetch a balance for an address + chain + query (supports compound query formats)"""
|
|
183
170
|
...
|
|
184
171
|
|
|
185
172
|
|
|
@@ -189,13 +176,18 @@ class PoolClientProtocol(Protocol):
|
|
|
189
176
|
async def get_pools_by_ids(
|
|
190
177
|
self,
|
|
191
178
|
*,
|
|
192
|
-
pool_ids: str,
|
|
179
|
+
pool_ids: list[str] | str,
|
|
193
180
|
) -> PoolList:
|
|
194
|
-
"""Fetch pools by comma-separated
|
|
181
|
+
"""Fetch pools by pool IDs (list or comma-separated string)"""
|
|
195
182
|
...
|
|
196
183
|
|
|
197
|
-
async def get_pools(
|
|
198
|
-
|
|
184
|
+
async def get_pools(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
chain_id: int | None = None,
|
|
188
|
+
project: str | None = None,
|
|
189
|
+
) -> LlamaMatchesResponse:
|
|
190
|
+
"""Fetch pools (optionally filtered by chain_id and project)"""
|
|
199
191
|
...
|
|
200
192
|
|
|
201
193
|
|
|
@@ -205,16 +197,13 @@ class BRAPClientProtocol(Protocol):
|
|
|
205
197
|
async def get_quote(
|
|
206
198
|
self,
|
|
207
199
|
*,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
slippage: float | None = None,
|
|
216
|
-
wayfinder_fee: float | None = None,
|
|
217
|
-
) -> BRAPQuote:
|
|
200
|
+
from_token: str,
|
|
201
|
+
to_token: str,
|
|
202
|
+
from_chain: int,
|
|
203
|
+
to_chain: int,
|
|
204
|
+
from_wallet: str,
|
|
205
|
+
from_amount: str,
|
|
206
|
+
) -> BRAPQuoteResponse:
|
|
218
207
|
"""Get a quote for a bridge/swap operation"""
|
|
219
208
|
...
|
|
220
209
|
|
|
@@ -38,59 +38,74 @@ class MockHyperlendClient:
|
|
|
38
38
|
async def get_stable_markets(
|
|
39
39
|
self,
|
|
40
40
|
*,
|
|
41
|
-
chain_id: int,
|
|
42
41
|
required_underlying_tokens: float | None = None,
|
|
43
42
|
buffer_bps: int | None = None,
|
|
44
43
|
min_buffer_tokens: float | None = None,
|
|
45
|
-
is_stable_symbol: bool | None = None,
|
|
46
44
|
) -> dict[str, Any]:
|
|
47
45
|
return {
|
|
48
|
-
"markets":
|
|
49
|
-
{
|
|
50
|
-
"chain_id": chain_id,
|
|
51
|
-
"token_address": "0xMockToken",
|
|
46
|
+
"markets": {
|
|
47
|
+
"0xMockToken": {
|
|
52
48
|
"symbol": "USDC",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
49
|
+
"symbol_canonical": "usdc",
|
|
50
|
+
"display_symbol": "USDC",
|
|
51
|
+
"reserve": {},
|
|
52
|
+
"decimals": 6,
|
|
53
|
+
"headroom": 1000000000000,
|
|
54
|
+
"supply_cap": 5000000000000,
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
},
|
|
57
|
+
"notes": [],
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async def get_assets_view(
|
|
60
61
|
self,
|
|
61
62
|
*,
|
|
62
|
-
chain_id: int,
|
|
63
63
|
user_address: str,
|
|
64
64
|
) -> dict[str, Any]:
|
|
65
65
|
return {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
66
|
+
"block_number": 12345,
|
|
67
|
+
"user": user_address,
|
|
68
|
+
"native_balance_wei": 0,
|
|
69
|
+
"native_balance": 0.0,
|
|
68
70
|
"assets": [],
|
|
71
|
+
"account_data": {
|
|
72
|
+
"total_collateral_base": 0,
|
|
73
|
+
"total_debt_base": 0,
|
|
74
|
+
"available_borrows_base": 0,
|
|
75
|
+
"current_liquidation_threshold": 0,
|
|
76
|
+
"ltv": 0,
|
|
77
|
+
"health_factor_wad": 0,
|
|
78
|
+
"health_factor": 0.0,
|
|
79
|
+
},
|
|
80
|
+
"base_currency_info": {
|
|
81
|
+
"marketReferenceCurrencyUnit": 100000000,
|
|
82
|
+
"marketReferenceCurrencyPriceInUsd": 100000000,
|
|
83
|
+
"networkBaseTokenPriceInUsd": 0,
|
|
84
|
+
"networkBaseTokenPriceDecimals": 8,
|
|
85
|
+
},
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
async def get_market_entry(
|
|
72
89
|
self,
|
|
73
90
|
*,
|
|
74
|
-
|
|
75
|
-
token_address: str,
|
|
91
|
+
token: str,
|
|
76
92
|
) -> dict[str, Any]:
|
|
77
93
|
return {
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
94
|
+
"symbol": "USDC",
|
|
95
|
+
"symbol_canonical": "usdc",
|
|
96
|
+
"display_symbol": "USDC",
|
|
97
|
+
"reserve": {},
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
async def get_lend_rate_history(
|
|
84
101
|
self,
|
|
85
102
|
*,
|
|
86
|
-
|
|
87
|
-
token_address: str,
|
|
103
|
+
token: str,
|
|
88
104
|
lookback_hours: int,
|
|
105
|
+
force_refresh: bool | None = None,
|
|
89
106
|
) -> dict[str, Any]:
|
|
90
107
|
return {
|
|
91
|
-
"
|
|
92
|
-
"token_address": token_address,
|
|
93
|
-
"rates": [],
|
|
108
|
+
"history": [],
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
|
|
@@ -83,17 +83,6 @@ ERC20_ABI = [
|
|
|
83
83
|
},
|
|
84
84
|
]
|
|
85
85
|
|
|
86
|
-
# Minimal ABI for specific use cases (e.g., when you only need certain functions)
|
|
87
|
-
ERC20_MINIMAL_ABI = [
|
|
88
|
-
{
|
|
89
|
-
"constant": True,
|
|
90
|
-
"inputs": [{"name": "account", "type": "address"}],
|
|
91
|
-
"name": "balanceOf",
|
|
92
|
-
"outputs": [{"name": "", "type": "uint256"}],
|
|
93
|
-
"type": "function",
|
|
94
|
-
}
|
|
95
|
-
]
|
|
96
|
-
|
|
97
86
|
ERC20_APPROVAL_ABI = [
|
|
98
87
|
{
|
|
99
88
|
"constant": True,
|
|
@@ -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
|
|
@@ -16,7 +15,6 @@ class StrategyJob:
|
|
|
16
15
|
config: StrategyJobConfig,
|
|
17
16
|
clients: dict[str, Any] | None = None,
|
|
18
17
|
skip_auth: bool = False,
|
|
19
|
-
api_key: str | None = None,
|
|
20
18
|
):
|
|
21
19
|
"""
|
|
22
20
|
Initialize a StrategyJob.
|
|
@@ -26,16 +24,12 @@ class StrategyJob:
|
|
|
26
24
|
config: Strategy job configuration.
|
|
27
25
|
clients: Optional dict of pre-instantiated clients to inject directly.
|
|
28
26
|
skip_auth: If True, skips authentication (for SDK usage).
|
|
29
|
-
api_key: Optional API key for service account authentication.
|
|
30
|
-
If provided, will be passed to ClientManager and strategy.
|
|
31
27
|
"""
|
|
32
28
|
self.strategy = strategy
|
|
33
29
|
self.config = config
|
|
34
30
|
|
|
35
31
|
self.job_id = strategy.name or "unknown"
|
|
36
|
-
self.clients = ClientManager(
|
|
37
|
-
clients=clients, skip_auth=skip_auth, api_key=api_key
|
|
38
|
-
)
|
|
32
|
+
self.clients = ClientManager(clients=clients, skip_auth=skip_auth)
|
|
39
33
|
|
|
40
34
|
def _setup_strategy(self):
|
|
41
35
|
"""Setup the strategy instance"""
|
|
@@ -44,23 +38,6 @@ class StrategyJob:
|
|
|
44
38
|
|
|
45
39
|
self.strategy.log = self.log
|
|
46
40
|
|
|
47
|
-
def _is_using_api_key(self) -> bool:
|
|
48
|
-
"""Check if API key authentication is being used."""
|
|
49
|
-
if self.clients._api_key:
|
|
50
|
-
return True
|
|
51
|
-
|
|
52
|
-
if self.clients.auth:
|
|
53
|
-
try:
|
|
54
|
-
creds = self.clients.auth._load_config_credentials()
|
|
55
|
-
if creds.get("api_key"):
|
|
56
|
-
return True
|
|
57
|
-
if os.getenv("WAYFINDER_API_KEY"):
|
|
58
|
-
return True
|
|
59
|
-
except Exception:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
41
|
async def setup(self):
|
|
65
42
|
"""
|
|
66
43
|
Initialize the strategy job and strategy.
|
|
@@ -69,38 +46,13 @@ class StrategyJob:
|
|
|
69
46
|
"""
|
|
70
47
|
self._setup_strategy()
|
|
71
48
|
|
|
72
|
-
# Ensure
|
|
49
|
+
# Ensure API key is set for API calls
|
|
50
|
+
# All clients inherit from WayfinderClient and have _ensure_api_key()
|
|
73
51
|
if not self.clients._skip_auth:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
if self.clients.auth:
|
|
79
|
-
await self.clients.auth._ensure_bearer_token()
|
|
80
|
-
else:
|
|
81
|
-
# Try to ensure bearer token is set, authenticate if needed
|
|
82
|
-
try:
|
|
83
|
-
if self.clients.auth:
|
|
84
|
-
await self.clients.auth._ensure_bearer_token()
|
|
85
|
-
except (PermissionError, Exception) as e:
|
|
86
|
-
if not isinstance(e, PermissionError):
|
|
87
|
-
logger.warning(
|
|
88
|
-
f"Authentication failed: {e}, trying OAuth fallback"
|
|
89
|
-
)
|
|
90
|
-
username = self.config.user.username
|
|
91
|
-
password = self.config.user.password
|
|
92
|
-
refresh_token = self.config.user.refresh_token
|
|
93
|
-
if refresh_token or (username and password):
|
|
94
|
-
await self.clients.authenticate(
|
|
95
|
-
username=username,
|
|
96
|
-
password=password,
|
|
97
|
-
refresh_token=refresh_token,
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"Authentication required: provide api_key parameter for service account auth, "
|
|
102
|
-
"or username+password/refresh_token in config.json for personal access"
|
|
103
|
-
) from e
|
|
52
|
+
# Ensure API key on any client (they all share the same method)
|
|
53
|
+
token_client = self.clients.token
|
|
54
|
+
if token_client:
|
|
55
|
+
token_client._ensure_api_key()
|
|
104
56
|
|
|
105
57
|
existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
|
|
106
58
|
strategy_cfg = dict(self.config.strategy_config or {})
|
|
@@ -110,7 +62,7 @@ class StrategyJob:
|
|
|
110
62
|
await self.strategy.setup()
|
|
111
63
|
|
|
112
64
|
async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
|
|
113
|
-
"""Execute a strategy action (deposit, withdraw, update, status, partial_liquidate)"""
|
|
65
|
+
"""Execute a strategy action (deposit, withdraw, update, status, exit, partial_liquidate)"""
|
|
114
66
|
try:
|
|
115
67
|
if action == "deposit":
|
|
116
68
|
result = await self.strategy.deposit(**kwargs)
|
|
@@ -120,6 +72,8 @@ class StrategyJob:
|
|
|
120
72
|
result = await self.strategy.update()
|
|
121
73
|
elif action == "status":
|
|
122
74
|
result = await self.strategy.status()
|
|
75
|
+
elif action == "exit":
|
|
76
|
+
result = await self.strategy.exit(**kwargs)
|
|
123
77
|
elif action == "partial_liquidate":
|
|
124
78
|
usd_value = kwargs.get("usd_value")
|
|
125
79
|
if usd_value is None:
|
|
@@ -20,6 +20,9 @@ SUGGESTED_PRIORITY_FEE_MULTIPLIER = 1.5
|
|
|
20
20
|
MAX_BASE_FEE_GROWTH_MULTIPLIER = 2
|
|
21
21
|
GAS_LIMIT_BUFFER_MULTIPLIER = 1.5
|
|
22
22
|
|
|
23
|
+
# Base chain ID (Base mainnet)
|
|
24
|
+
BASE_CHAIN_ID = 8453
|
|
25
|
+
|
|
23
26
|
# Chains that don't support EIP-1559 (London) and need legacy gas pricing
|
|
24
27
|
PRE_LONDON_GAS_CHAIN_IDS: set[int] = {56, 42161}
|
|
25
28
|
POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
|
|
@@ -155,13 +158,13 @@ class LocalEvmTxn(EvmTxn):
|
|
|
155
158
|
gas_price = await self._get_gas_price(w3)
|
|
156
159
|
|
|
157
160
|
transaction["gasPrice"] = int(gas_price * SUGGESTED_GAS_PRICE_MULTIPLIER)
|
|
158
|
-
elif chain_id == 999:
|
|
159
|
-
|
|
161
|
+
# elif chain_id == 999:
|
|
162
|
+
# big_block_gas_price = await w3.hype.big_block_gas_price()
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
# transaction["maxFeePerGas"] = int(
|
|
165
|
+
# big_block_gas_price * SUGGESTED_PRIORITY_FEE_MULTIPLIER
|
|
166
|
+
# )
|
|
167
|
+
# transaction["maxPriorityFeePerGas"] = 0
|
|
165
168
|
else:
|
|
166
169
|
base_fee = await self._get_base_fee(w3)
|
|
167
170
|
priority_fee = await self._get_priority_fee(w3)
|
|
@@ -182,12 +185,20 @@ class LocalEvmTxn(EvmTxn):
|
|
|
182
185
|
*,
|
|
183
186
|
wait_for_receipt: bool = True,
|
|
184
187
|
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
185
|
-
confirmations: int =
|
|
188
|
+
confirmations: int | None = None,
|
|
186
189
|
) -> tuple[bool, Any]:
|
|
187
190
|
try:
|
|
188
191
|
chain_id = transaction["chainId"]
|
|
189
192
|
from_address = transaction["from"]
|
|
190
193
|
|
|
194
|
+
# Default confirmation behavior:
|
|
195
|
+
# - Base: wait for 2 additional blocks after the receipt block
|
|
196
|
+
# - Others: do not wait for additional confirmations
|
|
197
|
+
effective_confirmations = confirmations
|
|
198
|
+
if effective_confirmations is None:
|
|
199
|
+
effective_confirmations = 2 if int(chain_id) == BASE_CHAIN_ID else 0
|
|
200
|
+
effective_confirmations = max(0, int(effective_confirmations))
|
|
201
|
+
|
|
191
202
|
web3 = self.get_web3(chain_id)
|
|
192
203
|
try:
|
|
193
204
|
transaction = self._validate_transaction(transaction)
|
|
@@ -210,6 +221,8 @@ class LocalEvmTxn(EvmTxn):
|
|
|
210
221
|
result["receipt"] = self._format_receipt(receipt)
|
|
211
222
|
# Add block_number at top level for convenience
|
|
212
223
|
result["block_number"] = result["receipt"].get("blockNumber")
|
|
224
|
+
result["confirmations"] = effective_confirmations
|
|
225
|
+
result["confirmed_block_number"] = result["block_number"]
|
|
213
226
|
|
|
214
227
|
receipt_status = result["receipt"].get("status")
|
|
215
228
|
if receipt_status is not None and int(receipt_status) != 1:
|
|
@@ -219,11 +232,14 @@ class LocalEvmTxn(EvmTxn):
|
|
|
219
232
|
)
|
|
220
233
|
|
|
221
234
|
# Wait for additional confirmations if requested
|
|
222
|
-
if
|
|
235
|
+
if effective_confirmations > 0:
|
|
223
236
|
tx_block = result["receipt"].get("blockNumber")
|
|
224
237
|
if tx_block:
|
|
225
238
|
await self._wait_for_confirmations(
|
|
226
|
-
web3, tx_block,
|
|
239
|
+
web3, tx_block, effective_confirmations
|
|
240
|
+
)
|
|
241
|
+
result["confirmed_block_number"] = int(tx_block) + int(
|
|
242
|
+
effective_confirmations
|
|
227
243
|
)
|
|
228
244
|
|
|
229
245
|
return (True, result)
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from eth_utils import to_checksum_address
|
|
8
8
|
from loguru import logger
|
|
9
|
-
from web3 import
|
|
9
|
+
from web3 import Web3
|
|
10
10
|
|
|
11
11
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
12
12
|
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
@@ -85,7 +85,6 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
85
85
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
86
86
|
"""Build the transaction dictionary for an ERC20 approval."""
|
|
87
87
|
try:
|
|
88
|
-
web3 = self.wallet_provider.get_web3(chain_id)
|
|
89
88
|
token_checksum = to_checksum_address(token_address)
|
|
90
89
|
from_checksum = to_checksum_address(from_address)
|
|
91
90
|
spender_checksum = to_checksum_address(spender)
|
|
@@ -99,7 +98,6 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
99
98
|
from_address=from_checksum,
|
|
100
99
|
spender=spender_checksum,
|
|
101
100
|
amount=amount_int,
|
|
102
|
-
web3=web3,
|
|
103
101
|
)
|
|
104
102
|
return True, approve_tx
|
|
105
103
|
|
|
@@ -154,7 +152,7 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
154
152
|
|
|
155
153
|
def _chain_id(self, chain: Any) -> int:
|
|
156
154
|
if isinstance(chain, dict):
|
|
157
|
-
chain_id = chain.get("id")
|
|
155
|
+
chain_id = chain.get("id")
|
|
158
156
|
else:
|
|
159
157
|
chain_id = getattr(chain, "id", None)
|
|
160
158
|
if chain_id is None:
|
|
@@ -215,10 +213,8 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
215
213
|
from_address: str,
|
|
216
214
|
spender: str,
|
|
217
215
|
amount: int,
|
|
218
|
-
web3: AsyncWeb3,
|
|
219
216
|
) -> dict[str, Any]:
|
|
220
217
|
"""Build an ERC20 approval transaction dict."""
|
|
221
|
-
del web3 # Use sync Web3 for encoding (AsyncContract doesn't have encodeABI)
|
|
222
218
|
token_checksum = to_checksum_address(token_address)
|
|
223
219
|
spender_checksum = to_checksum_address(spender)
|
|
224
220
|
from_checksum = to_checksum_address(from_address)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from wayfinder_paths.core.services.local_evm_txn import BASE_CHAIN_ID, LocalEvmTxn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _FakeTxHash:
|
|
11
|
+
def __init__(self, value: str):
|
|
12
|
+
self._value = value
|
|
13
|
+
|
|
14
|
+
def hex(self) -> str:
|
|
15
|
+
return self._value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_base_defaults_to_two_confirmations():
|
|
20
|
+
txn = LocalEvmTxn(config={})
|
|
21
|
+
|
|
22
|
+
fake_web3 = MagicMock()
|
|
23
|
+
fake_web3.eth = MagicMock()
|
|
24
|
+
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
25
|
+
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
26
|
+
return_value={
|
|
27
|
+
"status": 1,
|
|
28
|
+
"blockNumber": 100,
|
|
29
|
+
"transactionHash": "0x1",
|
|
30
|
+
"gasUsed": 21_000,
|
|
31
|
+
"logs": [],
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
36
|
+
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
37
|
+
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
38
|
+
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
39
|
+
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
40
|
+
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
41
|
+
txn._close_web3 = AsyncMock()
|
|
42
|
+
txn._wait_for_confirmations = AsyncMock()
|
|
43
|
+
|
|
44
|
+
ok, result = await txn.broadcast_transaction(
|
|
45
|
+
{
|
|
46
|
+
"chainId": BASE_CHAIN_ID,
|
|
47
|
+
"from": "0x0000000000000000000000000000000000000001",
|
|
48
|
+
"to": "0x0000000000000000000000000000000000000002",
|
|
49
|
+
"value": 0,
|
|
50
|
+
},
|
|
51
|
+
wait_for_receipt=True,
|
|
52
|
+
timeout=1,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert ok is True
|
|
56
|
+
txn._wait_for_confirmations.assert_awaited_once_with(fake_web3, 100, 2)
|
|
57
|
+
assert result["confirmations"] == 2
|
|
58
|
+
assert result["confirmed_block_number"] == 102
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_non_base_defaults_to_zero_confirmations():
|
|
63
|
+
txn = LocalEvmTxn(config={})
|
|
64
|
+
|
|
65
|
+
fake_web3 = MagicMock()
|
|
66
|
+
fake_web3.eth = MagicMock()
|
|
67
|
+
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
68
|
+
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
69
|
+
return_value={
|
|
70
|
+
"status": 1,
|
|
71
|
+
"blockNumber": 100,
|
|
72
|
+
"transactionHash": "0x1",
|
|
73
|
+
"gasUsed": 21_000,
|
|
74
|
+
"logs": [],
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
79
|
+
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
80
|
+
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
81
|
+
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
82
|
+
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
83
|
+
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
84
|
+
txn._close_web3 = AsyncMock()
|
|
85
|
+
txn._wait_for_confirmations = AsyncMock()
|
|
86
|
+
|
|
87
|
+
ok, result = await txn.broadcast_transaction(
|
|
88
|
+
{
|
|
89
|
+
"chainId": 1,
|
|
90
|
+
"from": "0x0000000000000000000000000000000000000001",
|
|
91
|
+
"to": "0x0000000000000000000000000000000000000002",
|
|
92
|
+
"value": 0,
|
|
93
|
+
},
|
|
94
|
+
wait_for_receipt=True,
|
|
95
|
+
timeout=1,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert ok is True
|
|
99
|
+
txn._wait_for_confirmations.assert_not_awaited()
|
|
100
|
+
assert result["confirmations"] == 0
|
|
101
|
+
assert result["confirmed_block_number"] == 100
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_explicit_confirmations_override_defaults():
|
|
106
|
+
txn = LocalEvmTxn(config={})
|
|
107
|
+
|
|
108
|
+
fake_web3 = MagicMock()
|
|
109
|
+
fake_web3.eth = MagicMock()
|
|
110
|
+
fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
|
|
111
|
+
fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
|
|
112
|
+
return_value={
|
|
113
|
+
"status": 1,
|
|
114
|
+
"blockNumber": 100,
|
|
115
|
+
"transactionHash": "0x1",
|
|
116
|
+
"gasUsed": 21_000,
|
|
117
|
+
"logs": [],
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
txn.get_web3 = MagicMock(return_value=fake_web3)
|
|
122
|
+
txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
|
|
123
|
+
txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
124
|
+
txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
|
|
125
|
+
txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
|
|
126
|
+
txn._sign_transaction = MagicMock(return_value=b"signed")
|
|
127
|
+
txn._close_web3 = AsyncMock()
|
|
128
|
+
txn._wait_for_confirmations = AsyncMock()
|
|
129
|
+
|
|
130
|
+
ok, result = await txn.broadcast_transaction(
|
|
131
|
+
{
|
|
132
|
+
"chainId": BASE_CHAIN_ID,
|
|
133
|
+
"from": "0x0000000000000000000000000000000000000001",
|
|
134
|
+
"to": "0x0000000000000000000000000000000000000002",
|
|
135
|
+
"value": 0,
|
|
136
|
+
},
|
|
137
|
+
wait_for_receipt=True,
|
|
138
|
+
timeout=1,
|
|
139
|
+
confirmations=0,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
assert ok is True
|
|
143
|
+
txn._wait_for_confirmations.assert_not_awaited()
|
|
144
|
+
assert result["confirmations"] == 0
|
|
145
|
+
assert result["confirmed_block_number"] == 100
|