wayfinder-paths 0.1.14__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.
- wayfinder_paths/adapters/balance_adapter/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -24
- wayfinder_paths/core/engine/StrategyJob.py +3 -1
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +22 -4
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -8
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +42 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/abis/generic/erc20.json +0 -383
- 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 -130
- wayfinder_paths/core/services/local_evm_txn.py +0 -334
- wayfinder_paths/core/services/local_token_txn.py +0 -242
- 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.14.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:
|
|
@@ -116,6 +126,50 @@ class BalanceAdapter(BaseAdapter):
|
|
|
116
126
|
skip_ledger=skip_ledger,
|
|
117
127
|
)
|
|
118
128
|
|
|
129
|
+
async def send_to_address(
|
|
130
|
+
self,
|
|
131
|
+
token_id: str,
|
|
132
|
+
amount: int,
|
|
133
|
+
from_wallet: dict[str, Any] | None,
|
|
134
|
+
to_address: str,
|
|
135
|
+
signing_callback=None,
|
|
136
|
+
skip_ledger: bool = True,
|
|
137
|
+
) -> tuple[bool, Any]:
|
|
138
|
+
"""Send tokens from a wallet to an arbitrary address (e.g., bridge contract)."""
|
|
139
|
+
from_address = self._wallet_address(from_wallet)
|
|
140
|
+
if not from_address:
|
|
141
|
+
return False, "from_wallet missing or invalid"
|
|
142
|
+
|
|
143
|
+
if not to_address:
|
|
144
|
+
return False, "to_address is required"
|
|
145
|
+
|
|
146
|
+
token_info = await self.token_client.get_token_details(token_id)
|
|
147
|
+
if not token_info:
|
|
148
|
+
return False, f"Token not found: {token_id}"
|
|
149
|
+
|
|
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(
|
|
157
|
+
from_address=from_address,
|
|
158
|
+
to_address=to_address,
|
|
159
|
+
token_address=token_address,
|
|
160
|
+
chain_id=chain_id,
|
|
161
|
+
amount=int(amount),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if self.simulation:
|
|
165
|
+
return True, {"simulation": tx}
|
|
166
|
+
|
|
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
|
|
172
|
+
|
|
119
173
|
async def _move_between_wallets(
|
|
120
174
|
self,
|
|
121
175
|
*,
|
|
@@ -128,9 +182,6 @@ class BalanceAdapter(BaseAdapter):
|
|
|
128
182
|
strategy_name: str,
|
|
129
183
|
skip_ledger: bool,
|
|
130
184
|
) -> tuple[bool, Any]:
|
|
131
|
-
if self.token_transactions is None:
|
|
132
|
-
return False, "Token transaction service not configured"
|
|
133
|
-
|
|
134
185
|
from_address = self._wallet_address(from_wallet)
|
|
135
186
|
to_address = self._wallet_address(to_wallet)
|
|
136
187
|
if not from_address or not to_address:
|
|
@@ -140,20 +191,21 @@ class BalanceAdapter(BaseAdapter):
|
|
|
140
191
|
if not token_info:
|
|
141
192
|
return False, f"Token not found: {token_id}"
|
|
142
193
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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(
|
|
146
202
|
from_address=from_address,
|
|
147
203
|
to_address=to_address,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return False, tx_data
|
|
152
|
-
|
|
153
|
-
tx = tx_data
|
|
154
|
-
broadcast_result = await self.wallet_provider.broadcast_transaction(
|
|
155
|
-
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,
|
|
156
207
|
)
|
|
208
|
+
broadcast_result = await self._send_tx(tx, from_address)
|
|
157
209
|
|
|
158
210
|
if broadcast_result[0] and not skip_ledger and ledger_method is not None:
|
|
159
211
|
wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
|
|
@@ -167,6 +219,23 @@ class BalanceAdapter(BaseAdapter):
|
|
|
167
219
|
|
|
168
220
|
return broadcast_result
|
|
169
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
|
+
|
|
170
239
|
async def _record_ledger_entry(
|
|
171
240
|
self,
|
|
172
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):
|
|
@@ -79,7 +73,7 @@ class TestBalanceAdapter:
|
|
|
79
73
|
wallet_address="0xWallet",
|
|
80
74
|
)
|
|
81
75
|
|
|
82
|
-
assert success
|
|
76
|
+
assert success
|
|
83
77
|
assert balance == 1000000
|
|
84
78
|
mock_token_client.get_token_details.assert_called_once_with("usd-coin-base")
|
|
85
79
|
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
@@ -108,7 +102,7 @@ class TestBalanceAdapter:
|
|
|
108
102
|
query={"token_id": "wsteth-base"},
|
|
109
103
|
wallet_address="0x123",
|
|
110
104
|
)
|
|
111
|
-
assert success
|
|
105
|
+
assert success
|
|
112
106
|
assert balance == 3000000
|
|
113
107
|
mock_token_client.get_token_details.assert_called_once_with("wsteth-base")
|
|
114
108
|
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
@@ -140,7 +134,7 @@ class TestBalanceAdapter:
|
|
|
140
134
|
chain_id=8453,
|
|
141
135
|
)
|
|
142
136
|
|
|
143
|
-
assert success
|
|
137
|
+
assert success
|
|
144
138
|
assert balance == 5000000
|
|
145
139
|
mock_wallet_client.get_token_balance_for_address.assert_called_once_with(
|
|
146
140
|
wallet_address="0xWallet",
|
|
@@ -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,11 +440,37 @@ 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)
|
|
444
448
|
|
|
445
|
-
|
|
449
|
+
def _as_address(value: Any) -> str | None:
|
|
450
|
+
if not isinstance(value, str):
|
|
451
|
+
return None
|
|
452
|
+
v = value.strip()
|
|
453
|
+
if (
|
|
454
|
+
v.startswith("0x")
|
|
455
|
+
and len(v) == 42
|
|
456
|
+
and v.lower() != ZERO_ADDRESS.lower()
|
|
457
|
+
):
|
|
458
|
+
return v
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
spender = (
|
|
462
|
+
_as_address(transaction.get("allowanceTarget"))
|
|
463
|
+
or _as_address(transaction.get("allowance_target"))
|
|
464
|
+
or _as_address(transaction.get("approvalAddress"))
|
|
465
|
+
or _as_address(transaction.get("approval_address"))
|
|
466
|
+
or _as_address(transaction.get("spender"))
|
|
467
|
+
or _as_address(quote.get("allowanceTarget"))
|
|
468
|
+
or _as_address(quote.get("allowance_target"))
|
|
469
|
+
or _as_address(quote.get("approvalAddress"))
|
|
470
|
+
or _as_address(quote.get("approval_address"))
|
|
471
|
+
or _as_address(quote.get("spender"))
|
|
472
|
+
or _as_address(transaction.get("to"))
|
|
473
|
+
)
|
|
446
474
|
approve_amount = (
|
|
447
475
|
quote.get("input_amount")
|
|
448
476
|
or quote.get("inputAmount")
|
|
@@ -466,16 +494,40 @@ class BRAPAdapter(BaseAdapter):
|
|
|
466
494
|
if not approve_success:
|
|
467
495
|
return (False, approve_response)
|
|
468
496
|
|
|
469
|
-
broadcast_success, broadcast_response = await self.
|
|
470
|
-
transaction
|
|
471
|
-
)
|
|
497
|
+
broadcast_success, broadcast_response = await self._send_tx(transaction)
|
|
472
498
|
self.logger.info(
|
|
473
499
|
f"Swap broadcast result: success={broadcast_success}, "
|
|
474
500
|
f"response={broadcast_response}"
|
|
475
501
|
)
|
|
502
|
+
# Log only key fields to avoid spamming raw HexBytes logs
|
|
503
|
+
if isinstance(broadcast_response, dict):
|
|
504
|
+
tx_hash_log = broadcast_response.get("tx_hash", "unknown")
|
|
505
|
+
block_log = broadcast_response.get("block_number", "unknown")
|
|
506
|
+
status_log = (
|
|
507
|
+
broadcast_response.get("receipt", {}).get("status", "unknown")
|
|
508
|
+
if isinstance(broadcast_response.get("receipt"), dict)
|
|
509
|
+
else "unknown"
|
|
510
|
+
)
|
|
511
|
+
self.logger.info(
|
|
512
|
+
f"Swap broadcast: success={broadcast_success}, tx={tx_hash_log}, block={block_log}, status={status_log}"
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
self.logger.info(f"Swap broadcast: success={broadcast_success}")
|
|
476
516
|
if not broadcast_success:
|
|
477
517
|
return (False, broadcast_response)
|
|
478
518
|
|
|
519
|
+
tx_hash = None
|
|
520
|
+
block_number = None
|
|
521
|
+
confirmations = None
|
|
522
|
+
confirmed_block_number = None
|
|
523
|
+
if isinstance(broadcast_response, dict):
|
|
524
|
+
tx_hash = broadcast_response.get("tx_hash") or broadcast_response.get(
|
|
525
|
+
"transaction_hash"
|
|
526
|
+
)
|
|
527
|
+
block_number = broadcast_response.get("block_number")
|
|
528
|
+
confirmations = broadcast_response.get("confirmations")
|
|
529
|
+
confirmed_block_number = broadcast_response.get("confirmed_block_number")
|
|
530
|
+
|
|
479
531
|
# Record the swap operation in ledger - but don't let ledger errors fail the swap
|
|
480
532
|
# since the on-chain transaction already succeeded
|
|
481
533
|
try:
|
|
@@ -491,15 +543,20 @@ class BRAPAdapter(BaseAdapter):
|
|
|
491
543
|
self.logger.warning(
|
|
492
544
|
f"Ledger recording failed (swap succeeded on-chain): {e}"
|
|
493
545
|
)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
546
|
+
ledger_record = {}
|
|
547
|
+
|
|
548
|
+
result_payload: dict[str, Any] = {
|
|
549
|
+
"from_amount": quote.get("input_amount"),
|
|
550
|
+
"to_amount": quote.get("output_amount"),
|
|
551
|
+
"tx_hash": tx_hash,
|
|
552
|
+
"block_number": block_number,
|
|
553
|
+
"confirmations": confirmations,
|
|
554
|
+
"confirmed_block_number": confirmed_block_number,
|
|
555
|
+
}
|
|
556
|
+
if isinstance(ledger_record, dict):
|
|
557
|
+
result_payload.update(ledger_record)
|
|
558
|
+
|
|
559
|
+
return (True, result_payload)
|
|
503
560
|
|
|
504
561
|
async def get_bridge_quote(
|
|
505
562
|
self,
|
|
@@ -678,46 +735,39 @@ class BRAPAdapter(BaseAdapter):
|
|
|
678
735
|
spender_checksum = to_checksum_address(spender_address)
|
|
679
736
|
|
|
680
737
|
if (chain_id, token_checksum.lower()) in _NEEDS_CLEAR_APPROVAL:
|
|
681
|
-
allowance = await
|
|
682
|
-
{"id": chain_id},
|
|
738
|
+
allowance = await get_token_allowance(
|
|
683
739
|
token_checksum,
|
|
740
|
+
chain_id,
|
|
684
741
|
owner_checksum,
|
|
685
742
|
spender_checksum,
|
|
686
743
|
)
|
|
687
|
-
if allowance
|
|
688
|
-
|
|
744
|
+
if allowance > 0:
|
|
745
|
+
clear_tx = await build_approve_transaction(
|
|
746
|
+
from_address=owner_checksum,
|
|
689
747
|
chain_id=chain_id,
|
|
690
748
|
token_address=token_checksum,
|
|
691
|
-
|
|
692
|
-
spender=spender_checksum,
|
|
749
|
+
spender_address=spender_checksum,
|
|
693
750
|
amount=0,
|
|
694
751
|
)
|
|
695
|
-
|
|
696
|
-
return False, clear_tx
|
|
697
|
-
clear_result = await self._broadcast_transaction(clear_tx)
|
|
752
|
+
clear_result = await self._send_tx(clear_tx)
|
|
698
753
|
if not clear_result[0]:
|
|
699
754
|
return clear_result
|
|
700
755
|
|
|
701
|
-
|
|
756
|
+
approve_tx = await build_approve_transaction(
|
|
757
|
+
from_address=owner_checksum,
|
|
702
758
|
chain_id=chain_id,
|
|
703
759
|
token_address=token_checksum,
|
|
704
|
-
|
|
705
|
-
spender=spender_checksum,
|
|
760
|
+
spender_address=spender_checksum,
|
|
706
761
|
amount=int(amount),
|
|
707
762
|
)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
return
|
|
716
|
-
transaction,
|
|
717
|
-
wait_for_receipt=True,
|
|
718
|
-
timeout=DEFAULT_TRANSACTION_TIMEOUT,
|
|
719
|
-
confirmations=confirmations,
|
|
720
|
-
)
|
|
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
|
|
721
771
|
|
|
722
772
|
async def _record_swap_operation(
|
|
723
773
|
self,
|