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,64 @@
|
|
|
1
|
+
"""Shared utilities for testing strategies and adapters."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_strategy_examples(strategy_test_file: Path) -> dict[str, Any]:
|
|
9
|
+
"""Load examples.json for a strategy test file.
|
|
10
|
+
|
|
11
|
+
This is REQUIRED for all strategy tests. The examples.json file serves
|
|
12
|
+
as both documentation and test data, ensuring tests stay in sync with examples.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
strategy_test_file: Path to the test_strategy.py file
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Dictionary containing examples from examples.json
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
FileNotFoundError: If examples.json does not exist
|
|
22
|
+
json.JSONDecodeError: If examples.json is invalid JSON
|
|
23
|
+
"""
|
|
24
|
+
examples_path = strategy_test_file.parent / "examples.json"
|
|
25
|
+
|
|
26
|
+
if not examples_path.exists():
|
|
27
|
+
raise FileNotFoundError(
|
|
28
|
+
f"examples.json is REQUIRED for strategy tests. "
|
|
29
|
+
f"Create it at: {examples_path}\n"
|
|
30
|
+
f"See TESTING.md for the required structure."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
with open(examples_path) as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_canonical_examples(examples: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
"""Extract canonical usage examples from examples.json.
|
|
39
|
+
|
|
40
|
+
Canonical usage is defined as the primary, documented usage patterns
|
|
41
|
+
that demonstrate how the strategy should be used. This includes:
|
|
42
|
+
- 'smoke' example: The basic lifecycle test (deposit → update → status → withdraw)
|
|
43
|
+
- Any examples without 'expect' fields (positive usage patterns)
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
examples: The full examples.json dictionary
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary of canonical examples keyed by their example name
|
|
50
|
+
"""
|
|
51
|
+
canonical = {}
|
|
52
|
+
|
|
53
|
+
# 'smoke' is always canonical
|
|
54
|
+
if "smoke" in examples:
|
|
55
|
+
canonical["smoke"] = examples["smoke"]
|
|
56
|
+
|
|
57
|
+
# Any example without 'expect' is considered canonical usage
|
|
58
|
+
for name, example_data in examples.items():
|
|
59
|
+
if name == "smoke":
|
|
60
|
+
continue # Already added
|
|
61
|
+
if isinstance(example_data, dict) and "expect" not in example_data:
|
|
62
|
+
canonical[name] = example_data
|
|
63
|
+
|
|
64
|
+
return canonical
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Balance Adapter
|
|
2
|
+
|
|
3
|
+
Adapter that exposes wallet, token, and pool balances backed by `WalletClient`/`TokenClient` and now orchestrates transfers between the configured main/vault wallets (with ledger bookkeeping).
|
|
4
|
+
|
|
5
|
+
- Entrypoint: `vaults.adapters.balance_adapter.adapter.BalanceAdapter`
|
|
6
|
+
- Manifest: `manifest.yaml`
|
|
7
|
+
- Tests: `test_adapter.py`
|
|
8
|
+
|
|
9
|
+
## Capabilities
|
|
10
|
+
|
|
11
|
+
The adapter declares both `wallet_read` and `wallet_transfer` capabilities in its manifest. Transfers are executed by leveraging the shared `DefaultWeb3Service.token_transactions` helper, but ledger recording + wallet selection now live inside the adapter.
|
|
12
|
+
|
|
13
|
+
## Construction
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
17
|
+
from wayfinder_paths.vaults.adapters.balance_adapter.adapter import BalanceAdapter
|
|
18
|
+
|
|
19
|
+
web3_service = DefaultWeb3Service(config)
|
|
20
|
+
balance = BalanceAdapter(config, web3_service=web3_service)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`web3_service` is required so the adapter can share the same wallet provider (and `TokenTxn` helper) as the rest of the strategy.
|
|
24
|
+
|
|
25
|
+
## API surface
|
|
26
|
+
|
|
27
|
+
### `get_balance(token_id: str, wallet_address: str)`
|
|
28
|
+
Returns the raw balance (as an integer) for a specific token on a wallet.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
success, balance = await balance.get_balance(
|
|
32
|
+
token_id="usd-coin-base",
|
|
33
|
+
wallet_address=config["main_wallet"]["address"],
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `get_pool_balance(pool_address: str, chain_id: int, user_address: str)`
|
|
38
|
+
Fetches the amount supplied to a specific pool, using the `/wallets/pool-balance` endpoint.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
success, amount = await balance.get_pool_balance(
|
|
42
|
+
pool_address="0xPool",
|
|
43
|
+
chain_id=8453,
|
|
44
|
+
user_address=config["vault_wallet"]["address"],
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `get_all_balances(wallet_address: str, enrich=True, from_cache=False, add_llama=True)`
|
|
49
|
+
Returns the enriched token balance payload Wayfinder exposes (including USD value, metadata, and optional DeFi Llama overlays).
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
success, snapshot = await balance.get_all_balances(
|
|
53
|
+
wallet_address=config["vault_wallet"]["address"],
|
|
54
|
+
enrich=True,
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `move_from_main_wallet_to_vault_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
|
|
59
|
+
Sends the specified token from the configured `main_wallet` to the `vault_wallet`, records the ledger deposit (unless `skip_ledger=True`), and returns the `(success, tx_result)` tuple from the underlying send helper.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
success, tx = await balance.move_from_main_wallet_to_vault_wallet(
|
|
63
|
+
token_id="usd-coin-base",
|
|
64
|
+
amount=1.5,
|
|
65
|
+
strategy_name="MyStrategy",
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `move_from_vault_wallet_to_main_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
|
|
70
|
+
Mirrors the previous method but withdraws from the vault wallet back to the main wallet while recording a ledger withdrawal entry.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
await balance.move_from_vault_wallet_to_main_wallet(
|
|
74
|
+
token_id="usd-coin-base",
|
|
75
|
+
amount=0.75,
|
|
76
|
+
strategy_name="MyStrategy",
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
All methods return `(success: bool, payload: Any)` tuples. On failure the payload is an error string.
|
|
81
|
+
|
|
82
|
+
## Usage inside strategies
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class MyStrategy(Strategy):
|
|
86
|
+
def __init__(self, config):
|
|
87
|
+
super().__init__()
|
|
88
|
+
web3_service = DefaultWeb3Service(config)
|
|
89
|
+
balance_adapter = BalanceAdapter(config, web3_service=web3_service)
|
|
90
|
+
self.register_adapters([balance_adapter])
|
|
91
|
+
self.balance_adapter = balance_adapter
|
|
92
|
+
|
|
93
|
+
async def _status(self):
|
|
94
|
+
success, pool_balance = await self.balance_adapter.get_pool_balance(
|
|
95
|
+
pool_address=self.current_pool["address"],
|
|
96
|
+
chain_id=self.current_pool["chain"]["id"],
|
|
97
|
+
user_address=self.config["vault_wallet"]["address"],
|
|
98
|
+
)
|
|
99
|
+
return {"portfolio_value": float(pool_balance or 0), ...}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Error handling and health checks
|
|
103
|
+
|
|
104
|
+
Any exception raised by the underlying `WalletClient`/`TokenClient` is caught and emitted as a `(False, "message")` tuple. The inherited `health_check()` method reports adapter status plus dependency status, making it safe to call from `Strategy.health_check`.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
4
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
5
|
+
from wayfinder_paths.core.clients.WalletClient import WalletClient
|
|
6
|
+
from wayfinder_paths.core.services.base import Web3Service
|
|
7
|
+
from wayfinder_paths.core.settings import settings
|
|
8
|
+
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
9
|
+
from wayfinder_paths.vaults.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
10
|
+
from wayfinder_paths.vaults.adapters.token_adapter.adapter import TokenAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BalanceAdapter(BaseAdapter):
|
|
14
|
+
adapter_type = "BALANCE"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
config: dict[str, Any],
|
|
19
|
+
web3_service: Web3Service,
|
|
20
|
+
):
|
|
21
|
+
super().__init__("balance", config)
|
|
22
|
+
self.wallet_client = WalletClient()
|
|
23
|
+
self.token_client = TokenClient()
|
|
24
|
+
self.token_adapter = TokenAdapter()
|
|
25
|
+
self.ledger_adapter = LedgerAdapter()
|
|
26
|
+
|
|
27
|
+
self.wallet_provider = web3_service.evm_transactions
|
|
28
|
+
self.token_transactions = web3_service.token_transactions
|
|
29
|
+
|
|
30
|
+
def _parse_balance(self, raw: Any) -> int:
|
|
31
|
+
"""Parse balance value to integer, handling various formats."""
|
|
32
|
+
if raw is None:
|
|
33
|
+
return 0
|
|
34
|
+
try:
|
|
35
|
+
return int(raw)
|
|
36
|
+
except (ValueError, TypeError):
|
|
37
|
+
try:
|
|
38
|
+
return int(float(raw))
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
async def get_balance(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
token_id: str,
|
|
46
|
+
wallet_address: str,
|
|
47
|
+
) -> tuple[bool, Any]:
|
|
48
|
+
"""Get token balance for a wallet."""
|
|
49
|
+
try:
|
|
50
|
+
data = await self.wallet_client.get_token_balance_for_wallet(
|
|
51
|
+
token_id=token_id,
|
|
52
|
+
wallet_address=wallet_address,
|
|
53
|
+
)
|
|
54
|
+
return (True, data.get("balance"))
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return (False, str(e))
|
|
57
|
+
|
|
58
|
+
async def move_from_main_wallet_to_vault_wallet(
|
|
59
|
+
self,
|
|
60
|
+
token_id: str,
|
|
61
|
+
amount: float,
|
|
62
|
+
strategy_name: str = "unknown",
|
|
63
|
+
skip_ledger: bool = False,
|
|
64
|
+
) -> tuple[bool, Any]:
|
|
65
|
+
"""Move funds from the configured main wallet into the vault wallet."""
|
|
66
|
+
return await self._move_between_wallets(
|
|
67
|
+
token_id=token_id,
|
|
68
|
+
amount=amount,
|
|
69
|
+
from_wallet=self.config.get("main_wallet"),
|
|
70
|
+
to_wallet=self.config.get("vault_wallet"),
|
|
71
|
+
ledger_method=self.ledger_adapter.record_deposit,
|
|
72
|
+
ledger_wallet="to",
|
|
73
|
+
strategy_name=strategy_name,
|
|
74
|
+
skip_ledger=skip_ledger,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def move_from_vault_wallet_to_main_wallet(
|
|
78
|
+
self,
|
|
79
|
+
token_id: str,
|
|
80
|
+
amount: float,
|
|
81
|
+
strategy_name: str = "unknown",
|
|
82
|
+
skip_ledger: bool = False,
|
|
83
|
+
) -> tuple[bool, Any]:
|
|
84
|
+
"""Move funds from the vault wallet back into the main wallet."""
|
|
85
|
+
return await self._move_between_wallets(
|
|
86
|
+
token_id=token_id,
|
|
87
|
+
amount=amount,
|
|
88
|
+
from_wallet=self.config.get("vault_wallet"),
|
|
89
|
+
to_wallet=self.config.get("main_wallet"),
|
|
90
|
+
ledger_method=self.ledger_adapter.record_withdrawal,
|
|
91
|
+
ledger_wallet="from",
|
|
92
|
+
strategy_name=strategy_name,
|
|
93
|
+
skip_ledger=skip_ledger,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def _move_between_wallets(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
token_id: str,
|
|
100
|
+
amount: float,
|
|
101
|
+
from_wallet: dict[str, Any] | None,
|
|
102
|
+
to_wallet: dict[str, Any] | None,
|
|
103
|
+
ledger_method,
|
|
104
|
+
ledger_wallet: str,
|
|
105
|
+
strategy_name: str,
|
|
106
|
+
skip_ledger: bool,
|
|
107
|
+
) -> tuple[bool, Any]:
|
|
108
|
+
if self.token_transactions is None:
|
|
109
|
+
return False, "Token transaction service not configured"
|
|
110
|
+
|
|
111
|
+
from_address = self._wallet_address(from_wallet)
|
|
112
|
+
to_address = self._wallet_address(to_wallet)
|
|
113
|
+
if not from_address or not to_address:
|
|
114
|
+
return False, "main_wallet or vault_wallet missing"
|
|
115
|
+
|
|
116
|
+
token_info = await self.token_client.get_token_details(token_id)
|
|
117
|
+
if not token_info:
|
|
118
|
+
return False, f"Token not found: {token_id}"
|
|
119
|
+
|
|
120
|
+
build_success, tx_data = await self.token_transactions.build_send(
|
|
121
|
+
token_id=token_id,
|
|
122
|
+
amount=amount,
|
|
123
|
+
from_address=from_address,
|
|
124
|
+
to_address=to_address,
|
|
125
|
+
token_info=token_info,
|
|
126
|
+
)
|
|
127
|
+
if not build_success:
|
|
128
|
+
return False, tx_data
|
|
129
|
+
|
|
130
|
+
tx = tx_data
|
|
131
|
+
if getattr(settings, "DRY_RUN", False):
|
|
132
|
+
broadcast_result = (True, {"dry_run": True, "transaction": tx})
|
|
133
|
+
else:
|
|
134
|
+
broadcast_result = await self.wallet_provider.broadcast_transaction(
|
|
135
|
+
tx, wait_for_receipt=True, timeout=120
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if broadcast_result[0] and not skip_ledger and ledger_method is not None:
|
|
139
|
+
wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
|
|
140
|
+
await self._record_ledger_entry(
|
|
141
|
+
ledger_method=ledger_method,
|
|
142
|
+
wallet_address=wallet_for_ledger,
|
|
143
|
+
token_info=token_info,
|
|
144
|
+
amount=amount,
|
|
145
|
+
strategy_name=strategy_name,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return broadcast_result
|
|
149
|
+
|
|
150
|
+
async def _record_ledger_entry(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
ledger_method,
|
|
154
|
+
wallet_address: str,
|
|
155
|
+
token_info: dict[str, Any],
|
|
156
|
+
amount: float,
|
|
157
|
+
strategy_name: str,
|
|
158
|
+
) -> None:
|
|
159
|
+
chain_id = resolve_chain_id(token_info, self.logger)
|
|
160
|
+
if chain_id is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
usd_value = await self._token_amount_usd(token_info, amount)
|
|
164
|
+
try:
|
|
165
|
+
success, response = await ledger_method(
|
|
166
|
+
wallet_address=wallet_address,
|
|
167
|
+
chain_id=chain_id,
|
|
168
|
+
token_address=token_info.get("address"),
|
|
169
|
+
token_amount=str(amount),
|
|
170
|
+
usd_value=usd_value,
|
|
171
|
+
data={
|
|
172
|
+
"token_id": token_info.get("id"),
|
|
173
|
+
"amount": str(amount),
|
|
174
|
+
"usd_value": usd_value,
|
|
175
|
+
},
|
|
176
|
+
strategy_name=strategy_name,
|
|
177
|
+
)
|
|
178
|
+
if not success:
|
|
179
|
+
self.logger.warning(
|
|
180
|
+
"Ledger entry failed",
|
|
181
|
+
wallet=wallet_address,
|
|
182
|
+
token_id=token_info.get("id"),
|
|
183
|
+
amount=amount,
|
|
184
|
+
error=response,
|
|
185
|
+
)
|
|
186
|
+
except Exception as exc: # noqa: BLE001
|
|
187
|
+
self.logger.warning(
|
|
188
|
+
f"Ledger entry raised: {exc}",
|
|
189
|
+
wallet=wallet_address,
|
|
190
|
+
token_id=token_info.get("id"),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def _token_amount_usd(
|
|
194
|
+
self, token_info: dict[str, Any], amount: float
|
|
195
|
+
) -> float:
|
|
196
|
+
token_id = token_info.get("id")
|
|
197
|
+
if not token_id:
|
|
198
|
+
return 0.0
|
|
199
|
+
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
200
|
+
if not success or not price_data:
|
|
201
|
+
return 0.0
|
|
202
|
+
return float(price_data.get("current_price", 0.0)) * float(amount)
|
|
203
|
+
|
|
204
|
+
def _wallet_address(self, wallet: dict[str, Any] | None) -> str | None:
|
|
205
|
+
if not wallet:
|
|
206
|
+
return None
|
|
207
|
+
address = wallet.get("address")
|
|
208
|
+
if address:
|
|
209
|
+
return str(address)
|
|
210
|
+
evm_wallet = wallet.get("evm") if isinstance(wallet, dict) else None
|
|
211
|
+
if isinstance(evm_wallet, dict):
|
|
212
|
+
return evm_wallet.get("address")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
async def get_pool_balance(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
pool_address: str,
|
|
219
|
+
chain_id: int,
|
|
220
|
+
user_address: str,
|
|
221
|
+
) -> tuple[bool, Any]:
|
|
222
|
+
"""Get pool balance for a wallet."""
|
|
223
|
+
try:
|
|
224
|
+
data = await self.wallet_client.get_pool_balance_for_wallet(
|
|
225
|
+
pool_address=pool_address,
|
|
226
|
+
chain_id=chain_id,
|
|
227
|
+
user_address=user_address,
|
|
228
|
+
human_readable=False,
|
|
229
|
+
)
|
|
230
|
+
raw = (
|
|
231
|
+
data.get("balance_raw") or data.get("balance")
|
|
232
|
+
if isinstance(data, dict)
|
|
233
|
+
else None
|
|
234
|
+
)
|
|
235
|
+
return (True, self._parse_balance(raw))
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return (False, str(e))
|
|
238
|
+
|
|
239
|
+
async def get_all_balances(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
wallet_address: str,
|
|
243
|
+
enrich: bool = True,
|
|
244
|
+
from_cache: bool = False,
|
|
245
|
+
add_llama: bool = True,
|
|
246
|
+
) -> tuple[bool, Any]:
|
|
247
|
+
"""Get all enriched token balances for a wallet."""
|
|
248
|
+
try:
|
|
249
|
+
data = await self.wallet_client.get_all_enriched_token_balances_for_wallet(
|
|
250
|
+
wallet_address=wallet_address,
|
|
251
|
+
enrich=enrich,
|
|
252
|
+
from_cache=from_cache,
|
|
253
|
+
add_llama=add_llama,
|
|
254
|
+
)
|
|
255
|
+
return (True, data)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
return (False, str(e))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.vaults.adapters.balance_adapter.adapter import BalanceAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestBalanceAdapter:
|
|
9
|
+
"""Test cases for BalanceAdapter"""
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_wallet_client(self):
|
|
13
|
+
"""Mock WalletClient for testing"""
|
|
14
|
+
mock_client = AsyncMock()
|
|
15
|
+
return mock_client
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_token_client(self):
|
|
19
|
+
"""Mock TokenClient for testing"""
|
|
20
|
+
mock_client = AsyncMock()
|
|
21
|
+
return mock_client
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def adapter(self, mock_wallet_client, mock_token_client):
|
|
25
|
+
"""Create a BalanceAdapter instance with mocked clients for testing"""
|
|
26
|
+
with (
|
|
27
|
+
patch(
|
|
28
|
+
"wayfinder_paths.vaults.adapters.balance_adapter.adapter.WalletClient",
|
|
29
|
+
return_value=mock_wallet_client,
|
|
30
|
+
),
|
|
31
|
+
patch(
|
|
32
|
+
"wayfinder_paths.vaults.adapters.balance_adapter.adapter.TokenClient",
|
|
33
|
+
return_value=mock_token_client,
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
return BalanceAdapter(config={})
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_health_check(self, adapter):
|
|
40
|
+
"""Test adapter health check"""
|
|
41
|
+
health = await adapter.health_check()
|
|
42
|
+
assert isinstance(health, dict)
|
|
43
|
+
assert health.get("status") in {"healthy", "unhealthy", "error"}
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_connect(self, adapter):
|
|
47
|
+
"""Test adapter connection"""
|
|
48
|
+
ok = await adapter.connect()
|
|
49
|
+
assert isinstance(ok, bool)
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_get_all_enriched_token_balances_for_wallet_success(
|
|
53
|
+
self, adapter, mock_wallet_client
|
|
54
|
+
):
|
|
55
|
+
"""Test successful retrieval of enriched token balances"""
|
|
56
|
+
mock_response = {
|
|
57
|
+
"balances": [
|
|
58
|
+
{
|
|
59
|
+
"token_id": "usd-coin-base",
|
|
60
|
+
"symbol": "USDC",
|
|
61
|
+
"balance": "1000000000",
|
|
62
|
+
"usd_value": 1000.0,
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"total_usd_value": 1000.0,
|
|
66
|
+
}
|
|
67
|
+
mock_wallet_client.get_all_enriched_token_balances_for_wallet = AsyncMock(
|
|
68
|
+
return_value=(True, mock_response)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
success, data = await adapter.get_all_enriched_token_balances_for_wallet(
|
|
72
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
73
|
+
enrich=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert success is True
|
|
77
|
+
assert isinstance(data, (dict, tuple))
|
|
78
|
+
if isinstance(data, dict):
|
|
79
|
+
assert "balances" in data
|
|
80
|
+
|
|
81
|
+
def test_adapter_type(self, adapter):
|
|
82
|
+
"""Test adapter has adapter_type"""
|
|
83
|
+
assert adapter.adapter_type == "BALANCE"
|