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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
- 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/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -24
- wayfinder_paths/core/engine/StrategyJob.py +3 -1
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +22 -4
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -8
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +42 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/sdk_example.py +0 -125
- wayfinder_paths/core/engine/__init__.py +0 -5
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +0 -130
- wayfinder_paths/core/services/local_evm_txn.py +0 -334
- wayfinder_paths/core/services/local_token_txn.py +0 -242
- wayfinder_paths/core/services/web3_service.py +0 -43
- wayfinder_paths/core/wallets/README.md +0 -88
- wayfinder_paths/core/wallets/WalletManager.py +0 -56
- wayfinder_paths/core/wallets/__init__.py +0 -7
- wayfinder_paths/scripts/run_strategy.py +0 -152
- wayfinder_paths/strategies/config.py +0 -85
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
|
@@ -41,7 +41,7 @@ class TestPoolAdapter:
|
|
|
41
41
|
pool_ids=["pool-123", "pool-456"]
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
assert success
|
|
44
|
+
assert success
|
|
45
45
|
assert data == mock_response
|
|
46
46
|
mock_pool_client.get_pools_by_ids.assert_called_once_with(
|
|
47
47
|
pool_ids=["pool-123", "pool-456"]
|
|
@@ -66,7 +66,7 @@ class TestPoolAdapter:
|
|
|
66
66
|
|
|
67
67
|
success, data = await adapter.get_pools()
|
|
68
68
|
|
|
69
|
-
assert success
|
|
69
|
+
assert success
|
|
70
70
|
assert data == mock_response
|
|
71
71
|
|
|
72
72
|
@pytest.mark.asyncio
|
|
@@ -33,7 +33,7 @@ class TestTokenAdapter:
|
|
|
33
33
|
):
|
|
34
34
|
success, data = await adapter.get_token("0x1234...")
|
|
35
35
|
|
|
36
|
-
assert success
|
|
36
|
+
assert success
|
|
37
37
|
assert data == mock_token_data
|
|
38
38
|
|
|
39
39
|
@pytest.mark.asyncio
|
|
@@ -55,7 +55,7 @@ class TestTokenAdapter:
|
|
|
55
55
|
):
|
|
56
56
|
success, data = await adapter.get_token("token-123")
|
|
57
57
|
|
|
58
|
-
assert success
|
|
58
|
+
assert success
|
|
59
59
|
assert data == mock_token_data
|
|
60
60
|
|
|
61
61
|
def test_adapter_type(self, adapter):
|
|
@@ -80,7 +80,7 @@ class TestTokenAdapter:
|
|
|
80
80
|
):
|
|
81
81
|
success, data = await adapter.get_token_price("test-token")
|
|
82
82
|
|
|
83
|
-
assert success
|
|
83
|
+
assert success
|
|
84
84
|
assert data["current_price"] == 1.50
|
|
85
85
|
assert data["symbol"] == "TEST"
|
|
86
86
|
assert data["name"] == "Test Token"
|
|
@@ -113,7 +113,7 @@ class TestTokenAdapter:
|
|
|
113
113
|
):
|
|
114
114
|
success, data = await adapter.get_gas_token("base")
|
|
115
115
|
|
|
116
|
-
assert success
|
|
116
|
+
assert success
|
|
117
117
|
assert data["symbol"] == "ETH"
|
|
118
118
|
assert data["name"] == "Ethereum"
|
|
119
119
|
|
wayfinder_paths/core/config.py
CHANGED
|
@@ -5,13 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
7
|
|
|
8
|
-
from wayfinder_paths.core.constants.base import (
|
|
9
|
-
ADAPTER_BALANCE,
|
|
10
|
-
ADAPTER_BRAP,
|
|
11
|
-
ADAPTER_HYPERLIQUID,
|
|
12
|
-
ADAPTER_MOONWELL,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
8
|
|
|
16
9
|
def _load_config_file() -> dict[str, Any]:
|
|
17
10
|
path = Path("config.json")
|
|
@@ -25,6 +18,14 @@ def _load_config_file() -> dict[str, Any]:
|
|
|
25
18
|
|
|
26
19
|
|
|
27
20
|
CONFIG = _load_config_file()
|
|
21
|
+
SUPPORTED_CHAINS = [
|
|
22
|
+
1,
|
|
23
|
+
8453,
|
|
24
|
+
56,
|
|
25
|
+
42161,
|
|
26
|
+
137,
|
|
27
|
+
999,
|
|
28
|
+
]
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass
|
|
@@ -193,46 +194,6 @@ class StrategyJobConfig:
|
|
|
193
194
|
strategy_config=strategy_config,
|
|
194
195
|
)
|
|
195
196
|
|
|
196
|
-
def get_adapter_config(self, adapter_name: str) -> dict[str, Any]:
|
|
197
|
-
config = {
|
|
198
|
-
"api_base_url": self.system.api_base_url,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if adapter_name in [
|
|
202
|
-
ADAPTER_BALANCE,
|
|
203
|
-
ADAPTER_BRAP,
|
|
204
|
-
ADAPTER_MOONWELL,
|
|
205
|
-
ADAPTER_HYPERLIQUID,
|
|
206
|
-
]:
|
|
207
|
-
strategy_wallet = self.strategy_config.get("strategy_wallet")
|
|
208
|
-
main_wallet = self.strategy_config.get("main_wallet")
|
|
209
|
-
config["strategy_wallet"] = (
|
|
210
|
-
{"address": strategy_wallet["address"]}
|
|
211
|
-
if strategy_wallet
|
|
212
|
-
and isinstance(strategy_wallet, dict)
|
|
213
|
-
and strategy_wallet.get("address")
|
|
214
|
-
else {}
|
|
215
|
-
)
|
|
216
|
-
config["main_wallet"] = (
|
|
217
|
-
{"address": main_wallet["address"]}
|
|
218
|
-
if main_wallet
|
|
219
|
-
and isinstance(main_wallet, dict)
|
|
220
|
-
and main_wallet.get("address")
|
|
221
|
-
else {}
|
|
222
|
-
)
|
|
223
|
-
config["user_wallet"] = (
|
|
224
|
-
config.get("strategy_wallet") or config.get("main_wallet") or {}
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
if adapter_name == ADAPTER_BRAP:
|
|
228
|
-
config["default_slippage"] = self.user.default_slippage
|
|
229
|
-
config["gas_multiplier"] = self.user.gas_multiplier
|
|
230
|
-
|
|
231
|
-
if adapter_name in self.strategy_config.get("adapters", {}):
|
|
232
|
-
config.update(self.strategy_config["adapters"][adapter_name])
|
|
233
|
-
|
|
234
|
-
return config
|
|
235
|
-
|
|
236
197
|
|
|
237
198
|
def get_api_base_url() -> str:
|
|
238
199
|
system = CONFIG.get("system", {}) if isinstance(CONFIG, dict) else {}
|
|
@@ -29,7 +29,6 @@ DEFAULT_SLIPPAGE = 0.005
|
|
|
29
29
|
# Base L2 (and some RPC providers) can occasionally take >2 minutes to index/return receipts,
|
|
30
30
|
# even if the transaction is eventually mined. A longer timeout reduces false negatives that
|
|
31
31
|
# can lead to unsafe retry behavior (nonce gaps, duplicate swaps, etc.).
|
|
32
|
-
DEFAULT_TRANSACTION_TIMEOUT = 300 # Transaction receipt wait timeout
|
|
33
32
|
DEFAULT_HTTP_TIMEOUT = 30.0 # HTTP client timeout
|
|
34
33
|
|
|
35
34
|
# Adapter type identifiers
|
|
@@ -8,8 +8,8 @@ ERC20_ABI = [
|
|
|
8
8
|
{
|
|
9
9
|
"constant": True,
|
|
10
10
|
"inputs": [
|
|
11
|
-
{"name": "
|
|
12
|
-
{"name": "
|
|
11
|
+
{"name": "owner", "type": "address"},
|
|
12
|
+
{"name": "spender", "type": "address"},
|
|
13
13
|
],
|
|
14
14
|
"name": "allowance",
|
|
15
15
|
"outputs": [{"name": "", "type": "uint256"}],
|
|
@@ -18,8 +18,8 @@ ERC20_ABI = [
|
|
|
18
18
|
{
|
|
19
19
|
"constant": False,
|
|
20
20
|
"inputs": [
|
|
21
|
-
{"name": "
|
|
22
|
-
{"name": "
|
|
21
|
+
{"name": "spender", "type": "address"},
|
|
22
|
+
{"name": "value", "type": "uint256"},
|
|
23
23
|
],
|
|
24
24
|
"name": "approve",
|
|
25
25
|
"outputs": [{"name": "", "type": "bool"}],
|
|
@@ -63,8 +63,8 @@ ERC20_ABI = [
|
|
|
63
63
|
{
|
|
64
64
|
"constant": False,
|
|
65
65
|
"inputs": [
|
|
66
|
-
{"name": "
|
|
67
|
-
{"name": "
|
|
66
|
+
{"name": "to", "type": "address"},
|
|
67
|
+
{"name": "value", "type": "uint256"},
|
|
68
68
|
],
|
|
69
69
|
"name": "transfer",
|
|
70
70
|
"outputs": [{"name": "", "type": "bool"}],
|
|
@@ -73,9 +73,9 @@ ERC20_ABI = [
|
|
|
73
73
|
{
|
|
74
74
|
"constant": False,
|
|
75
75
|
"inputs": [
|
|
76
|
-
{"name": "
|
|
77
|
-
{"name": "
|
|
78
|
-
{"name": "
|
|
76
|
+
{"name": "from", "type": "address"},
|
|
77
|
+
{"name": "to", "type": "address"},
|
|
78
|
+
{"name": "value", "type": "uint256"},
|
|
79
79
|
],
|
|
80
80
|
"name": "transferFrom",
|
|
81
81
|
"outputs": [{"name": "", "type": "bool"}],
|
|
@@ -83,23 +83,12 @@ 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,
|
|
100
89
|
"inputs": [
|
|
101
|
-
{"name": "
|
|
102
|
-
{"name": "
|
|
90
|
+
{"name": "owner", "type": "address"},
|
|
91
|
+
{"name": "spender", "type": "address"},
|
|
103
92
|
],
|
|
104
93
|
"name": "allowance",
|
|
105
94
|
"outputs": [{"name": "", "type": "uint256"}],
|
|
@@ -108,8 +97,8 @@ ERC20_APPROVAL_ABI = [
|
|
|
108
97
|
{
|
|
109
98
|
"constant": False,
|
|
110
99
|
"inputs": [
|
|
111
|
-
{"name": "
|
|
112
|
-
{"name": "
|
|
100
|
+
{"name": "spender", "type": "address"},
|
|
101
|
+
{"name": "value", "type": "uint256"},
|
|
113
102
|
],
|
|
114
103
|
"name": "approve",
|
|
115
104
|
"outputs": [{"name": "", "type": "bool"}],
|
|
@@ -62,7 +62,7 @@ class StrategyJob:
|
|
|
62
62
|
await self.strategy.setup()
|
|
63
63
|
|
|
64
64
|
async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
|
|
65
|
-
"""Execute a strategy action (deposit, withdraw, update, status, partial_liquidate)"""
|
|
65
|
+
"""Execute a strategy action (deposit, withdraw, update, status, exit, partial_liquidate)"""
|
|
66
66
|
try:
|
|
67
67
|
if action == "deposit":
|
|
68
68
|
result = await self.strategy.deposit(**kwargs)
|
|
@@ -72,6 +72,8 @@ class StrategyJob:
|
|
|
72
72
|
result = await self.strategy.update()
|
|
73
73
|
elif action == "status":
|
|
74
74
|
result = await self.strategy.status()
|
|
75
|
+
elif action == "exit":
|
|
76
|
+
result = await self.strategy.exit(**kwargs)
|
|
75
77
|
elif action == "partial_liquidate":
|
|
76
78
|
usd_value = kwargs.get("usd_value")
|
|
77
79
|
if usd_value is None:
|
|
@@ -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
|
|
@@ -8,7 +8,6 @@ from typing import Any, TypedDict
|
|
|
8
8
|
from loguru import logger
|
|
9
9
|
|
|
10
10
|
from wayfinder_paths.core.clients.TokenClient import TokenDetails
|
|
11
|
-
from wayfinder_paths.core.services.base import Web3Service
|
|
12
11
|
from wayfinder_paths.core.strategies.descriptors import StratDescriptor
|
|
13
12
|
|
|
14
13
|
|
|
@@ -56,12 +55,17 @@ class Strategy(ABC):
|
|
|
56
55
|
*,
|
|
57
56
|
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
58
57
|
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
|
-
|
|
58
|
+
api_key: str | None = None,
|
|
59
|
+
main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
|
|
60
|
+
strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
|
|
61
|
+
| None = None,
|
|
60
62
|
):
|
|
61
63
|
self.adapters = {}
|
|
62
64
|
self.ledger_adapter = None
|
|
63
65
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
64
66
|
self.config = config
|
|
67
|
+
self.main_wallet_signing_callback = main_wallet_signing_callback
|
|
68
|
+
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
65
69
|
|
|
66
70
|
async def setup(self) -> None:
|
|
67
71
|
"""Initialize strategy-specific setup after construction"""
|
|
@@ -143,8 +147,22 @@ class Strategy(ABC):
|
|
|
143
147
|
@abstractmethod
|
|
144
148
|
async def update(self) -> StatusTuple:
|
|
145
149
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
Deploy funds to protocols (no main wallet access).
|
|
151
|
+
Called after deposit() has transferred assets to strategy wallet.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Tuple of (success: bool, message: str)
|
|
155
|
+
"""
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
160
|
+
"""
|
|
161
|
+
Transfer funds from strategy wallet to main wallet.
|
|
162
|
+
Called after withdraw() has liquidated all positions.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Tuple of (success: bool, message: str)
|
|
148
166
|
"""
|
|
149
167
|
pass
|
|
150
168
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from web3 import AsyncWeb3
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
4
|
+
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
5
|
+
|
|
6
|
+
NATIVE_CURRENCY_ADDRESSES: set = {
|
|
7
|
+
"0x0000000000000000000000000000000000000000",
|
|
8
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
9
|
+
# TODO: This is not a proper SOL address, this short form is for LIFI only, fix this after fixing lifi
|
|
10
|
+
"11111111111111111111111111111111",
|
|
11
|
+
"0x0000000000000000000000000000000000001010",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def get_token_balance(
|
|
16
|
+
token_address: str, chain_id: int, wallet_address: str
|
|
17
|
+
) -> int:
|
|
18
|
+
async with web3_from_chain_id(chain_id) as web3:
|
|
19
|
+
checksum_wallet = AsyncWeb3.to_checksum_address(wallet_address)
|
|
20
|
+
if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
|
|
21
|
+
balance = await web3.eth.get_balance(checksum_wallet)
|
|
22
|
+
return int(balance)
|
|
23
|
+
|
|
24
|
+
checksum_token = AsyncWeb3.to_checksum_address(token_address)
|
|
25
|
+
contract = web3.eth.contract(address=checksum_token, abi=ERC20_ABI)
|
|
26
|
+
balance = await contract.functions.balanceOf(checksum_wallet).call(
|
|
27
|
+
block_identifier="pending"
|
|
28
|
+
)
|
|
29
|
+
return int(balance)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def get_token_allowance(
|
|
33
|
+
token_address: str, chain_id: int, owner_address: str, spender_address: str
|
|
34
|
+
):
|
|
35
|
+
async with web3_from_chain_id(chain_id) as web3:
|
|
36
|
+
contract = web3.eth.contract(
|
|
37
|
+
address=web3.to_checksum_address(token_address), abi=ERC20_ABI
|
|
38
|
+
)
|
|
39
|
+
return await contract.functions.allowance(
|
|
40
|
+
web3.to_checksum_address(owner_address),
|
|
41
|
+
web3.to_checksum_address(spender_address),
|
|
42
|
+
).call(block_identifier="pending")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def build_approve_transaction(
|
|
46
|
+
from_address: str,
|
|
47
|
+
chain_id: int,
|
|
48
|
+
token_address: str,
|
|
49
|
+
spender_address: str,
|
|
50
|
+
amount: int,
|
|
51
|
+
) -> dict:
|
|
52
|
+
"""Build an ERC20 approve transaction."""
|
|
53
|
+
async with web3_from_chain_id(chain_id) as web3:
|
|
54
|
+
contract = web3.eth.contract(
|
|
55
|
+
address=web3.to_checksum_address(token_address), abi=ERC20_ABI
|
|
56
|
+
)
|
|
57
|
+
data = contract.encode_abi(
|
|
58
|
+
"approve",
|
|
59
|
+
[
|
|
60
|
+
web3.to_checksum_address(spender_address),
|
|
61
|
+
amount,
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
return {
|
|
65
|
+
"to": web3.to_checksum_address(token_address),
|
|
66
|
+
"from": web3.to_checksum_address(from_address),
|
|
67
|
+
"data": data,
|
|
68
|
+
"chainId": chain_id,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def build_send_transaction(
|
|
73
|
+
from_address: str,
|
|
74
|
+
to_address: str,
|
|
75
|
+
token_address: str | None,
|
|
76
|
+
chain_id: int,
|
|
77
|
+
amount: int,
|
|
78
|
+
) -> dict:
|
|
79
|
+
async with web3_from_chain_id(chain_id) as web3:
|
|
80
|
+
from_checksum = web3.to_checksum_address(from_address)
|
|
81
|
+
to_checksum = web3.to_checksum_address(to_address)
|
|
82
|
+
|
|
83
|
+
if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
|
|
84
|
+
return {
|
|
85
|
+
"to": to_checksum,
|
|
86
|
+
"from": from_checksum,
|
|
87
|
+
"value": amount,
|
|
88
|
+
"chainId": chain_id,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
token_checksum = web3.to_checksum_address(token_address)
|
|
92
|
+
contract = web3.eth.contract(address=token_checksum, abi=ERC20_ABI)
|
|
93
|
+
data = contract.encode_abi("transfer", [to_checksum, amount])
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"to": token_checksum,
|
|
97
|
+
"from": from_checksum,
|
|
98
|
+
"data": data,
|
|
99
|
+
"chainId": chain_id,
|
|
100
|
+
}
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
EVM helper utilities for common blockchain operations.
|
|
3
3
|
|
|
4
4
|
This module provides reusable functions for EVM-related operations that are shared
|
|
5
|
-
across multiple adapters
|
|
5
|
+
across multiple adapters.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
-
from pathlib import Path
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
12
|
from loguru import logger
|
|
@@ -170,9 +169,3 @@ async def get_abi_filtered(
|
|
|
170
169
|
if item.get("type") == "function" and item.get("name") in function_names
|
|
171
170
|
]
|
|
172
171
|
return filtered_abi
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
with open(Path(__file__).parent.parent.parent.joinpath("abis/generic/erc20.json")) as f:
|
|
176
|
-
erc20_abi_raw = f.read()
|
|
177
|
-
|
|
178
|
-
ERC20_ABI = json.loads(erc20_abi_raw)
|