wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.24__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/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from loguru import logger
|
|
5
|
-
|
|
6
|
-
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
7
|
-
from wayfinder_paths.core.config import StrategyJobConfig
|
|
8
|
-
from wayfinder_paths.core.strategies.Strategy import Strategy
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class StrategyJob:
|
|
12
|
-
def __init__(
|
|
13
|
-
self,
|
|
14
|
-
strategy: Strategy,
|
|
15
|
-
config: StrategyJobConfig,
|
|
16
|
-
clients: dict[str, Any] | None = None,
|
|
17
|
-
skip_auth: bool = False,
|
|
18
|
-
):
|
|
19
|
-
self.strategy = strategy
|
|
20
|
-
self.config = config
|
|
21
|
-
|
|
22
|
-
self.job_id = strategy.name or "unknown"
|
|
23
|
-
self.clients = ClientManager(clients=clients, skip_auth=skip_auth)
|
|
24
|
-
|
|
25
|
-
def _setup_strategy(self):
|
|
26
|
-
if not self.strategy:
|
|
27
|
-
raise ValueError("No strategy provided to StrategyJob")
|
|
28
|
-
|
|
29
|
-
self.strategy.log = self.log
|
|
30
|
-
|
|
31
|
-
async def setup(self):
|
|
32
|
-
self._setup_strategy()
|
|
33
|
-
|
|
34
|
-
# Ensure API key is set for API calls
|
|
35
|
-
# All clients inherit from WayfinderClient and have _ensure_api_key()
|
|
36
|
-
if not self.clients._skip_auth:
|
|
37
|
-
# Ensure API key on any client (they all share the same method)
|
|
38
|
-
token_client = self.clients.token
|
|
39
|
-
if token_client:
|
|
40
|
-
token_client._ensure_api_key()
|
|
41
|
-
|
|
42
|
-
existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
|
|
43
|
-
strategy_cfg = dict(self.config.strategy_config or {})
|
|
44
|
-
merged_cfg = {**strategy_cfg, **existing_cfg}
|
|
45
|
-
self.strategy.config = merged_cfg
|
|
46
|
-
self.strategy.clients = self.clients
|
|
47
|
-
await self.strategy.setup()
|
|
48
|
-
|
|
49
|
-
async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
|
|
50
|
-
try:
|
|
51
|
-
if action == "deposit":
|
|
52
|
-
result = await self.strategy.deposit(**kwargs)
|
|
53
|
-
elif action == "withdraw":
|
|
54
|
-
result = await self.strategy.withdraw(**kwargs)
|
|
55
|
-
elif action == "update":
|
|
56
|
-
result = await self.strategy.update()
|
|
57
|
-
elif action == "status":
|
|
58
|
-
result = await self.strategy.status()
|
|
59
|
-
elif action == "exit":
|
|
60
|
-
result = await self.strategy.exit(**kwargs)
|
|
61
|
-
elif action == "partial_liquidate":
|
|
62
|
-
usd_value = kwargs.get("usd_value")
|
|
63
|
-
if usd_value is None:
|
|
64
|
-
result = (
|
|
65
|
-
False,
|
|
66
|
-
"usd_value parameter is required for partial_liquidate",
|
|
67
|
-
)
|
|
68
|
-
else:
|
|
69
|
-
result = await self.strategy.partial_liquidate(usd_value)
|
|
70
|
-
else:
|
|
71
|
-
result = {"success": False, "message": f"Unknown action: {action}"}
|
|
72
|
-
|
|
73
|
-
await self.log(f"Strategy action '{action}' completed: {result}")
|
|
74
|
-
return result
|
|
75
|
-
|
|
76
|
-
except Exception as e:
|
|
77
|
-
error_msg = f"Strategy action '{action}' failed: {str(e)}"
|
|
78
|
-
await self.log(error_msg)
|
|
79
|
-
await self.handle_error({"error": str(e), "action": action})
|
|
80
|
-
return {"success": False, "error": str(e)}
|
|
81
|
-
|
|
82
|
-
async def run_continuous(self, interval_seconds: int | None = None):
|
|
83
|
-
interval = interval_seconds or self.config.system.update_interval
|
|
84
|
-
logger.info(
|
|
85
|
-
f"Starting continuous execution for strategy: {self.strategy.name} with interval {interval}s"
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
while True:
|
|
89
|
-
try:
|
|
90
|
-
await self.execute_strategy("update")
|
|
91
|
-
await asyncio.sleep(interval)
|
|
92
|
-
|
|
93
|
-
except asyncio.CancelledError:
|
|
94
|
-
logger.info("Continuous execution cancelled")
|
|
95
|
-
break
|
|
96
|
-
except Exception as e:
|
|
97
|
-
logger.error(f"Error in continuous execution: {str(e)}")
|
|
98
|
-
await asyncio.sleep(interval)
|
|
99
|
-
|
|
100
|
-
async def log(self, msg: str):
|
|
101
|
-
logger.info(f"Job {self.job_id}: {msg}")
|
|
102
|
-
|
|
103
|
-
async def handle_error(self, error_data: dict[str, Any]) -> None:
|
|
104
|
-
pass
|
|
105
|
-
|
|
106
|
-
async def stop(self):
|
|
107
|
-
if hasattr(self.strategy, "stop"):
|
|
108
|
-
await self.strategy.stop()
|
|
109
|
-
|
|
110
|
-
logger.info(f"Strategy job {self.job_id} stopped")
|
|
@@ -1,145 +0,0 @@
|
|
|
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
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
import re
|
|
5
|
-
import shutil
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from wayfinder_paths.core.utils.wallets import make_random_wallet, write_wallet_to_json
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def sanitize_name(name: str) -> str:
|
|
12
|
-
# Replace spaces and special chars with underscores, lowercase
|
|
13
|
-
name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
|
|
14
|
-
name = re.sub(r"_+", "_", name)
|
|
15
|
-
name = name.strip("_")
|
|
16
|
-
return name.lower()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def update_strategy_file(strategy_path: Path, class_name: str) -> None:
|
|
20
|
-
content = strategy_path.read_text()
|
|
21
|
-
# Replace MyStrategy with the new class name
|
|
22
|
-
content = content.replace("MyStrategy", class_name)
|
|
23
|
-
# Replace my_strategy references in docstrings/comments
|
|
24
|
-
content = re.sub(
|
|
25
|
-
r"my_strategy", class_name.lower().replace("Strategy", ""), content
|
|
26
|
-
)
|
|
27
|
-
strategy_path.write_text(content)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def main():
|
|
31
|
-
parser = argparse.ArgumentParser(
|
|
32
|
-
description="Create a new strategy from template with dedicated wallet"
|
|
33
|
-
)
|
|
34
|
-
parser.add_argument(
|
|
35
|
-
"name",
|
|
36
|
-
help="Strategy name (e.g., 'my_awesome_strategy' or 'My Awesome Strategy')",
|
|
37
|
-
)
|
|
38
|
-
parser.add_argument(
|
|
39
|
-
"--template-dir",
|
|
40
|
-
type=Path,
|
|
41
|
-
default=Path(__file__).parent.parent / "templates" / "strategy",
|
|
42
|
-
help="Path to strategy template directory",
|
|
43
|
-
)
|
|
44
|
-
parser.add_argument(
|
|
45
|
-
"--strategies-dir",
|
|
46
|
-
type=Path,
|
|
47
|
-
default=Path(__file__).parent.parent / "strategies",
|
|
48
|
-
help="Path to strategies directory",
|
|
49
|
-
)
|
|
50
|
-
parser.add_argument(
|
|
51
|
-
"--wallets-file",
|
|
52
|
-
type=Path,
|
|
53
|
-
default=Path(__file__).parent.parent.parent / "config.json",
|
|
54
|
-
help="Path to config.json file",
|
|
55
|
-
)
|
|
56
|
-
parser.add_argument(
|
|
57
|
-
"--override",
|
|
58
|
-
action="store_true",
|
|
59
|
-
help="Override existing strategy directory if it exists",
|
|
60
|
-
)
|
|
61
|
-
args = parser.parse_args()
|
|
62
|
-
|
|
63
|
-
# Sanitize name for directory
|
|
64
|
-
dir_name = sanitize_name(args.name)
|
|
65
|
-
strategy_dir = args.strategies_dir / dir_name
|
|
66
|
-
|
|
67
|
-
if strategy_dir.exists() and not args.override:
|
|
68
|
-
raise SystemExit(
|
|
69
|
-
f"Strategy directory already exists: {strategy_dir}\n"
|
|
70
|
-
"Use --override to replace it"
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
if not args.template_dir.exists():
|
|
74
|
-
raise SystemExit(f"Template directory not found: {args.template_dir}")
|
|
75
|
-
|
|
76
|
-
if strategy_dir.exists():
|
|
77
|
-
print(f"Removing existing directory: {strategy_dir}")
|
|
78
|
-
shutil.rmtree(strategy_dir)
|
|
79
|
-
strategy_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
-
print(f"Created strategy directory: {strategy_dir}")
|
|
81
|
-
|
|
82
|
-
# Copy template files
|
|
83
|
-
template_files = [
|
|
84
|
-
"strategy.py",
|
|
85
|
-
"test_strategy.py",
|
|
86
|
-
"examples.json",
|
|
87
|
-
"README.md",
|
|
88
|
-
]
|
|
89
|
-
for filename in template_files:
|
|
90
|
-
src = args.template_dir / filename
|
|
91
|
-
if src.exists():
|
|
92
|
-
dst = strategy_dir / filename
|
|
93
|
-
shutil.copy2(src, dst)
|
|
94
|
-
print(f" Copied {filename}")
|
|
95
|
-
|
|
96
|
-
# Generate class name from strategy name
|
|
97
|
-
class_name = "".join(word.capitalize() for word in dir_name.split("_"))
|
|
98
|
-
if not class_name.endswith("Strategy"):
|
|
99
|
-
class_name += "Strategy"
|
|
100
|
-
|
|
101
|
-
strategy_file = strategy_dir / "strategy.py"
|
|
102
|
-
if strategy_file.exists():
|
|
103
|
-
update_strategy_file(strategy_file, class_name)
|
|
104
|
-
print(f" Updated strategy.py with class name: {class_name}")
|
|
105
|
-
|
|
106
|
-
# Generate wallet with label matching directory name (strategy identifier)
|
|
107
|
-
# If config.json doesn't exist, create it with a main wallet first
|
|
108
|
-
if not args.wallets_file.exists():
|
|
109
|
-
print(" Creating new config.json with main wallet...")
|
|
110
|
-
main_wallet = make_random_wallet()
|
|
111
|
-
main_wallet["label"] = "main"
|
|
112
|
-
write_wallet_to_json(
|
|
113
|
-
main_wallet,
|
|
114
|
-
out_dir=args.wallets_file.parent,
|
|
115
|
-
filename=args.wallets_file.name,
|
|
116
|
-
)
|
|
117
|
-
print(f" Generated main wallet: {main_wallet['address']}")
|
|
118
|
-
|
|
119
|
-
# Generate strategy wallet (will append to existing config.json)
|
|
120
|
-
wallet = make_random_wallet()
|
|
121
|
-
wallet["label"] = dir_name
|
|
122
|
-
write_wallet_to_json(
|
|
123
|
-
wallet, out_dir=args.wallets_file.parent, filename=args.wallets_file.name
|
|
124
|
-
)
|
|
125
|
-
print(f" Generated strategy wallet: {wallet['address']} (label: {dir_name})")
|
|
126
|
-
|
|
127
|
-
print("\n✅ Strategy created successfully!")
|
|
128
|
-
print(f" Directory: {strategy_dir}")
|
|
129
|
-
print(f" Name: {dir_name}")
|
|
130
|
-
print(f" Class: {class_name}")
|
|
131
|
-
print(f" Wallet: {wallet['address']}")
|
|
132
|
-
print("\nNext steps:")
|
|
133
|
-
print(f" 1. Edit {strategy_dir / 'strategy.py'} to implement your strategy")
|
|
134
|
-
print(" 2. Add required adapters in __init__")
|
|
135
|
-
print(f" 3. Test with: just test-strategy {dir_name}")
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if __name__ == "__main__":
|
|
139
|
-
main()
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import json
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from eth_account import Account
|
|
6
|
-
|
|
7
|
-
from wayfinder_paths.core.utils.wallets import (
|
|
8
|
-
load_wallets,
|
|
9
|
-
make_random_wallet,
|
|
10
|
-
write_wallet_to_json,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def to_keystore_json(private_key_hex: str, password: str):
|
|
15
|
-
return Account.encrypt(private_key_hex, password)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def main():
|
|
19
|
-
parser = argparse.ArgumentParser(description="Generate local dev wallets")
|
|
20
|
-
parser.add_argument(
|
|
21
|
-
"-n",
|
|
22
|
-
type=int,
|
|
23
|
-
default=0,
|
|
24
|
-
help="Number of wallets to create (ignored if --label is used)",
|
|
25
|
-
)
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
"--out-dir",
|
|
28
|
-
type=Path,
|
|
29
|
-
default=Path("."),
|
|
30
|
-
help="Output directory for config.json (and keystore files)",
|
|
31
|
-
)
|
|
32
|
-
parser.add_argument(
|
|
33
|
-
"--keystore-password",
|
|
34
|
-
type=str,
|
|
35
|
-
default=None,
|
|
36
|
-
help="Optional password to write geth-compatible keystores",
|
|
37
|
-
)
|
|
38
|
-
parser.add_argument(
|
|
39
|
-
"--label",
|
|
40
|
-
type=str,
|
|
41
|
-
default=None,
|
|
42
|
-
help="Create a wallet with a custom label (e.g., strategy name). If not provided, auto-generates labels.",
|
|
43
|
-
)
|
|
44
|
-
parser.add_argument(
|
|
45
|
-
"--default",
|
|
46
|
-
action="store_true",
|
|
47
|
-
help="Create a default 'main' wallet if none exists (used by CI)",
|
|
48
|
-
)
|
|
49
|
-
args = parser.parse_args()
|
|
50
|
-
|
|
51
|
-
# --default is equivalent to -n 1 (create main wallet if needed)
|
|
52
|
-
if args.default and args.n == 0 and not args.label:
|
|
53
|
-
args.n = 1
|
|
54
|
-
|
|
55
|
-
args.out_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
|
|
57
|
-
existing = load_wallets(args.out_dir, "config.json")
|
|
58
|
-
has_main = any(w.get("label") in ("main", "default") for w in existing)
|
|
59
|
-
|
|
60
|
-
rows: list[dict[str, str]] = []
|
|
61
|
-
index = 0
|
|
62
|
-
|
|
63
|
-
# Custom labeled wallet (e.g., for strategy name)
|
|
64
|
-
if args.label:
|
|
65
|
-
# Check if label already exists - if so, skip (don't create duplicate)
|
|
66
|
-
if any(w.get("label") == args.label for w in existing):
|
|
67
|
-
print(f"Wallet with label '{args.label}' already exists, skipping...")
|
|
68
|
-
else:
|
|
69
|
-
w = make_random_wallet()
|
|
70
|
-
w["label"] = args.label
|
|
71
|
-
rows.append(w)
|
|
72
|
-
print(f"[{index}] {w['address']} (label: {args.label})")
|
|
73
|
-
write_wallet_to_json(w, out_dir=args.out_dir, filename="config.json")
|
|
74
|
-
if args.keystore_password:
|
|
75
|
-
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
76
|
-
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
77
|
-
ks_path.write_text(json.dumps(ks))
|
|
78
|
-
index += 1
|
|
79
|
-
|
|
80
|
-
# If no wallets existed before, also create a "main" wallet
|
|
81
|
-
if not existing:
|
|
82
|
-
main_w = make_random_wallet()
|
|
83
|
-
main_w["label"] = "main"
|
|
84
|
-
rows.append(main_w)
|
|
85
|
-
print(f"[{index}] {main_w['address']} (main)")
|
|
86
|
-
write_wallet_to_json(
|
|
87
|
-
main_w, out_dir=args.out_dir, filename="config.json"
|
|
88
|
-
)
|
|
89
|
-
if args.keystore_password:
|
|
90
|
-
ks = to_keystore_json(
|
|
91
|
-
main_w["private_key_hex"], args.keystore_password
|
|
92
|
-
)
|
|
93
|
-
ks_path = args.out_dir / f"keystore_{main_w['address']}.json"
|
|
94
|
-
ks_path.write_text(json.dumps(ks))
|
|
95
|
-
index += 1
|
|
96
|
-
else:
|
|
97
|
-
if args.n == 0:
|
|
98
|
-
args.n = 1
|
|
99
|
-
|
|
100
|
-
# Find next temporary number
|
|
101
|
-
existing_labels = {
|
|
102
|
-
w.get("label", "")
|
|
103
|
-
for w in existing
|
|
104
|
-
if w.get("label", "").startswith("temporary_")
|
|
105
|
-
}
|
|
106
|
-
temp_numbers = set()
|
|
107
|
-
for label in existing_labels:
|
|
108
|
-
try:
|
|
109
|
-
num = int(label.replace("temporary_", ""))
|
|
110
|
-
temp_numbers.add(num)
|
|
111
|
-
except ValueError:
|
|
112
|
-
pass
|
|
113
|
-
next_temp_num = 1
|
|
114
|
-
if temp_numbers:
|
|
115
|
-
next_temp_num = max(temp_numbers) + 1
|
|
116
|
-
|
|
117
|
-
for i in range(args.n):
|
|
118
|
-
w = make_random_wallet()
|
|
119
|
-
# Label first wallet as "main" if main doesn't exist, otherwise use temporary_N
|
|
120
|
-
if i == 0 and not has_main:
|
|
121
|
-
w["label"] = "main"
|
|
122
|
-
rows.append(w)
|
|
123
|
-
print(f"[{index}] {w['address']} (main)")
|
|
124
|
-
else:
|
|
125
|
-
# Find next available temporary number
|
|
126
|
-
while next_temp_num in temp_numbers:
|
|
127
|
-
next_temp_num += 1
|
|
128
|
-
w["label"] = f"temporary_{next_temp_num}"
|
|
129
|
-
temp_numbers.add(next_temp_num)
|
|
130
|
-
rows.append(w)
|
|
131
|
-
print(f"[{index}] {w['address']} (label: temporary_{next_temp_num})")
|
|
132
|
-
|
|
133
|
-
write_wallet_to_json(w, out_dir=args.out_dir, filename="config.json")
|
|
134
|
-
if args.keystore_password:
|
|
135
|
-
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
136
|
-
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
137
|
-
ks_path.write_text(json.dumps(ks))
|
|
138
|
-
index += 1
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if __name__ == "__main__":
|
|
142
|
-
main()
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# Adapter Template
|
|
2
|
-
|
|
3
|
-
Adapters expose protocol-specific capabilities to strategies. They wrap one or more clients from `wayfinder_paths.core.clients`.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
1. Copy the template:
|
|
8
|
-
```bash
|
|
9
|
-
cp -r wayfinder_paths/templates/adapter wayfinder_paths/adapters/my_adapter
|
|
10
|
-
```
|
|
11
|
-
2. Rename `MyAdapter` in `adapter.py` to match your adapter's purpose.
|
|
12
|
-
3. Set `adapter_type` to a unique identifier (e.g., `"MY_PROTOCOL"`).
|
|
13
|
-
4. Implement your public methods.
|
|
14
|
-
5. Add tests in `test_adapter.py`.
|
|
15
|
-
|
|
16
|
-
## Directory Structure
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
my_adapter/
|
|
20
|
-
├── adapter.py # Adapter implementation
|
|
21
|
-
├── examples.json # Example payloads (optional)
|
|
22
|
-
├── test_adapter.py # Pytest tests
|
|
23
|
-
└── README.md # Adapter documentation
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Adapter Structure
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
from typing import Any
|
|
30
|
-
|
|
31
|
-
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
32
|
-
from wayfinder_paths.core.clients.SomeClient import SomeClient
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class MyAdapter(BaseAdapter):
|
|
36
|
-
"""Adapter for MyProtocol operations."""
|
|
37
|
-
|
|
38
|
-
adapter_type = "MY_PROTOCOL"
|
|
39
|
-
|
|
40
|
-
def __init__(self, config: dict[str, Any] | None = None):
|
|
41
|
-
super().__init__("my_adapter", config)
|
|
42
|
-
self.client = SomeClient()
|
|
43
|
-
|
|
44
|
-
async def connect(self) -> bool:
|
|
45
|
-
"""Optional: Establish connectivity."""
|
|
46
|
-
return True
|
|
47
|
-
|
|
48
|
-
async def do_something(self, param: str) -> tuple[bool, Any]:
|
|
49
|
-
"""
|
|
50
|
-
Execute an operation.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
param: Operation parameter
|
|
54
|
-
|
|
55
|
-
Returns:
|
|
56
|
-
Tuple of (success, data) where data is result or error message
|
|
57
|
-
"""
|
|
58
|
-
try:
|
|
59
|
-
result = await self.client.call(param)
|
|
60
|
-
return (True, result)
|
|
61
|
-
except Exception as e:
|
|
62
|
-
self.logger.error(f"Operation failed: {e}")
|
|
63
|
-
return (False, str(e))
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Key Conventions
|
|
67
|
-
|
|
68
|
-
1. **Return tuples**: All methods return `(success: bool, data: Any)`
|
|
69
|
-
2. **Adapter type**: Set `adapter_type` for registry lookups
|
|
70
|
-
3. **Config access**: Use `self.config` for configuration
|
|
71
|
-
4. **Logging**: Use `self.logger` for consistent logging
|
|
72
|
-
5. **Error handling**: Catch exceptions and return `(False, error_message)`
|
|
73
|
-
|
|
74
|
-
## BaseAdapter Interface
|
|
75
|
-
|
|
76
|
-
```python
|
|
77
|
-
class BaseAdapter(ABC):
|
|
78
|
-
adapter_type: str | None = None
|
|
79
|
-
|
|
80
|
-
def __init__(self, name: str, config: dict | None = None):
|
|
81
|
-
self.name = name
|
|
82
|
-
self.config = config or {}
|
|
83
|
-
self.logger = logger.bind(adapter=self.__class__.__name__)
|
|
84
|
-
|
|
85
|
-
async def connect(self) -> bool:
|
|
86
|
-
"""Establish connectivity (default: True)."""
|
|
87
|
-
return True
|
|
88
|
-
|
|
89
|
-
async def get_balance(self, asset: str) -> dict:
|
|
90
|
-
"""Get balance (raises NotImplementedError by default)."""
|
|
91
|
-
raise NotImplementedError
|
|
92
|
-
|
|
93
|
-
async def health_check(self) -> dict:
|
|
94
|
-
"""Check adapter health."""
|
|
95
|
-
...
|
|
96
|
-
|
|
97
|
-
async def close(self) -> None:
|
|
98
|
-
"""Clean up resources."""
|
|
99
|
-
pass
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Testing
|
|
103
|
-
|
|
104
|
-
Create `test_adapter.py`:
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
import pytest
|
|
108
|
-
from unittest.mock import AsyncMock, patch
|
|
109
|
-
from .adapter import MyAdapter
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class TestMyAdapter:
|
|
113
|
-
@pytest.fixture
|
|
114
|
-
def adapter(self):
|
|
115
|
-
return MyAdapter()
|
|
116
|
-
|
|
117
|
-
@pytest.mark.asyncio
|
|
118
|
-
async def test_do_something_success(self, adapter):
|
|
119
|
-
with patch.object(adapter, "client") as mock_client:
|
|
120
|
-
mock_client.call = AsyncMock(return_value={"result": "ok"})
|
|
121
|
-
|
|
122
|
-
success, data = await adapter.do_something(param="test")
|
|
123
|
-
|
|
124
|
-
assert success
|
|
125
|
-
assert data["result"] == "ok"
|
|
126
|
-
|
|
127
|
-
@pytest.mark.asyncio
|
|
128
|
-
async def test_do_something_failure(self, adapter):
|
|
129
|
-
with patch.object(adapter, "client") as mock_client:
|
|
130
|
-
mock_client.call = AsyncMock(side_effect=Exception("API error"))
|
|
131
|
-
|
|
132
|
-
success, data = await adapter.do_something(param="test")
|
|
133
|
-
|
|
134
|
-
assert not success
|
|
135
|
-
assert "error" in data.lower()
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Run tests:
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
poetry run pytest wayfinder_paths/adapters/my_adapter/ -v
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
## Best Practices
|
|
145
|
-
|
|
146
|
-
- Keep adapters thin - business logic belongs in strategies
|
|
147
|
-
- Mock clients in tests, not adapters
|
|
148
|
-
- Document each public method with Args/Returns docstrings
|
|
149
|
-
- Use type hints for all parameters and return values
|
|
150
|
-
- Log errors with context for debugging
|