wayfinder-paths 0.1.15__py3-none-any.whl → 0.1.16__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/adapters/balance_adapter/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +66 -37
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +2 -8
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +33 -34
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +2 -18
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -56
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +1 -8
- wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +301 -662
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +275 -179
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -13
- wayfinder_paths/core/strategies/Strategy.py +6 -2
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -1
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/run_strategy.py +37 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +200 -224
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +128 -151
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +52 -78
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +0 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +39 -64
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +42 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +35 -44
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/sdk_example.py +0 -125
- wayfinder_paths/core/engine/__init__.py +0 -5
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +0 -131
- wayfinder_paths/core/services/local_evm_txn.py +0 -350
- wayfinder_paths/core/services/local_token_txn.py +0 -238
- wayfinder_paths/core/services/web3_service.py +0 -43
- wayfinder_paths/core/wallets/README.md +0 -88
- wayfinder_paths/core/wallets/WalletManager.py +0 -56
- wayfinder_paths/core/wallets/__init__.py +0 -7
- wayfinder_paths/scripts/run_strategy.py +0 -152
- wayfinder_paths/strategies/config.py +0 -85
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
|
@@ -7,43 +7,42 @@ Adapter that exposes wallet, token, and pool balances backed by `WalletClient`/`
|
|
|
7
7
|
|
|
8
8
|
## Capabilities
|
|
9
9
|
|
|
10
|
-
The adapter provides both wallet read and wallet transfer capabilities.
|
|
10
|
+
The adapter provides both wallet read and wallet transfer capabilities. Ledger recording + wallet selection now live inside the adapter.
|
|
11
11
|
|
|
12
12
|
## Construction
|
|
13
13
|
|
|
14
14
|
```python
|
|
15
|
-
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
16
15
|
from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
17
16
|
|
|
18
|
-
web3_service = DefaultWeb3Service(config)
|
|
19
|
-
balance = BalanceAdapter(config, web3_service=web3_service)
|
|
20
17
|
```
|
|
21
18
|
|
|
22
|
-
`web3_service` is required so the adapter can share the same wallet provider (and `TokenTxn` helper) as the rest of the strategy.
|
|
23
|
-
|
|
24
19
|
## API surface
|
|
25
20
|
|
|
26
|
-
### `get_balance(
|
|
27
|
-
Returns the raw balance (as an integer) for a specific token or pool on a wallet.
|
|
21
|
+
### `get_balance(token_id: str, wallet_address: str)`
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
Returns the raw balance (as an integer) for a specific token on a wallet.
|
|
30
24
|
|
|
31
25
|
```python
|
|
32
|
-
# Token balance (chain_id auto-resolved)
|
|
33
26
|
success, balance = await balance.get_balance(
|
|
34
|
-
|
|
27
|
+
token_id="usd-coin-base",
|
|
35
28
|
wallet_address=config["main_wallet"]["address"],
|
|
36
29
|
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `get_pool_balance(pool_address: str, chain_id: int, user_address: str)`
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
Fetches the amount supplied to a specific pool, using the `/wallets/pool-balance` endpoint.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
success, amount = await balance.get_pool_balance(
|
|
38
|
+
pool_address="0xPool",
|
|
42
39
|
chain_id=8453,
|
|
40
|
+
user_address=config["strategy_wallet"]["address"],
|
|
43
41
|
)
|
|
44
42
|
```
|
|
45
43
|
|
|
46
44
|
### `move_from_main_wallet_to_strategy_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
|
|
45
|
+
|
|
47
46
|
Sends the specified token from the configured `main_wallet` to the strategy wallet, records the ledger deposit (unless `skip_ledger=True`), and returns the `(success, tx_result)` tuple from the underlying send helper.
|
|
48
47
|
|
|
49
48
|
```python
|
|
@@ -55,6 +54,7 @@ success, tx = await balance.move_from_main_wallet_to_strategy_wallet(
|
|
|
55
54
|
```
|
|
56
55
|
|
|
57
56
|
### `move_from_strategy_wallet_to_main_wallet(token_id: str, amount: float, strategy_name="unknown", skip_ledger=False)`
|
|
57
|
+
|
|
58
58
|
Mirrors the previous method but withdraws from the strategy wallet back to the main wallet while recording a ledger withdrawal entry.
|
|
59
59
|
|
|
60
60
|
```python
|
|
@@ -73,16 +73,15 @@ All methods return `(success: bool, payload: Any)` tuples. On failure the payloa
|
|
|
73
73
|
class MyStrategy(Strategy):
|
|
74
74
|
def __init__(self, config):
|
|
75
75
|
super().__init__()
|
|
76
|
-
|
|
77
|
-
balance_adapter = BalanceAdapter(config, web3_service=web3_service)
|
|
76
|
+
balance_adapter = BalanceAdapter(config)
|
|
78
77
|
self.register_adapters([balance_adapter])
|
|
79
78
|
self.balance_adapter = balance_adapter
|
|
80
79
|
|
|
81
80
|
async def _status(self):
|
|
82
|
-
success, pool_balance = await self.balance_adapter.
|
|
83
|
-
|
|
84
|
-
wallet_address=self.config["strategy_wallet"]["address"],
|
|
81
|
+
success, pool_balance = await self.balance_adapter.get_pool_balance(
|
|
82
|
+
pool_address=self.current_pool["address"],
|
|
85
83
|
chain_id=self.current_pool["chain"]["id"],
|
|
84
|
+
user_address=self.config["strategy_wallet"]["address"],
|
|
86
85
|
)
|
|
87
86
|
return {"portfolio_value": float(pool_balance or 0), ...}
|
|
88
87
|
```
|
|
@@ -5,9 +5,9 @@ from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
|
|
|
5
5
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
6
6
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
7
7
|
from wayfinder_paths.core.clients.WalletClient import WalletClient
|
|
8
|
-
from wayfinder_paths.core.
|
|
9
|
-
from wayfinder_paths.core.services.base import Web3Service
|
|
8
|
+
from wayfinder_paths.core.utils.erc20_service import build_send_transaction
|
|
10
9
|
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
10
|
+
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class BalanceAdapter(BaseAdapter):
|
|
@@ -16,17 +16,19 @@ class BalanceAdapter(BaseAdapter):
|
|
|
16
16
|
def __init__(
|
|
17
17
|
self,
|
|
18
18
|
config: dict[str, Any],
|
|
19
|
-
|
|
19
|
+
simulation: bool = False,
|
|
20
|
+
main_wallet_signing_callback=None,
|
|
21
|
+
strategy_wallet_signing_callback=None,
|
|
20
22
|
):
|
|
21
23
|
super().__init__("balance", config)
|
|
24
|
+
self.simulation = simulation
|
|
25
|
+
self.main_wallet_signing_callback = main_wallet_signing_callback
|
|
26
|
+
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
22
27
|
self.wallet_client = WalletClient()
|
|
23
28
|
self.token_client = TokenClient()
|
|
24
29
|
self.token_adapter = TokenAdapter()
|
|
25
30
|
self.ledger_adapter = LedgerAdapter()
|
|
26
31
|
|
|
27
|
-
self.wallet_provider = web3_service.evm_transactions
|
|
28
|
-
self.token_transactions = web3_service.token_transactions
|
|
29
|
-
|
|
30
32
|
def _parse_balance(self, raw: Any) -> int:
|
|
31
33
|
"""Parse balance value to integer, handling various formats."""
|
|
32
34
|
if raw is None:
|
|
@@ -42,15 +44,23 @@ class BalanceAdapter(BaseAdapter):
|
|
|
42
44
|
async def get_balance(
|
|
43
45
|
self,
|
|
44
46
|
*,
|
|
45
|
-
query: str | dict[str, Any],
|
|
47
|
+
query: str | dict[str, Any] | None = None,
|
|
48
|
+
token_id: str | None = None,
|
|
46
49
|
wallet_address: str,
|
|
47
50
|
chain_id: int | None = None,
|
|
48
51
|
) -> tuple[bool, str | int]:
|
|
49
52
|
"""Get token or pool balance for a wallet.
|
|
50
53
|
|
|
51
54
|
query: token_id/address string or a dict with a "token_id" key.
|
|
55
|
+
token_id: alternative to query for convenience.
|
|
52
56
|
"""
|
|
53
|
-
|
|
57
|
+
# Support both query= and token_id= for caller convenience
|
|
58
|
+
effective_query = query if query is not None else token_id
|
|
59
|
+
resolved = (
|
|
60
|
+
effective_query
|
|
61
|
+
if isinstance(effective_query, str)
|
|
62
|
+
else (effective_query or {}).get("token_id")
|
|
63
|
+
)
|
|
54
64
|
if not resolved:
|
|
55
65
|
return (False, "missing query")
|
|
56
66
|
try:
|
|
@@ -119,15 +129,13 @@ class BalanceAdapter(BaseAdapter):
|
|
|
119
129
|
async def send_to_address(
|
|
120
130
|
self,
|
|
121
131
|
token_id: str,
|
|
122
|
-
amount:
|
|
132
|
+
amount: int,
|
|
123
133
|
from_wallet: dict[str, Any] | None,
|
|
124
134
|
to_address: str,
|
|
135
|
+
signing_callback=None,
|
|
125
136
|
skip_ledger: bool = True,
|
|
126
137
|
) -> tuple[bool, Any]:
|
|
127
138
|
"""Send tokens from a wallet to an arbitrary address (e.g., bridge contract)."""
|
|
128
|
-
if self.token_transactions is None:
|
|
129
|
-
return False, "Token transaction service not configured"
|
|
130
|
-
|
|
131
139
|
from_address = self._wallet_address(from_wallet)
|
|
132
140
|
if not from_address:
|
|
133
141
|
return False, "from_wallet missing or invalid"
|
|
@@ -139,22 +147,28 @@ class BalanceAdapter(BaseAdapter):
|
|
|
139
147
|
if not token_info:
|
|
140
148
|
return False, f"Token not found: {token_id}"
|
|
141
149
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
chain_id = resolve_chain_id(token_info, self.logger)
|
|
151
|
+
if chain_id is None:
|
|
152
|
+
return False, f"Token {token_id} is missing chain_id"
|
|
153
|
+
|
|
154
|
+
token_address = token_info.get("address")
|
|
155
|
+
|
|
156
|
+
tx = await build_send_transaction(
|
|
145
157
|
from_address=from_address,
|
|
146
158
|
to_address=to_address,
|
|
147
|
-
|
|
159
|
+
token_address=token_address,
|
|
160
|
+
chain_id=chain_id,
|
|
161
|
+
amount=int(amount),
|
|
148
162
|
)
|
|
149
|
-
if not build_success:
|
|
150
|
-
return False, tx_data
|
|
151
163
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
155
|
-
)
|
|
164
|
+
if self.simulation:
|
|
165
|
+
return True, {"simulation": tx}
|
|
156
166
|
|
|
157
|
-
|
|
167
|
+
if not signing_callback:
|
|
168
|
+
return False, "signing_callback is required"
|
|
169
|
+
|
|
170
|
+
tx_hash = await send_transaction(tx, signing_callback)
|
|
171
|
+
return True, tx_hash
|
|
158
172
|
|
|
159
173
|
async def _move_between_wallets(
|
|
160
174
|
self,
|
|
@@ -168,9 +182,6 @@ class BalanceAdapter(BaseAdapter):
|
|
|
168
182
|
strategy_name: str,
|
|
169
183
|
skip_ledger: bool,
|
|
170
184
|
) -> tuple[bool, Any]:
|
|
171
|
-
if self.token_transactions is None:
|
|
172
|
-
return False, "Token transaction service not configured"
|
|
173
|
-
|
|
174
185
|
from_address = self._wallet_address(from_wallet)
|
|
175
186
|
to_address = self._wallet_address(to_wallet)
|
|
176
187
|
if not from_address or not to_address:
|
|
@@ -180,20 +191,21 @@ class BalanceAdapter(BaseAdapter):
|
|
|
180
191
|
if not token_info:
|
|
181
192
|
return False, f"Token not found: {token_id}"
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
chain_id = resolve_chain_id(token_info, self.logger)
|
|
195
|
+
if chain_id is None:
|
|
196
|
+
return False, f"Token {token_id} is missing chain_id"
|
|
197
|
+
|
|
198
|
+
decimals = token_info.get("decimals", 18)
|
|
199
|
+
raw_amount = int(amount * (10**decimals))
|
|
200
|
+
|
|
201
|
+
tx = await build_send_transaction(
|
|
186
202
|
from_address=from_address,
|
|
187
203
|
to_address=to_address,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return False, tx_data
|
|
192
|
-
|
|
193
|
-
tx = tx_data
|
|
194
|
-
broadcast_result = await self.wallet_provider.broadcast_transaction(
|
|
195
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
204
|
+
token_address=token_info.get("address"),
|
|
205
|
+
chain_id=chain_id,
|
|
206
|
+
amount=raw_amount,
|
|
196
207
|
)
|
|
208
|
+
broadcast_result = await self._send_tx(tx, from_address)
|
|
197
209
|
|
|
198
210
|
if broadcast_result[0] and not skip_ledger and ledger_method is not None:
|
|
199
211
|
wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
|
|
@@ -207,6 +219,23 @@ class BalanceAdapter(BaseAdapter):
|
|
|
207
219
|
|
|
208
220
|
return broadcast_result
|
|
209
221
|
|
|
222
|
+
async def _send_tx(self, tx: dict[str, Any], from_address: str) -> tuple[bool, Any]:
|
|
223
|
+
"""Send transaction with simulation check, using appropriate signing callback."""
|
|
224
|
+
if self.simulation:
|
|
225
|
+
return True, {"simulation": tx}
|
|
226
|
+
|
|
227
|
+
# Choose callback based on which wallet is sending
|
|
228
|
+
main_wallet = self.config.get("main_wallet") or {}
|
|
229
|
+
main_addr = main_wallet.get("address", "").lower()
|
|
230
|
+
|
|
231
|
+
if from_address.lower() == main_addr:
|
|
232
|
+
callback = self.main_wallet_signing_callback
|
|
233
|
+
else:
|
|
234
|
+
callback = self.strategy_wallet_signing_callback
|
|
235
|
+
|
|
236
|
+
txn_hash = await send_transaction(tx, callback)
|
|
237
|
+
return True, txn_hash
|
|
238
|
+
|
|
210
239
|
async def _record_ledger_entry(
|
|
211
240
|
self,
|
|
212
241
|
*,
|
|
@@ -21,13 +21,7 @@ class TestBalanceAdapter:
|
|
|
21
21
|
return mock_client
|
|
22
22
|
|
|
23
23
|
@pytest.fixture
|
|
24
|
-
def
|
|
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):
|
|
24
|
+
def adapter(self, mock_wallet_client, mock_token_client):
|
|
31
25
|
"""Create a BalanceAdapter instance with mocked clients for testing"""
|
|
32
26
|
with (
|
|
33
27
|
patch(
|
|
@@ -39,7 +33,7 @@ class TestBalanceAdapter:
|
|
|
39
33
|
return_value=mock_token_client,
|
|
40
34
|
),
|
|
41
35
|
):
|
|
42
|
-
return BalanceAdapter(config={}
|
|
36
|
+
return BalanceAdapter(config={})
|
|
43
37
|
|
|
44
38
|
@pytest.mark.asyncio
|
|
45
39
|
async def test_health_check(self, adapter):
|
|
@@ -13,9 +13,8 @@ A Wayfinder adapter that provides high-level operations for cross-chain swaps an
|
|
|
13
13
|
|
|
14
14
|
## Configuration
|
|
15
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
16
|
The BRAPClient will automatically:
|
|
17
|
+
|
|
19
18
|
- Use the WAYFINDER_API_URL from settings
|
|
20
19
|
- Handle authentication via config.json
|
|
21
20
|
- Manage token refresh and retry logic
|
|
@@ -25,11 +24,9 @@ The BRAPClient will automatically:
|
|
|
25
24
|
### Initialize the Adapter
|
|
26
25
|
|
|
27
26
|
```python
|
|
28
|
-
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
29
27
|
from wayfinder_paths.adapters.brap_adapter.adapter import BRAPAdapter
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
adapter = BRAPAdapter(web3_service=web3_service)
|
|
29
|
+
adapter = BRAPAdapter()
|
|
33
30
|
```
|
|
34
31
|
|
|
35
32
|
### Get Swap Quote
|
|
@@ -46,9 +43,10 @@ success, data = await adapter.get_swap_quote(
|
|
|
46
43
|
slippage=0.01 # 1% slippage
|
|
47
44
|
)
|
|
48
45
|
if success:
|
|
49
|
-
|
|
46
|
+
quotes = data.get("quotes", {})
|
|
47
|
+
best_quote = quotes.get("best_quote", {})
|
|
50
48
|
print(f"Output amount: {best_quote.get('output_amount')}")
|
|
51
|
-
print(f"
|
|
49
|
+
print(f"Total fee: {best_quote.get('total_fee')}")
|
|
52
50
|
else:
|
|
53
51
|
print(f"Error: {data}")
|
|
54
52
|
```
|
|
@@ -67,7 +65,8 @@ success, data = await adapter.get_best_quote(
|
|
|
67
65
|
)
|
|
68
66
|
if success:
|
|
69
67
|
print(f"Best output: {data.get('output_amount')}")
|
|
70
|
-
print(f"
|
|
68
|
+
print(f"Gas fee: {data.get('gas_fee')}")
|
|
69
|
+
print(f"Bridge fee: {data.get('bridge_fee')}")
|
|
71
70
|
else:
|
|
72
71
|
print(f"Error: {data}")
|
|
73
72
|
```
|
|
@@ -86,8 +85,10 @@ success, data = await adapter.calculate_swap_fees(
|
|
|
86
85
|
if success:
|
|
87
86
|
print(f"Input amount: {data.get('input_amount')}")
|
|
88
87
|
print(f"Output amount: {data.get('output_amount')}")
|
|
89
|
-
print(f"Gas
|
|
90
|
-
print(f"
|
|
88
|
+
print(f"Gas fee: {data.get('gas_fee')}")
|
|
89
|
+
print(f"Bridge fee: {data.get('bridge_fee')}")
|
|
90
|
+
print(f"Protocol fee: {data.get('protocol_fee')}")
|
|
91
|
+
print(f"Total fee: {data.get('total_fee')}")
|
|
91
92
|
print(f"Price impact: {data.get('price_impact')}")
|
|
92
93
|
else:
|
|
93
94
|
print(f"Error: {data}")
|
|
@@ -106,9 +107,9 @@ success, data = await adapter.compare_routes(
|
|
|
106
107
|
if success:
|
|
107
108
|
print(f"Total routes available: {data.get('total_routes')}")
|
|
108
109
|
print(f"Best route output: {data.get('best_route', {}).get('output_amount')}")
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
for i, route in enumerate(data.get('all_routes', [])):
|
|
111
|
-
print(f
|
|
112
|
+
print(f"Route {i+1}: Output {route.get('output_amount')}, Fee {route.get('total_fee')}")
|
|
112
113
|
else:
|
|
113
114
|
print(f"Error: {data}")
|
|
114
115
|
```
|
|
@@ -166,7 +167,7 @@ success, data = await adapter.get_bridge_quote(
|
|
|
166
167
|
slippage=0.01
|
|
167
168
|
)
|
|
168
169
|
if success:
|
|
169
|
-
print(f"Bridge quote received: {data.get('best_quote', {}).get('output_amount')}")
|
|
170
|
+
print(f"Bridge quote received: {data.get('quotes', {}).get('best_quote', {}).get('output_amount')}")
|
|
170
171
|
else:
|
|
171
172
|
print(f"Error: {data}")
|
|
172
173
|
```
|
|
@@ -190,9 +191,10 @@ if success:
|
|
|
190
191
|
highest_output = analysis.get("highest_output")
|
|
191
192
|
lowest_fees = analysis.get("lowest_fees")
|
|
192
193
|
fastest = analysis.get("fastest")
|
|
193
|
-
|
|
194
|
+
|
|
194
195
|
print(f"Highest output route: {highest_output.get('output_amount')}")
|
|
195
|
-
print(f
|
|
196
|
+
print(f"Lowest fees route: {lowest_fees.get('total_fee')}")
|
|
197
|
+
print(f"Fastest route: {fastest.get('estimated_time')} seconds")
|
|
196
198
|
```
|
|
197
199
|
|
|
198
200
|
### Fee Analysis
|
|
@@ -210,23 +212,24 @@ success, data = await adapter.calculate_swap_fees(
|
|
|
210
212
|
if success:
|
|
211
213
|
input_amount = int(data.get("input_amount", 0))
|
|
212
214
|
output_amount = int(data.get("output_amount", 0))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
total_fee = int(data.get("total_fee", 0))
|
|
216
|
+
|
|
215
217
|
# Calculate effective rate
|
|
216
218
|
effective_rate = (input_amount - output_amount) / input_amount
|
|
217
219
|
print(f"Effective rate: {effective_rate:.4f} ({effective_rate * 100:.2f}%)")
|
|
218
|
-
print(f"Total fees:
|
|
220
|
+
print(f"Total fees: {total_fee / 1e18:.6f} tokens")
|
|
219
221
|
```
|
|
220
222
|
|
|
221
223
|
## API Endpoints
|
|
222
224
|
|
|
223
225
|
The adapter uses the following Wayfinder API endpoints:
|
|
224
226
|
|
|
225
|
-
- `
|
|
227
|
+
- `POST /api/v1/public/quotes/` - Get swap/bridge quotes
|
|
226
228
|
|
|
227
229
|
## Error Handling
|
|
228
230
|
|
|
229
231
|
All methods return a tuple of `(success: bool, data: Any)` where:
|
|
232
|
+
|
|
230
233
|
- `success` is `True` if the operation succeeded
|
|
231
234
|
- `data` contains the response data on success or error message on failure
|
|
232
235
|
|
|
@@ -15,8 +15,11 @@ from wayfinder_paths.core.clients.BRAPClient import (
|
|
|
15
15
|
from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
|
|
16
16
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
17
17
|
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
|
|
18
|
-
from wayfinder_paths.core.
|
|
19
|
-
|
|
18
|
+
from wayfinder_paths.core.utils.erc20_service import (
|
|
19
|
+
build_approve_transaction,
|
|
20
|
+
get_token_allowance,
|
|
21
|
+
)
|
|
22
|
+
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
20
23
|
|
|
21
24
|
_NEEDS_CLEAR_APPROVAL = {
|
|
22
25
|
(1, "0xdac17f958d2ee523a2206206994597c13d831ec7"),
|
|
@@ -41,17 +44,16 @@ class BRAPAdapter(BaseAdapter):
|
|
|
41
44
|
def __init__(
|
|
42
45
|
self,
|
|
43
46
|
config: dict[str, Any] | None = None,
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
simulation: bool = False,
|
|
48
|
+
strategy_wallet_signing_callback=None,
|
|
46
49
|
):
|
|
47
50
|
super().__init__("brap_adapter", config)
|
|
51
|
+
self.simulation = simulation
|
|
52
|
+
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
48
53
|
self.brap_client = BRAPClient()
|
|
49
54
|
self.token_client = TokenClient()
|
|
50
55
|
self.token_adapter = TokenAdapter()
|
|
51
56
|
self.ledger_adapter = LedgerAdapter()
|
|
52
|
-
self.web3_service = web3_service
|
|
53
|
-
self.wallet_provider = web3_service.evm_transactions
|
|
54
|
-
self.token_transactions = web3_service.token_transactions
|
|
55
57
|
|
|
56
58
|
async def get_swap_quote(
|
|
57
59
|
self,
|
|
@@ -438,6 +440,8 @@ class BRAPAdapter(BaseAdapter):
|
|
|
438
440
|
if not transaction or not transaction.get("data"):
|
|
439
441
|
return (False, "Quote missing calldata")
|
|
440
442
|
transaction["chainId"] = chain_id
|
|
443
|
+
if "value" in transaction:
|
|
444
|
+
transaction["value"] = int(transaction["value"])
|
|
441
445
|
# Always set the sender to the strategy wallet for broadcast.
|
|
442
446
|
# (Calldata may include either "from" or "from_address" depending on provider.)
|
|
443
447
|
transaction["from"] = to_checksum_address(from_address)
|
|
@@ -490,8 +494,10 @@ class BRAPAdapter(BaseAdapter):
|
|
|
490
494
|
if not approve_success:
|
|
491
495
|
return (False, approve_response)
|
|
492
496
|
|
|
493
|
-
broadcast_success, broadcast_response = await self.
|
|
494
|
-
|
|
497
|
+
broadcast_success, broadcast_response = await self._send_tx(transaction)
|
|
498
|
+
self.logger.info(
|
|
499
|
+
f"Swap broadcast result: success={broadcast_success}, "
|
|
500
|
+
f"response={broadcast_response}"
|
|
495
501
|
)
|
|
496
502
|
# Log only key fields to avoid spamming raw HexBytes logs
|
|
497
503
|
if isinstance(broadcast_response, dict):
|
|
@@ -729,46 +735,39 @@ class BRAPAdapter(BaseAdapter):
|
|
|
729
735
|
spender_checksum = to_checksum_address(spender_address)
|
|
730
736
|
|
|
731
737
|
if (chain_id, token_checksum.lower()) in _NEEDS_CLEAR_APPROVAL:
|
|
732
|
-
allowance = await
|
|
733
|
-
{"id": chain_id},
|
|
738
|
+
allowance = await get_token_allowance(
|
|
734
739
|
token_checksum,
|
|
740
|
+
chain_id,
|
|
735
741
|
owner_checksum,
|
|
736
742
|
spender_checksum,
|
|
737
743
|
)
|
|
738
|
-
if allowance
|
|
739
|
-
|
|
744
|
+
if allowance > 0:
|
|
745
|
+
clear_tx = await build_approve_transaction(
|
|
746
|
+
from_address=owner_checksum,
|
|
740
747
|
chain_id=chain_id,
|
|
741
748
|
token_address=token_checksum,
|
|
742
|
-
|
|
743
|
-
spender=spender_checksum,
|
|
749
|
+
spender_address=spender_checksum,
|
|
744
750
|
amount=0,
|
|
745
751
|
)
|
|
746
|
-
|
|
747
|
-
return False, clear_tx
|
|
748
|
-
clear_result = await self._broadcast_transaction(clear_tx)
|
|
752
|
+
clear_result = await self._send_tx(clear_tx)
|
|
749
753
|
if not clear_result[0]:
|
|
750
754
|
return clear_result
|
|
751
755
|
|
|
752
|
-
|
|
756
|
+
approve_tx = await build_approve_transaction(
|
|
757
|
+
from_address=owner_checksum,
|
|
753
758
|
chain_id=chain_id,
|
|
754
759
|
token_address=token_checksum,
|
|
755
|
-
|
|
756
|
-
spender=spender_checksum,
|
|
760
|
+
spender_address=spender_checksum,
|
|
757
761
|
amount=int(amount),
|
|
758
762
|
)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
transaction,
|
|
768
|
-
wait_for_receipt=True,
|
|
769
|
-
timeout=DEFAULT_TRANSACTION_TIMEOUT,
|
|
770
|
-
confirmations=confirmations,
|
|
771
|
-
)
|
|
763
|
+
return await self._send_tx(approve_tx)
|
|
764
|
+
|
|
765
|
+
async def _send_tx(self, tx: dict[str, Any]) -> tuple[bool, Any]:
|
|
766
|
+
"""Send transaction with simulation check."""
|
|
767
|
+
if self.simulation:
|
|
768
|
+
return True, {"simulation": tx}
|
|
769
|
+
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
770
|
+
return True, txn_hash
|
|
772
771
|
|
|
773
772
|
async def _record_swap_operation(
|
|
774
773
|
self,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from types import SimpleNamespace
|
|
2
1
|
from unittest.mock import AsyncMock
|
|
3
2
|
|
|
4
3
|
import pytest
|
|
@@ -16,24 +15,9 @@ class TestBRAPAdapter:
|
|
|
16
15
|
return mock_client
|
|
17
16
|
|
|
18
17
|
@pytest.fixture
|
|
19
|
-
def
|
|
20
|
-
"""Minimal Web3Service stub for adapter construction."""
|
|
21
|
-
wallet_provider = SimpleNamespace(
|
|
22
|
-
broadcast_transaction=AsyncMock(return_value=(True, {}))
|
|
23
|
-
)
|
|
24
|
-
token_txn = SimpleNamespace(
|
|
25
|
-
build_send=AsyncMock(return_value=(True, {})),
|
|
26
|
-
build_erc20_approve=AsyncMock(return_value=(True, {})),
|
|
27
|
-
read_erc20_allowance=AsyncMock(return_value={"allowance": 0}),
|
|
28
|
-
)
|
|
29
|
-
return SimpleNamespace(
|
|
30
|
-
evm_transactions=wallet_provider, token_transactions=token_txn
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
@pytest.fixture
|
|
34
|
-
def adapter(self, mock_brap_client, mock_web3_service):
|
|
18
|
+
def adapter(self, mock_brap_client):
|
|
35
19
|
"""Create a BRAPAdapter instance with mocked client for testing"""
|
|
36
|
-
adapter = BRAPAdapter(
|
|
20
|
+
adapter = BRAPAdapter()
|
|
37
21
|
adapter.brap_client = mock_brap_client
|
|
38
22
|
return adapter
|
|
39
23
|
|