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.

Files changed (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. 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,6 @@
1
+ {
2
+ "smoke": {
3
+ "get_balance": {"asset": "base_0x0000000000000000000000000000000000000000", "wallet_type": "vault"}
4
+ }
5
+ }
6
+
@@ -0,0 +1,8 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "vaults.adapters.balance_adapter.adapter.BalanceAdapter"
3
+ capabilities:
4
+ - "wallet_read"
5
+ - "wallet_transfer"
6
+ dependencies:
7
+ - "TokenClient"
8
+ - "WalletClient"
@@ -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"