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.
- wayfinder_paths/CONFIG_GUIDE.md +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -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 +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -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 +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -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/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -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-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
4
|
+
from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
|
|
5
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
6
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
7
|
+
from wayfinder_paths.core.clients.WalletClient import WalletClient
|
|
8
|
+
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
9
|
+
from wayfinder_paths.core.services.base import Web3Service
|
|
10
|
+
from wayfinder_paths.core.settings import settings
|
|
11
|
+
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BalanceAdapter(BaseAdapter):
|
|
15
|
+
adapter_type = "BALANCE"
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
config: dict[str, Any],
|
|
20
|
+
web3_service: Web3Service,
|
|
21
|
+
):
|
|
22
|
+
super().__init__("balance", config)
|
|
23
|
+
self.wallet_client = WalletClient()
|
|
24
|
+
self.token_client = TokenClient()
|
|
25
|
+
self.token_adapter = TokenAdapter()
|
|
26
|
+
self.ledger_adapter = LedgerAdapter()
|
|
27
|
+
|
|
28
|
+
self.wallet_provider = web3_service.evm_transactions
|
|
29
|
+
self.token_transactions = web3_service.token_transactions
|
|
30
|
+
|
|
31
|
+
def _parse_balance(self, raw: Any) -> int:
|
|
32
|
+
"""Parse balance value to integer, handling various formats."""
|
|
33
|
+
if raw is None:
|
|
34
|
+
return 0
|
|
35
|
+
try:
|
|
36
|
+
return int(raw)
|
|
37
|
+
except (ValueError, TypeError):
|
|
38
|
+
try:
|
|
39
|
+
return int(float(raw))
|
|
40
|
+
except (ValueError, TypeError):
|
|
41
|
+
return 0
|
|
42
|
+
|
|
43
|
+
async def get_balance(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
token_id: str,
|
|
47
|
+
wallet_address: str,
|
|
48
|
+
) -> tuple[bool, str | int]:
|
|
49
|
+
"""Get token balance for a wallet."""
|
|
50
|
+
try:
|
|
51
|
+
data = await self.wallet_client.get_token_balance_for_wallet(
|
|
52
|
+
token_id=token_id,
|
|
53
|
+
wallet_address=wallet_address,
|
|
54
|
+
)
|
|
55
|
+
return (True, data.get("balance"))
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return (False, str(e))
|
|
58
|
+
|
|
59
|
+
async def move_from_main_wallet_to_strategy_wallet(
|
|
60
|
+
self,
|
|
61
|
+
token_id: str,
|
|
62
|
+
amount: float,
|
|
63
|
+
strategy_name: str = "unknown",
|
|
64
|
+
skip_ledger: bool = False,
|
|
65
|
+
) -> tuple[bool, Any]:
|
|
66
|
+
"""Move funds from the configured main wallet into the strategy wallet."""
|
|
67
|
+
return await self._move_between_wallets(
|
|
68
|
+
token_id=token_id,
|
|
69
|
+
amount=amount,
|
|
70
|
+
from_wallet=self.config.get("main_wallet"),
|
|
71
|
+
to_wallet=self.config.get("strategy_wallet"),
|
|
72
|
+
ledger_method=self.ledger_adapter.record_deposit,
|
|
73
|
+
ledger_wallet="to",
|
|
74
|
+
strategy_name=strategy_name,
|
|
75
|
+
skip_ledger=skip_ledger,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def move_from_strategy_wallet_to_main_wallet(
|
|
79
|
+
self,
|
|
80
|
+
token_id: str,
|
|
81
|
+
amount: float,
|
|
82
|
+
strategy_name: str = "unknown",
|
|
83
|
+
skip_ledger: bool = False,
|
|
84
|
+
) -> tuple[bool, Any]:
|
|
85
|
+
"""Move funds from the strategy wallet back into the main wallet."""
|
|
86
|
+
return await self._move_between_wallets(
|
|
87
|
+
token_id=token_id,
|
|
88
|
+
amount=amount,
|
|
89
|
+
from_wallet=self.config.get("strategy_wallet"),
|
|
90
|
+
to_wallet=self.config.get("main_wallet"),
|
|
91
|
+
ledger_method=self.ledger_adapter.record_withdrawal,
|
|
92
|
+
ledger_wallet="from",
|
|
93
|
+
strategy_name=strategy_name,
|
|
94
|
+
skip_ledger=skip_ledger,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def _move_between_wallets(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
token_id: str,
|
|
101
|
+
amount: float,
|
|
102
|
+
from_wallet: dict[str, Any] | None,
|
|
103
|
+
to_wallet: dict[str, Any] | None,
|
|
104
|
+
ledger_method,
|
|
105
|
+
ledger_wallet: str,
|
|
106
|
+
strategy_name: str,
|
|
107
|
+
skip_ledger: bool,
|
|
108
|
+
) -> tuple[bool, Any]:
|
|
109
|
+
if self.token_transactions is None:
|
|
110
|
+
return False, "Token transaction service not configured"
|
|
111
|
+
|
|
112
|
+
from_address = self._wallet_address(from_wallet)
|
|
113
|
+
to_address = self._wallet_address(to_wallet)
|
|
114
|
+
if not from_address or not to_address:
|
|
115
|
+
return False, "main_wallet or strategy_wallet missing"
|
|
116
|
+
|
|
117
|
+
token_info = await self.token_client.get_token_details(token_id)
|
|
118
|
+
if not token_info:
|
|
119
|
+
return False, f"Token not found: {token_id}"
|
|
120
|
+
|
|
121
|
+
build_success, tx_data = await self.token_transactions.build_send(
|
|
122
|
+
token_id=token_id,
|
|
123
|
+
amount=amount,
|
|
124
|
+
from_address=from_address,
|
|
125
|
+
to_address=to_address,
|
|
126
|
+
token_info=token_info,
|
|
127
|
+
)
|
|
128
|
+
if not build_success:
|
|
129
|
+
return False, tx_data
|
|
130
|
+
|
|
131
|
+
tx = tx_data
|
|
132
|
+
if getattr(settings, "DRY_RUN", False):
|
|
133
|
+
broadcast_result = (True, {"dry_run": True, "transaction": tx})
|
|
134
|
+
else:
|
|
135
|
+
broadcast_result = await self.wallet_provider.broadcast_transaction(
|
|
136
|
+
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if broadcast_result[0] and not skip_ledger and ledger_method is not None:
|
|
140
|
+
wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
|
|
141
|
+
await self._record_ledger_entry(
|
|
142
|
+
ledger_method=ledger_method,
|
|
143
|
+
wallet_address=wallet_for_ledger,
|
|
144
|
+
token_info=token_info,
|
|
145
|
+
amount=amount,
|
|
146
|
+
strategy_name=strategy_name,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return broadcast_result
|
|
150
|
+
|
|
151
|
+
async def _record_ledger_entry(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
ledger_method,
|
|
155
|
+
wallet_address: str,
|
|
156
|
+
token_info: dict[str, Any],
|
|
157
|
+
amount: float,
|
|
158
|
+
strategy_name: str,
|
|
159
|
+
) -> None:
|
|
160
|
+
chain_id = resolve_chain_id(token_info, self.logger)
|
|
161
|
+
if chain_id is None:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
usd_value = await self._token_amount_usd(token_info, amount)
|
|
165
|
+
try:
|
|
166
|
+
success, response = await ledger_method(
|
|
167
|
+
wallet_address=wallet_address,
|
|
168
|
+
chain_id=chain_id,
|
|
169
|
+
token_address=token_info.get("address"),
|
|
170
|
+
token_amount=str(amount),
|
|
171
|
+
usd_value=usd_value,
|
|
172
|
+
data={
|
|
173
|
+
"token_id": token_info.get("id"),
|
|
174
|
+
"amount": str(amount),
|
|
175
|
+
"usd_value": usd_value,
|
|
176
|
+
},
|
|
177
|
+
strategy_name=strategy_name,
|
|
178
|
+
)
|
|
179
|
+
if not success:
|
|
180
|
+
self.logger.warning(
|
|
181
|
+
"Ledger entry failed",
|
|
182
|
+
wallet=wallet_address,
|
|
183
|
+
token_id=token_info.get("id"),
|
|
184
|
+
amount=amount,
|
|
185
|
+
error=response,
|
|
186
|
+
)
|
|
187
|
+
except Exception as exc: # noqa: BLE001
|
|
188
|
+
self.logger.warning(
|
|
189
|
+
f"Ledger entry raised: {exc}",
|
|
190
|
+
wallet=wallet_address,
|
|
191
|
+
token_id=token_info.get("id"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def _token_amount_usd(
|
|
195
|
+
self, token_info: dict[str, Any], amount: float
|
|
196
|
+
) -> float:
|
|
197
|
+
token_id = token_info.get("id")
|
|
198
|
+
if not token_id:
|
|
199
|
+
return 0.0
|
|
200
|
+
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
201
|
+
if not success or not price_data:
|
|
202
|
+
return 0.0
|
|
203
|
+
return float(price_data.get("current_price", 0.0)) * float(amount)
|
|
204
|
+
|
|
205
|
+
def _wallet_address(self, wallet: dict[str, Any] | None) -> str | None:
|
|
206
|
+
if not wallet:
|
|
207
|
+
return None
|
|
208
|
+
address = wallet.get("address")
|
|
209
|
+
if address:
|
|
210
|
+
return str(address)
|
|
211
|
+
evm_wallet = wallet.get("evm") if isinstance(wallet, dict) else None
|
|
212
|
+
if isinstance(evm_wallet, dict):
|
|
213
|
+
return evm_wallet.get("address")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
async def get_pool_balance(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
pool_address: str,
|
|
220
|
+
chain_id: int,
|
|
221
|
+
user_address: str,
|
|
222
|
+
) -> tuple[bool, Any]:
|
|
223
|
+
"""Get pool balance for a wallet."""
|
|
224
|
+
try:
|
|
225
|
+
data = await self.wallet_client.get_pool_balance_for_wallet(
|
|
226
|
+
pool_address=pool_address,
|
|
227
|
+
chain_id=chain_id,
|
|
228
|
+
user_address=user_address,
|
|
229
|
+
human_readable=False,
|
|
230
|
+
)
|
|
231
|
+
raw = (
|
|
232
|
+
data.get("balance_raw") or data.get("balance")
|
|
233
|
+
if isinstance(data, dict)
|
|
234
|
+
else None
|
|
235
|
+
)
|
|
236
|
+
return (True, self._parse_balance(raw))
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return (False, str(e))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.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 mock_web3_service(self):
|
|
25
|
+
"""Mock TokenClient for testing"""
|
|
26
|
+
mock_client = AsyncMock()
|
|
27
|
+
return mock_client
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def adapter(self, mock_wallet_client, mock_token_client, mock_web3_service):
|
|
31
|
+
"""Create a BalanceAdapter instance with mocked clients for testing"""
|
|
32
|
+
with (
|
|
33
|
+
patch(
|
|
34
|
+
"wayfinder_paths.adapters.balance_adapter.adapter.WalletClient",
|
|
35
|
+
return_value=mock_wallet_client,
|
|
36
|
+
),
|
|
37
|
+
patch(
|
|
38
|
+
"wayfinder_paths.adapters.balance_adapter.adapter.TokenClient",
|
|
39
|
+
return_value=mock_token_client,
|
|
40
|
+
),
|
|
41
|
+
):
|
|
42
|
+
return BalanceAdapter(config={}, web3_service=mock_web3_service)
|
|
43
|
+
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_health_check(self, adapter):
|
|
46
|
+
"""Test adapter health check"""
|
|
47
|
+
health = await adapter.health_check()
|
|
48
|
+
assert isinstance(health, dict)
|
|
49
|
+
assert health.get("status") in {"healthy", "unhealthy", "error"}
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_connect(self, adapter):
|
|
53
|
+
"""Test adapter connection"""
|
|
54
|
+
ok = await adapter.connect()
|
|
55
|
+
assert isinstance(ok, bool)
|
|
56
|
+
|
|
57
|
+
def test_adapter_type(self, adapter):
|
|
58
|
+
"""Test adapter has adapter_type"""
|
|
59
|
+
assert adapter.adapter_type == "BALANCE"
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# BRAP Adapter
|
|
2
|
+
|
|
3
|
+
A Wayfinder adapter that provides high-level operations for cross-chain swaps and quotes via the BRAP (Bridge/Router/Adapter Protocol). This adapter wraps the `BRAPClient` to offer strategy-friendly methods for getting quotes, comparing routes, and executing cross-chain transactions.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
|
|
7
|
+
- `swap.quote`: Get quotes for cross-chain swap operations
|
|
8
|
+
- `swap.execute`: Execute cross-chain swaps
|
|
9
|
+
- `bridge.quote`: Get quotes for bridge operations
|
|
10
|
+
- `bridge.execute`: Execute bridge operations
|
|
11
|
+
- `route.optimize`: Compare and optimize routes
|
|
12
|
+
- `fee.calculate`: Calculate fees and costs
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
The adapter uses the BRAPClient which automatically handles authentication and API configuration through the Wayfinder settings. Pass a `Web3Service` instance so it can broadcast transactions and reuse the shared `LocalTokenTxnService` for approvals.
|
|
17
|
+
|
|
18
|
+
The BRAPClient will automatically:
|
|
19
|
+
- Use the WAYFINDER_API_URL from settings
|
|
20
|
+
- Handle authentication via environment variables or config.json
|
|
21
|
+
- Manage token refresh and retry logic
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Initialize the Adapter
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
29
|
+
from wayfinder_paths.adapters.brap_adapter.adapter import BRAPAdapter
|
|
30
|
+
|
|
31
|
+
web3_service = DefaultWeb3Service(config)
|
|
32
|
+
adapter = BRAPAdapter(web3_service=web3_service)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Get Swap Quote
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
success, data = await adapter.get_swap_quote(
|
|
39
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
40
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
41
|
+
from_chain_id=8453, # Base
|
|
42
|
+
to_chain_id=1, # Ethereum
|
|
43
|
+
from_address="0x1234567890123456789012345678901234567890",
|
|
44
|
+
to_address="0x1234567890123456789012345678901234567890",
|
|
45
|
+
amount="1000000000000000000", # 1 token (18 decimals)
|
|
46
|
+
slippage=0.01 # 1% slippage
|
|
47
|
+
)
|
|
48
|
+
if success:
|
|
49
|
+
quotes = data.get("quotes", {})
|
|
50
|
+
best_quote = quotes.get("best_quote", {})
|
|
51
|
+
print(f"Output amount: {best_quote.get('output_amount')}")
|
|
52
|
+
print(f"Total fee: {best_quote.get('total_fee')}")
|
|
53
|
+
else:
|
|
54
|
+
print(f"Error: {data}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Get Best Quote
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
success, data = await adapter.get_best_quote(
|
|
61
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
62
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
63
|
+
from_chain_id=8453,
|
|
64
|
+
to_chain_id=1,
|
|
65
|
+
from_address="0x1234567890123456789012345678901234567890",
|
|
66
|
+
to_address="0x1234567890123456789012345678901234567890",
|
|
67
|
+
amount="1000000000000000000"
|
|
68
|
+
)
|
|
69
|
+
if success:
|
|
70
|
+
print(f"Best output: {data.get('output_amount')}")
|
|
71
|
+
print(f"Gas fee: {data.get('gas_fee')}")
|
|
72
|
+
print(f"Bridge fee: {data.get('bridge_fee')}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Error: {data}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Calculate Swap Fees
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
success, data = await adapter.calculate_swap_fees(
|
|
81
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
82
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
83
|
+
from_chain_id=8453,
|
|
84
|
+
to_chain_id=1,
|
|
85
|
+
amount="1000000000000000000",
|
|
86
|
+
slippage=0.01
|
|
87
|
+
)
|
|
88
|
+
if success:
|
|
89
|
+
print(f"Input amount: {data.get('input_amount')}")
|
|
90
|
+
print(f"Output amount: {data.get('output_amount')}")
|
|
91
|
+
print(f"Gas fee: {data.get('gas_fee')}")
|
|
92
|
+
print(f"Bridge fee: {data.get('bridge_fee')}")
|
|
93
|
+
print(f"Protocol fee: {data.get('protocol_fee')}")
|
|
94
|
+
print(f"Total fee: {data.get('total_fee')}")
|
|
95
|
+
print(f"Price impact: {data.get('price_impact')}")
|
|
96
|
+
else:
|
|
97
|
+
print(f"Error: {data}")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Compare Routes
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
success, data = await adapter.compare_routes(
|
|
104
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
105
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
106
|
+
from_chain_id=8453,
|
|
107
|
+
to_chain_id=1,
|
|
108
|
+
amount="1000000000000000000"
|
|
109
|
+
)
|
|
110
|
+
if success:
|
|
111
|
+
print(f"Total routes available: {data.get('total_routes')}")
|
|
112
|
+
print(f"Best route output: {data.get('best_route', {}).get('output_amount')}")
|
|
113
|
+
|
|
114
|
+
for i, route in enumerate(data.get('all_routes', [])):
|
|
115
|
+
print(f"Route {i+1}: Output {route.get('output_amount')}, Fee {route.get('total_fee')}")
|
|
116
|
+
else:
|
|
117
|
+
print(f"Error: {data}")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Estimate Gas Costs
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
success, data = await adapter.estimate_gas_cost(
|
|
124
|
+
from_chain_id=8453, # Base
|
|
125
|
+
to_chain_id=1, # Ethereum
|
|
126
|
+
operation_type="swap"
|
|
127
|
+
)
|
|
128
|
+
if success:
|
|
129
|
+
print(f"From chain: {data.get('from_chain')}")
|
|
130
|
+
print(f"To chain: {data.get('to_chain')}")
|
|
131
|
+
print(f"From gas estimate: {data.get('from_gas_estimate')}")
|
|
132
|
+
print(f"To gas estimate: {data.get('to_gas_estimate')}")
|
|
133
|
+
print(f"Total operations: {data.get('total_operations')}")
|
|
134
|
+
else:
|
|
135
|
+
print(f"Error: {data}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Validate Swap Parameters
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
success, data = await adapter.validate_swap_parameters(
|
|
142
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
143
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
144
|
+
from_chain_id=8453,
|
|
145
|
+
to_chain_id=1,
|
|
146
|
+
amount="1000000000000000000"
|
|
147
|
+
)
|
|
148
|
+
if success:
|
|
149
|
+
if data.get("valid"):
|
|
150
|
+
print("Parameters are valid")
|
|
151
|
+
print(f"Estimated output: {data.get('estimated_output')}")
|
|
152
|
+
else:
|
|
153
|
+
print("Parameters are invalid:")
|
|
154
|
+
for error in data.get("errors", []):
|
|
155
|
+
print(f" - {error}")
|
|
156
|
+
else:
|
|
157
|
+
print(f"Error: {data}")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Get Bridge Quote
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Bridge operations use the same interface as swaps
|
|
164
|
+
success, data = await adapter.get_bridge_quote(
|
|
165
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
166
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
167
|
+
from_chain_id=8453,
|
|
168
|
+
to_chain_id=1,
|
|
169
|
+
amount="1000000000000000000",
|
|
170
|
+
slippage=0.01
|
|
171
|
+
)
|
|
172
|
+
if success:
|
|
173
|
+
print(f"Bridge quote received: {data.get('quotes', {}).get('best_quote', {}).get('output_amount')}")
|
|
174
|
+
else:
|
|
175
|
+
print(f"Error: {data}")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Advanced Usage
|
|
179
|
+
|
|
180
|
+
### Route Optimization
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
# Compare multiple routes to find the best option
|
|
184
|
+
success, data = await adapter.compare_routes(
|
|
185
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
186
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
187
|
+
from_chain_id=8453,
|
|
188
|
+
to_chain_id=1,
|
|
189
|
+
amount="1000000000000000000"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if success:
|
|
193
|
+
analysis = data.get("route_analysis", {})
|
|
194
|
+
highest_output = analysis.get("highest_output")
|
|
195
|
+
lowest_fees = analysis.get("lowest_fees")
|
|
196
|
+
fastest = analysis.get("fastest")
|
|
197
|
+
|
|
198
|
+
print(f"Highest output route: {highest_output.get('output_amount')}")
|
|
199
|
+
print(f"Lowest fees route: {lowest_fees.get('total_fee')}")
|
|
200
|
+
print(f"Fastest route: {fastest.get('estimated_time')} seconds")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Fee Analysis
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Analyze fees for a swap operation
|
|
207
|
+
success, data = await adapter.calculate_swap_fees(
|
|
208
|
+
from_token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
209
|
+
to_token_address="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
210
|
+
from_chain_id=8453,
|
|
211
|
+
to_chain_id=1,
|
|
212
|
+
amount="1000000000000000000"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if success:
|
|
216
|
+
input_amount = int(data.get("input_amount", 0))
|
|
217
|
+
output_amount = int(data.get("output_amount", 0))
|
|
218
|
+
total_fee = int(data.get("total_fee", 0))
|
|
219
|
+
|
|
220
|
+
# Calculate effective rate
|
|
221
|
+
effective_rate = (input_amount - output_amount) / input_amount
|
|
222
|
+
print(f"Effective rate: {effective_rate:.4f} ({effective_rate * 100:.2f}%)")
|
|
223
|
+
print(f"Total fees: {total_fee / 1e18:.6f} tokens")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## API Endpoints
|
|
227
|
+
|
|
228
|
+
The adapter uses the following Wayfinder API endpoints:
|
|
229
|
+
|
|
230
|
+
- `POST /api/v1/public/quotes/` - Get swap/bridge quotes
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
All methods return a tuple of `(success: bool, data: Any)` where:
|
|
235
|
+
- `success` is `True` if the operation succeeded
|
|
236
|
+
- `data` contains the response data on success or error message on failure
|
|
237
|
+
|
|
238
|
+
## Testing
|
|
239
|
+
|
|
240
|
+
Run the adapter tests:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
pytest wayfinder_paths/adapters/brap_adapter/test_adapter.py -v
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Dependencies
|
|
247
|
+
|
|
248
|
+
- `BRAPClient` - Low-level API client for BRAP operations
|
|
249
|
+
- `BaseAdapter` - Base adapter class with common functionality
|