wayfinder-paths 0.1.1__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/CONFIG_GUIDE.md +394 -0
- wayfinder_paths/__init__.py +21 -0
- wayfinder_paths/config.example.json +20 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +13 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +90 -0
- wayfinder_paths/core/clients/ClientManager.py +231 -0
- wayfinder_paths/core/clients/HyperlendClient.py +151 -0
- wayfinder_paths/core/clients/LedgerClient.py +222 -0
- wayfinder_paths/core/clients/PoolClient.py +96 -0
- wayfinder_paths/core/clients/SimulationClient.py +180 -0
- wayfinder_paths/core/clients/TokenClient.py +73 -0
- wayfinder_paths/core/clients/TransactionClient.py +47 -0
- wayfinder_paths/core/clients/WalletClient.py +90 -0
- wayfinder_paths/core/clients/WayfinderClient.py +258 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +295 -0
- wayfinder_paths/core/clients/sdk_example.py +115 -0
- wayfinder_paths/core/config.py +369 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +25 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/VaultJob.py +182 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +177 -0
- wayfinder_paths/core/services/local_evm_txn.py +429 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +183 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +165 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/run_strategy.py +409 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +160 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths/vaults/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
- wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
- wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
- wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
- wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
- wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
- wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
- wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
- wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths/vaults/strategies/config.py +85 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
- wayfinder_paths/vaults/templates/adapter/README.md +105 -0
- wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
- wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
- wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/vaults/templates/strategy/README.md +152 -0
- wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
- wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
- wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
- wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
- wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Strategy Template
|
|
2
|
+
|
|
3
|
+
This template provides the scaffolding for a new vault strategy. It mirrors the structure in `wayfinder_paths/vaults/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/vaults/templates/strategy wayfinder_paths/vaults/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 `vaults.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 vault 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.vaults.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("vault_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: "vaults.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.vaults.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
|
+
poetry run python wayfinder_paths/scripts/make_wallets.py --default --vault
|
|
139
|
+
|
|
140
|
+
# Copy config and edit credentials
|
|
141
|
+
cp wayfinder_paths/config.example.json config.json
|
|
142
|
+
|
|
143
|
+
# Run your strategy
|
|
144
|
+
poetry run python wayfinder_paths/run_strategy.py my_strategy --action status --config $(pwd)/config.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Best practices
|
|
148
|
+
|
|
149
|
+
- Return `(success: bool, message: str)` tuples from `deposit`/`update`.
|
|
150
|
+
- Always populate `portfolio_value`, `net_deposit`, and `strategy_status` keys in `_status`.
|
|
151
|
+
- Use the adapters declared in `manifest.yaml`—they are injected via `register_adapters`.
|
|
152
|
+
- Keep on-chain permissions tight by scoping the manifest policy to the strategy wallet and expected contracts.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
schema_version: "0.1"
|
|
2
|
+
entrypoint: "vaults.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 vaults/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.vaults.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
|
+
"vault_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
s = MyStrategy(
|
|
58
|
+
config=mock_config,
|
|
59
|
+
main_wallet=mock_config["main_wallet"],
|
|
60
|
+
vault_wallet=mock_config["vault_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_vault_wallet = AsyncMock(
|
|
100
|
+
# return_value=(True, "Transfer successful (simulated)")
|
|
101
|
+
# )
|
|
102
|
+
# s.tx_adapter.move_from_vault_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_vault_net_deposit = AsyncMock(
|
|
109
|
+
# return_value=(True, {"net_deposit": 0})
|
|
110
|
+
# )
|
|
111
|
+
# s.ledger_adapter.get_vault_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
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Wayfinder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|