wayfinder-paths 0.1.7__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 (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,49 @@
1
+ """Test template for adapters.
2
+
3
+ Quick setup:
4
+ 1. Replace MyAdapter with your actual adapter class name
5
+ 2. Implement test_basic_functionality with your adapter's core methods
6
+ 3. Add client mocking if your adapter uses external clients
7
+ 4. Run: pytest wayfinder_paths/adapters/your_adapter/ -v
8
+
9
+ Note: examples.json is optional for adapters (not required).
10
+ """
11
+
12
+ import pytest
13
+
14
+ # TODO: Replace MyAdapter with your actual adapter class name
15
+ from .adapter import MyAdapter
16
+
17
+ # For mocking clients, uncomment when needed:
18
+ # from unittest.mock import AsyncMock, patch
19
+
20
+
21
+ class TestMyAdapter:
22
+ """Test cases for MyAdapter"""
23
+
24
+ @pytest.fixture
25
+ def adapter(self):
26
+ """Create adapter instance for testing."""
27
+ return MyAdapter(config={})
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_health_check(self, adapter):
31
+ """Test adapter health check"""
32
+ health = await adapter.health_check()
33
+ assert isinstance(health, dict)
34
+ assert health.get("status") in {"healthy", "unhealthy", "error"}
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_connect(self, adapter):
38
+ """Test adapter connection"""
39
+ ok = await adapter.connect()
40
+ assert isinstance(ok, bool)
41
+
42
+ def test_capabilities(self, adapter):
43
+ """Test adapter capabilities match manifest"""
44
+ assert hasattr(adapter, "adapter_type")
45
+
46
+ @pytest.mark.asyncio
47
+ async def test_basic_functionality(self, adapter):
48
+ """REQUIRED: Test your adapter's core functionality."""
49
+ assert adapter is not None
@@ -0,0 +1,153 @@
1
+ # Strategy Template
2
+
3
+ This template provides the scaffolding for a new strategy. It mirrors the structure in `wayfinder_paths/strategies/...` and wires into the runner via a manifest.
4
+
5
+ ## Quick Start
6
+
7
+ 1. Copy the template to a new folder:
8
+ ```
9
+ cp -r wayfinder_paths/templates/strategy wayfinder_paths/strategies/my_strategy
10
+ ```
11
+ 2. Rename the class in `strategy.py` and update `manifest.yaml` so `entrypoint` points to your new module (for example `strategies.my_strategy.strategy.MyStrategy`).
12
+ 3. Fill out `examples.json` with sample CLI invocations and `test_strategy.py` with at least one smoke test.
13
+ 4. Implement the required strategy methods (`deposit`, `update`, `_status`, optionally override `withdraw`).
14
+
15
+ ## Layout
16
+
17
+ ```
18
+ my_strategy/
19
+ ├── strategy.py # Strategy implementation
20
+ ├── manifest.yaml # Entrypoint + adapter requirements + policy
21
+ ├── examples.json # Example CLI payloads
22
+ ├── test_strategy.py # Pytest-based smoke tests
23
+ └── README.md # Strategy-specific documentation
24
+ ```
25
+
26
+ ## Required methods
27
+
28
+ ```python
29
+ async def deposit(self, main_token_amount: float, gas_token_amount: float) -> StatusTuple:
30
+ """Move funds from the main wallet into the strategy wallet and prepare on-chain positions."""
31
+
32
+ async def update(self) -> StatusTuple:
33
+ """Periodic rebalance/update loop."""
34
+
35
+ async def _status(self) -> StatusDict:
36
+ """Return portfolio_value, net_deposit, and strategy_status payloads."""
37
+ ```
38
+
39
+ `Strategy.withdraw` already unwinds ledger operations. Override it only if you need custom exit logic.
40
+
41
+ ## Wiring adapters
42
+
43
+ Strategies typically:
44
+
45
+ 1. Build a `DefaultWeb3Service` so every adapter shares a wallet provider.
46
+ 2. Instantiate adapters (balance, ledger, protocol specific, etc.).
47
+ 3. Register adapters via `self.register_adapters([...])` and keep references as attributes.
48
+
49
+ ```python
50
+ from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
51
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
52
+ from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
53
+
54
+
55
+ class MyStrategy(Strategy):
56
+ name = "Demo Strategy"
57
+
58
+ def __init__(self, config: dict | None = None):
59
+ super().__init__()
60
+ self.config = config or {}
61
+ web3_service = DefaultWeb3Service(self.config)
62
+ balance_adapter = BalanceAdapter(self.config, web3_service=web3_service)
63
+ self.register_adapters([balance_adapter])
64
+ self.balance_adapter = balance_adapter
65
+
66
+ async def deposit(
67
+ self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
68
+ ) -> StatusTuple:
69
+ """Perform validation, move funds, and optionally deploy capital."""
70
+ if main_token_amount <= 0:
71
+ return (False, "Nothing to deposit")
72
+
73
+ success, _ = await self.balance_adapter.get_balance(
74
+ token_id=self.config.get("token_id"),
75
+ wallet_address=self.config.get("main_wallet", {}).get("address"),
76
+ )
77
+ if not success:
78
+ return (False, "Unable to fetch balances")
79
+
80
+ # Use BalanceAdapter (which leverages LocalTokenTxnService builders) for transfers.
81
+ self.last_deposit = main_token_amount
82
+ return (True, f"Deposited {main_token_amount} tokens")
83
+
84
+ async def update(self) -> StatusTuple:
85
+ """Execute your strategy logic periodically."""
86
+ return (True, "No-op update")
87
+
88
+ async def _status(self) -> StatusDict:
89
+ """Surface state back to run_strategy.py."""
90
+ success, balance = await self.balance_adapter.get_balance(
91
+ token_id=self.config.get("token_id"),
92
+ wallet_address=self.config.get("strategy_wallet", {}).get("address"),
93
+ )
94
+ return {
95
+ "portfolio_value": float(balance or 0),
96
+ "net_deposit": float(getattr(self, "last_deposit", 0.0)),
97
+ "strategy_status": {"message": "healthy" if success else "unknown"},
98
+ }
99
+ ```
100
+
101
+ ## Manifest configuration
102
+
103
+ Your `manifest.yaml` should define:
104
+
105
+ ```yaml
106
+ schema_version: "0.1"
107
+ entrypoint: "strategies.my_strategy.strategy.MyStrategy"
108
+ permissions:
109
+ policy: "(wallet.id == 'FORMAT_WALLET_ID') && (eth.tx.to == '0x...')"
110
+ adapters:
111
+ - name: "BALANCE"
112
+ capabilities: ["wallet_read", "wallet_transfer"]
113
+ - name: "POOL"
114
+ capabilities: ["pool.read", "pool.analytics"]
115
+ ```
116
+
117
+ ## Testing
118
+
119
+ `test_strategy.py` should cover at least deposit/update/status. Use `pytest.mark.asyncio` and patch adapters or services as needed.
120
+
121
+ ```python
122
+ import pytest
123
+ from wayfinder_paths.strategies.my_strategy.strategy import MyStrategy
124
+
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_status_shape():
128
+ strat = MyStrategy(config={})
129
+ status = await strat._status()
130
+ assert set(status) == {"portfolio_value", "net_deposit", "strategy_status"}
131
+ ```
132
+
133
+ ## Running the strategy locally
134
+
135
+ ```bash
136
+ # Install dependencies & create wallets first
137
+ poetry install
138
+ # Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
139
+ poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
140
+
141
+ # Copy config and edit credentials
142
+ cp wayfinder_paths/config.example.json config.json
143
+
144
+ # Run your strategy
145
+ poetry run python wayfinder_paths/run_strategy.py my_strategy --action status --config $(pwd)/config.json
146
+ ```
147
+
148
+ ## Best practices
149
+
150
+ - Return `(success: bool, message: str)` tuples from `deposit`/`update`.
151
+ - Always populate `portfolio_value`, `net_deposit`, and `strategy_status` keys in `_status`.
152
+ - Use the adapters declared in `manifest.yaml`—they are injected via `register_adapters`.
153
+ - Keep on-chain permissions tight by scoping the manifest policy to the strategy wallet and expected contracts.
@@ -0,0 +1,11 @@
1
+ {
2
+ "deposit": {
3
+ "main_token_amount": 100,
4
+ "gas_token_amount": 0.001
5
+ },
6
+ "update": {},
7
+ "status": {},
8
+ "withdraw": {}
9
+ }
10
+
11
+
@@ -0,0 +1,8 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "strategies.my_strategy.strategy.MyStrategy"
3
+ name: "my_strategy" # Unique name for this strategy instance (used for wallet lookup)
4
+ permissions:
5
+ policy: "(wallet.id == 'FORMAT_WALLET_ID')"
6
+ adapters:
7
+ - name: "BALANCE"
8
+ capabilities: ["wallet_read"]
@@ -0,0 +1,57 @@
1
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
2
+
3
+
4
+ class MyStrategy(Strategy):
5
+ name = "My Strategy"
6
+ description = "Short description of what the strategy does."
7
+ summary = "One-line summary for discovery."
8
+
9
+ def __init__(self):
10
+ super().__init__()
11
+
12
+ async def setup(self):
13
+ """Optional initialization logic."""
14
+ return None
15
+
16
+ async def deposit(
17
+ self, main_token_amount: float, gas_token_amount: float
18
+ ) -> StatusTuple:
19
+ """Deposit funds into the strategy.
20
+
21
+ Args:
22
+ main_token_amount: Amount of the main token to deposit (e.g., USDC, USDT0)
23
+ gas_token_amount: Amount of gas token to deposit (e.g., ETH, HYPE)
24
+ """
25
+ return (True, "Deposit successful")
26
+
27
+ async def withdraw(self, amount: float | None = None) -> StatusTuple:
28
+ """Withdraw funds from the strategy.
29
+
30
+ This method is required. The base Strategy class provides a default
31
+ implementation that unwinds all ledger operations. You can either:
32
+ 1. Call the parent implementation (as shown here, recommended for most cases)
33
+ 2. Override this method for custom withdrawal logic (e.g., unwinding specific positions,
34
+ converting tokens, handling partial withdrawals)
35
+
36
+ Args:
37
+ amount: Optional amount to withdraw. If None, withdraws all funds.
38
+ """
39
+ # Call parent implementation which unwinds all ledger operations
40
+ return await super().withdraw(amount=amount)
41
+
42
+ async def update(self) -> StatusTuple:
43
+ """Rebalance or update positions."""
44
+ return (True, "Update successful")
45
+
46
+ async def _status(self) -> StatusDict:
47
+ """Report strategy status."""
48
+ return {
49
+ "portfolio_value": 0.0,
50
+ "net_deposit": 0.0,
51
+ "strategy_status": {},
52
+ }
53
+
54
+ @staticmethod
55
+ def policies() -> list[str]:
56
+ """Return policy strings used to scope on-chain permissions."""
57
+ return []
@@ -0,0 +1,197 @@
1
+ """Test template for strategies.
2
+
3
+ REQUIRED: This template uses examples.json for all test data.
4
+ Replace 'MyStrategy' with your actual strategy class name.
5
+
6
+ Quick setup:
7
+ 1. Replace MyStrategy with your strategy class name
8
+ 2. Create examples.json with a 'smoke' example (see TESTING.md)
9
+ 3. Add mocking if your strategy uses adapters
10
+ 4. Run: pytest wayfinder_paths/strategies/your_strategy/ -v
11
+ """
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ # TODO: Replace MyStrategy with your actual strategy class name
17
+ from wayfinder_paths.strategies.your_strategy.strategy import (
18
+ MyStrategy, # noqa: E402
19
+ )
20
+
21
+ # Ensure wayfinder-paths is on path for tests.test_utils import
22
+ # This is a workaround until conftest loading order is resolved
23
+ _wayfinder_path_dir = Path(__file__).parent.parent.parent.parent.resolve()
24
+ _wayfinder_path_str = str(_wayfinder_path_dir)
25
+ if _wayfinder_path_str not in sys.path:
26
+ sys.path.insert(0, _wayfinder_path_str)
27
+ elif sys.path.index(_wayfinder_path_str) > 0:
28
+ # Move to front to take precedence
29
+ sys.path.remove(_wayfinder_path_str)
30
+ sys.path.insert(0, _wayfinder_path_str)
31
+
32
+ import pytest # noqa: E402
33
+
34
+ # Import test utilities
35
+ try:
36
+ from tests.test_utils import get_canonical_examples, load_strategy_examples
37
+ except ImportError:
38
+ # Fallback if path setup didn't work
39
+ import importlib.util
40
+
41
+ test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
42
+ spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
43
+ test_utils = importlib.util.module_from_spec(spec)
44
+ spec.loader.exec_module(test_utils)
45
+ get_canonical_examples = test_utils.get_canonical_examples
46
+ load_strategy_examples = test_utils.load_strategy_examples
47
+
48
+
49
+ @pytest.fixture
50
+ def strategy():
51
+ """Create a strategy instance for testing with minimal config."""
52
+ mock_config = {
53
+ "main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
54
+ "strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
55
+ }
56
+
57
+ s = MyStrategy(
58
+ config=mock_config,
59
+ main_wallet=mock_config["main_wallet"],
60
+ strategy_wallet=mock_config["strategy_wallet"],
61
+ simulation=True,
62
+ )
63
+
64
+ # TODO: Add mocking for your adapters here if needed
65
+ # Example for balance_adapter:
66
+ # if hasattr(s, "balance_adapter") and s.balance_adapter:
67
+ # usdc_balance_mock = AsyncMock(return_value=(True, 60000000))
68
+ # gas_balance_mock = AsyncMock(return_value=(True, 2000000000000000))
69
+ #
70
+ # def get_balance_side_effect(token_id, wallet_address, **kwargs):
71
+ # if token_id == "usd-coin-base" or token_id == "usd-coin":
72
+ # return usdc_balance_mock.return_value
73
+ # elif token_id == "ethereum-base" or token_id == "ethereum":
74
+ # return gas_balance_mock.return_value
75
+ # return (True, 1000000000)
76
+ #
77
+ # s.balance_adapter.get_token_balance_for_wallet = AsyncMock(
78
+ # side_effect=get_balance_side_effect
79
+ # )
80
+ # s.balance_adapter.get_all_enriched_token_balances_for_wallet = AsyncMock(
81
+ # return_value=(True, {"balances": []})
82
+ # )
83
+
84
+ # Example for token_adapter:
85
+ # if hasattr(s, "token_adapter") and s.token_adapter:
86
+ # default_token = {
87
+ # "id": "usd-coin-base",
88
+ # "symbol": "USDC",
89
+ # "name": "USD Coin",
90
+ # "decimals": 6,
91
+ # "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
92
+ # "chain": {"code": "base", "id": 8453, "name": "Base"},
93
+ # }
94
+ # s.token_adapter.get_token = AsyncMock(return_value=(True, default_token))
95
+ # s.token_adapter.get_gas_token = AsyncMock(return_value=(True, default_token))
96
+
97
+ # Example for transaction adapters:
98
+ # if hasattr(s, "tx_adapter") and s.tx_adapter:
99
+ # s.tx_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
100
+ # return_value=(True, "Transfer successful (simulated)")
101
+ # )
102
+ # s.tx_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
103
+ # return_value=(True, "Transfer successful (simulated)")
104
+ # )
105
+
106
+ # Example for ledger_adapter:
107
+ # if hasattr(s, "ledger_adapter") and s.ledger_adapter:
108
+ # s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
109
+ # return_value=(True, {"net_deposit": 0})
110
+ # )
111
+ # s.ledger_adapter.get_strategy_transactions = AsyncMock(
112
+ # return_value=(True, {"transactions": []})
113
+ # )
114
+
115
+ return s
116
+
117
+
118
+ @pytest.mark.asyncio
119
+ @pytest.mark.smoke
120
+ async def test_smoke(strategy):
121
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
122
+ examples = load_strategy_examples(Path(__file__))
123
+ smoke_data = examples["smoke"]
124
+
125
+ st = await strategy.status()
126
+ assert isinstance(st, dict)
127
+ assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
128
+
129
+ deposit_params = smoke_data.get("deposit", {})
130
+ ok, msg = await strategy.deposit(**deposit_params)
131
+ assert isinstance(ok, bool)
132
+ assert isinstance(msg, str)
133
+
134
+ ok, msg = await strategy.update(**smoke_data.get("update", {}))
135
+ assert isinstance(ok, bool)
136
+
137
+ ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
138
+ assert isinstance(ok, bool)
139
+
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_canonical_usage(strategy):
143
+ """REQUIRED: Test canonical usage examples from examples.json (minimum).
144
+
145
+ Canonical usage = all positive usage examples (excluding error cases).
146
+ This is the MINIMUM requirement - feel free to add more test cases here.
147
+ """
148
+ examples = load_strategy_examples(Path(__file__))
149
+ canonical = get_canonical_examples(examples)
150
+
151
+ for example_name, example_data in canonical.items():
152
+ if "deposit" in example_data:
153
+ deposit_params = example_data.get("deposit", {})
154
+ ok, _ = await strategy.deposit(**deposit_params)
155
+ assert ok, f"Canonical example '{example_name}' deposit failed"
156
+
157
+ if "update" in example_data:
158
+ ok, msg = await strategy.update()
159
+ assert ok, f"Canonical example '{example_name}' update failed: {msg}"
160
+
161
+ if "status" in example_data:
162
+ st = await strategy.status()
163
+ assert isinstance(st, dict), (
164
+ f"Canonical example '{example_name}' status failed"
165
+ )
166
+
167
+
168
+ @pytest.mark.asyncio
169
+ async def test_error_cases(strategy):
170
+ """OPTIONAL: Test error scenarios from examples.json."""
171
+ examples = load_strategy_examples(Path(__file__))
172
+
173
+ for example_name, example_data in examples.items():
174
+ if isinstance(example_data, dict) and "expect" in example_data:
175
+ expect = example_data.get("expect", {})
176
+
177
+ if "deposit" in example_data:
178
+ deposit_params = example_data.get("deposit", {})
179
+ ok, _ = await strategy.deposit(**deposit_params)
180
+
181
+ if expect.get("success") is False:
182
+ assert ok is False, (
183
+ f"Expected {example_name} deposit to fail but it succeeded"
184
+ )
185
+ elif expect.get("success") is True:
186
+ assert ok is True, (
187
+ f"Expected {example_name} deposit to succeed but it failed"
188
+ )
189
+
190
+ if "update" in example_data:
191
+ ok, _ = await strategy.update()
192
+ if "success" in expect:
193
+ expected_success = expect.get("success")
194
+ assert ok == expected_success, (
195
+ f"Expected {example_name} update to "
196
+ f"{'succeed' if expected_success else 'fail'} but got opposite"
197
+ )
File without changes
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
5
+
6
+
7
+ class FakeAdapter(BaseAdapter):
8
+ adapter_type = "FAKE"
9
+
10
+ async def connect(self) -> bool:
11
+ return True
12
+
13
+ async def get_balance(self, asset: str):
14
+ return {"asset": asset, "amount": 100}
15
+
16
+
17
+ class FakeStrategy(Strategy):
18
+ name = "Fake Strategy"
19
+
20
+ async def deposit(self, amount: float = 0) -> StatusTuple:
21
+ return (True, "deposited")
22
+
23
+ async def update(self) -> StatusTuple:
24
+ return (True, "updated")
25
+
26
+ async def withdraw(self, amount: float = 0) -> StatusTuple:
27
+ return (True, "withdrew")
28
+
29
+ async def status(self) -> StatusDict:
30
+ return {"total_earned": 0.0, "strategy_status": {"ok": True}}
31
+
32
+ @staticmethod
33
+ def policy() -> str:
34
+ return "wallet.id == 'TEST'"
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_smoke_deposit_update_withdraw_status():
39
+ s = FakeStrategy()
40
+ s.register_adapters([FakeAdapter("fake")])
41
+ ok, msg = await s.deposit(amount=1)
42
+ assert ok
43
+ ok, msg = await s.update()
44
+ assert ok
45
+ ok, msg = await s.withdraw(amount=1)
46
+ assert ok
47
+ st = await s.status()
48
+ assert "total_earned" in st