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.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {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