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,205 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestLedgerAdapter:
|
|
9
|
+
"""Test cases for LedgerAdapter"""
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_ledger_client(self):
|
|
13
|
+
"""Mock LedgerClient for testing"""
|
|
14
|
+
mock_client = AsyncMock()
|
|
15
|
+
return mock_client
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def adapter(self, mock_ledger_client):
|
|
19
|
+
"""Create a LedgerAdapter instance with mocked client for testing"""
|
|
20
|
+
adapter = LedgerAdapter()
|
|
21
|
+
adapter.ledger_client = mock_ledger_client
|
|
22
|
+
return adapter
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_get_strategy_transactions_success(self, adapter, mock_ledger_client):
|
|
26
|
+
"""Test successful strategy transaction retrieval"""
|
|
27
|
+
mock_response = {
|
|
28
|
+
"transactions": [
|
|
29
|
+
{
|
|
30
|
+
"id": "tx_123",
|
|
31
|
+
"operation": "DEPOSIT",
|
|
32
|
+
"amount": "1000000000000000000",
|
|
33
|
+
"usd_value": "1000.00",
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"total": 1,
|
|
37
|
+
}
|
|
38
|
+
mock_ledger_client.get_strategy_transactions = AsyncMock(
|
|
39
|
+
return_value=mock_response
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
success, data = await adapter.get_strategy_transactions(
|
|
43
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
44
|
+
limit=10,
|
|
45
|
+
offset=0,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert success is True
|
|
49
|
+
assert data == mock_response
|
|
50
|
+
mock_ledger_client.get_strategy_transactions.assert_called_once_with(
|
|
51
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
52
|
+
limit=10,
|
|
53
|
+
offset=0,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_get_strategy_transactions_failure(self, adapter, mock_ledger_client):
|
|
58
|
+
"""Test strategy transaction retrieval failure"""
|
|
59
|
+
mock_ledger_client.get_strategy_transactions = AsyncMock(
|
|
60
|
+
side_effect=Exception("API Error")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
success, data = await adapter.get_strategy_transactions(
|
|
64
|
+
wallet_address="0x1234567890123456789012345678901234567890"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert success is False
|
|
68
|
+
assert "API Error" in data
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_get_strategy_net_deposit_success(self, adapter, mock_ledger_client):
|
|
72
|
+
"""Test successful strategy net deposit retrieval"""
|
|
73
|
+
mock_response = {
|
|
74
|
+
"net_deposit": "1000.00",
|
|
75
|
+
"total_deposits": "1500.00",
|
|
76
|
+
"total_withdrawals": "500.00",
|
|
77
|
+
}
|
|
78
|
+
mock_ledger_client.get_strategy_net_deposit = AsyncMock(
|
|
79
|
+
return_value=mock_response
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Test
|
|
83
|
+
success, data = await adapter.get_strategy_net_deposit(
|
|
84
|
+
wallet_address="0x1234567890123456789012345678901234567890"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert success is True
|
|
88
|
+
assert data == mock_response
|
|
89
|
+
mock_ledger_client.get_strategy_net_deposit.assert_called_once_with(
|
|
90
|
+
wallet_address="0x1234567890123456789012345678901234567890"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_record_deposit_success(self, adapter, mock_ledger_client):
|
|
95
|
+
"""Test successful deposit recording"""
|
|
96
|
+
mock_response = {
|
|
97
|
+
"transaction_id": "tx_456",
|
|
98
|
+
"status": "recorded",
|
|
99
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
100
|
+
}
|
|
101
|
+
mock_ledger_client.add_strategy_deposit.return_value = mock_response
|
|
102
|
+
|
|
103
|
+
# Test
|
|
104
|
+
success, data = await adapter.record_deposit(
|
|
105
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
106
|
+
chain_id=8453,
|
|
107
|
+
token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
108
|
+
token_amount="1000000000000000000",
|
|
109
|
+
usd_value="1000.00",
|
|
110
|
+
strategy_name="TestStrategy",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
assert success is True
|
|
114
|
+
assert data == mock_response
|
|
115
|
+
mock_ledger_client.add_strategy_deposit.assert_called_once_with(
|
|
116
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
117
|
+
chain_id=8453,
|
|
118
|
+
token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
119
|
+
token_amount="1000000000000000000",
|
|
120
|
+
usd_value="1000.00",
|
|
121
|
+
data=None,
|
|
122
|
+
strategy_name="TestStrategy",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_record_withdrawal_success(self, adapter, mock_ledger_client):
|
|
127
|
+
"""Test successful withdrawal recording"""
|
|
128
|
+
mock_response = {
|
|
129
|
+
"transaction_id": "tx_789",
|
|
130
|
+
"status": "recorded",
|
|
131
|
+
"timestamp": "2024-01-15T11:00:00Z",
|
|
132
|
+
}
|
|
133
|
+
mock_ledger_client.add_strategy_withdraw.return_value = mock_response
|
|
134
|
+
|
|
135
|
+
# Test
|
|
136
|
+
success, data = await adapter.record_withdrawal(
|
|
137
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
138
|
+
chain_id=8453,
|
|
139
|
+
token_address="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
140
|
+
token_amount="500000000000000000",
|
|
141
|
+
usd_value="500.00",
|
|
142
|
+
strategy_name="TestStrategy",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
assert success is True
|
|
146
|
+
assert data == mock_response
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_record_operation_success(self, adapter, mock_ledger_client):
|
|
150
|
+
"""Test successful operation recording"""
|
|
151
|
+
from wayfinder_paths.core.adapters.models import SWAP
|
|
152
|
+
|
|
153
|
+
mock_response = {
|
|
154
|
+
"operation_id": "op_123",
|
|
155
|
+
"status": "recorded",
|
|
156
|
+
"timestamp": "2024-01-15T10:45:00Z",
|
|
157
|
+
}
|
|
158
|
+
mock_ledger_client.add_strategy_operation.return_value = mock_response
|
|
159
|
+
|
|
160
|
+
# Test
|
|
161
|
+
operation_data = SWAP(
|
|
162
|
+
from_token_id="0xA0b86a33E6441c8C06DdD4D4c4c4c4c4c4c4c4c4c",
|
|
163
|
+
to_token_id="0xB1c97a44F7552d9Dd5e5e5e5e5e5e5e5e5e5e5e5e5e",
|
|
164
|
+
from_amount="1000000000000000000",
|
|
165
|
+
to_amount="995000000000000000",
|
|
166
|
+
from_amount_usd=1000.0,
|
|
167
|
+
to_amount_usd=995.0,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
success, data = await adapter.record_operation(
|
|
171
|
+
wallet_address="0x1234567890123456789012345678901234567890",
|
|
172
|
+
operation_data=operation_data,
|
|
173
|
+
usd_value="1000.00",
|
|
174
|
+
strategy_name="TestStrategy",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
assert success is True
|
|
178
|
+
assert data == mock_response
|
|
179
|
+
|
|
180
|
+
@pytest.mark.asyncio
|
|
181
|
+
async def test_get_transaction_summary_success(self, adapter, mock_ledger_client):
|
|
182
|
+
"""Test successful transaction summary generation"""
|
|
183
|
+
mock_transactions = {
|
|
184
|
+
"transactions": [
|
|
185
|
+
{"operation": "DEPOSIT", "amount": "1000000000000000000"},
|
|
186
|
+
{"operation": "WITHDRAW", "amount": "500000000000000000"},
|
|
187
|
+
{"operation": "SWAP", "amount": "200000000000000000"},
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
mock_ledger_client.get_strategy_transactions.return_value = mock_transactions
|
|
191
|
+
|
|
192
|
+
# Test
|
|
193
|
+
success, data = await adapter.get_transaction_summary(
|
|
194
|
+
wallet_address="0x1234567890123456789012345678901234567890", limit=10
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert success is True
|
|
198
|
+
assert data["total_transactions"] == 3
|
|
199
|
+
assert data["operations"]["deposits"] == 1
|
|
200
|
+
assert data["operations"]["withdrawals"] == 1
|
|
201
|
+
assert data["operations"]["operations"] == 1
|
|
202
|
+
|
|
203
|
+
def test_adapter_type(self, adapter):
|
|
204
|
+
"""Test adapter has adapter_type"""
|
|
205
|
+
assert adapter.adapter_type == "LEDGER"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Pool Adapter
|
|
2
|
+
|
|
3
|
+
A Wayfinder adapter that provides high-level operations for DeFi pool data and analytics. This adapter wraps the `PoolClient` to offer strategy-friendly methods for discovering, analyzing, and filtering yield opportunities.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
|
|
7
|
+
- `pool.read`: Read pool information and metadata
|
|
8
|
+
- `pool.analytics`: Get comprehensive pool analytics
|
|
9
|
+
- `pool.discovery`: Find and search pools
|
|
10
|
+
- `llama.data`: Access Llama protocol data
|
|
11
|
+
- `pool.reports`: Get pool reports and analytics
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
The adapter uses the PoolClient which automatically handles authentication and API configuration through the Wayfinder settings. No additional configuration is required.
|
|
16
|
+
|
|
17
|
+
The PoolClient will automatically:
|
|
18
|
+
- Use the WAYFINDER_API_URL from settings
|
|
19
|
+
- Handle authentication via environment variables or config.json
|
|
20
|
+
- Manage token refresh and retry logic
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Initialize the Adapter
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from wayfinder_paths.adapters.pool_adapter.adapter import PoolAdapter
|
|
28
|
+
|
|
29
|
+
# No configuration needed - uses PoolClient with automatic settings
|
|
30
|
+
adapter = PoolAdapter()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Get Pools by IDs
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
success, data = await adapter.get_pools_by_ids(
|
|
37
|
+
pool_ids=["pool-123", "pool-456"],
|
|
38
|
+
merge_external=True
|
|
39
|
+
)
|
|
40
|
+
if success:
|
|
41
|
+
pools = data.get("pools", [])
|
|
42
|
+
print(f"Found {len(pools)} pools")
|
|
43
|
+
else:
|
|
44
|
+
print(f"Error: {data}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Get All Pools
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
success, data = await adapter.get_all_pools(merge_external=False)
|
|
51
|
+
if success:
|
|
52
|
+
pools = data.get("pools", [])
|
|
53
|
+
print(f"Total pools available: {len(pools)}")
|
|
54
|
+
else:
|
|
55
|
+
print(f"Error: {data}")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Find High Yield Pools
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
success, data = await adapter.find_high_yield_pools(
|
|
62
|
+
min_apy=0.03, # 3% minimum APY
|
|
63
|
+
min_tvl=500000, # $500k minimum TVL
|
|
64
|
+
stablecoin_only=True,
|
|
65
|
+
network_codes=["base", "ethereum"]
|
|
66
|
+
)
|
|
67
|
+
if success:
|
|
68
|
+
pools = data.get("pools", [])
|
|
69
|
+
print(f"Found {len(pools)} high-yield pools")
|
|
70
|
+
for pool in pools:
|
|
71
|
+
print(f"Pool: {pool.get('id')} - APY: {pool.get('llama_apy_pct')}%")
|
|
72
|
+
else:
|
|
73
|
+
print(f"Error: {data}")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Get Pool Analytics
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
success, data = await adapter.get_pool_analytics(
|
|
80
|
+
pool_ids=["pool-123", "pool-456"]
|
|
81
|
+
)
|
|
82
|
+
if success:
|
|
83
|
+
analytics = data.get("analytics", [])
|
|
84
|
+
for pool_analytics in analytics:
|
|
85
|
+
pool = pool_analytics.get("pool", {})
|
|
86
|
+
combined_apy = pool_analytics.get("combined_apy", 0)
|
|
87
|
+
tvl_usd = pool_analytics.get("tvl_usd", 0)
|
|
88
|
+
print(f"Pool: {pool.get('name')} - APY: {combined_apy:.2%} - TVL: ${tvl_usd:,.0f}")
|
|
89
|
+
else:
|
|
90
|
+
print(f"Error: {data}")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Search Pools
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
success, data = await adapter.search_pools(
|
|
97
|
+
query="USDC",
|
|
98
|
+
limit=10
|
|
99
|
+
)
|
|
100
|
+
if success:
|
|
101
|
+
pools = data.get("pools", [])
|
|
102
|
+
print(f"Found {len(pools)} pools matching 'USDC'")
|
|
103
|
+
for pool in pools:
|
|
104
|
+
print(f"Pool: {pool.get('name')} - {pool.get('symbol')}")
|
|
105
|
+
else:
|
|
106
|
+
print(f"Error: {data}")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Get Llama Matches
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
success, data = await adapter.get_llama_matches()
|
|
113
|
+
if success:
|
|
114
|
+
matches = data.get("matches", [])
|
|
115
|
+
print(f"Found {len(matches)} Llama matches")
|
|
116
|
+
for match in matches:
|
|
117
|
+
if match.get("llama_stablecoin"):
|
|
118
|
+
print(f"Stablecoin pool: {match.get('id')} - APY: {match.get('llama_apy_pct')}%")
|
|
119
|
+
else:
|
|
120
|
+
print(f"Error: {data}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Get Llama Reports
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
success, data = await adapter.get_llama_reports(
|
|
127
|
+
identifiers=["pool-123", "usd-coin-base", "base_0x1234..."]
|
|
128
|
+
)
|
|
129
|
+
if success:
|
|
130
|
+
reports = data
|
|
131
|
+
for identifier, report in reports.items():
|
|
132
|
+
print(f"Report for {identifier}: APY {report.get('llama_apy_pct', 0)}%")
|
|
133
|
+
else:
|
|
134
|
+
print(f"Error: {data}")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Advanced Usage
|
|
138
|
+
|
|
139
|
+
### Filtering High Yield Pools
|
|
140
|
+
|
|
141
|
+
The `find_high_yield_pools` method provides powerful filtering capabilities:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# Find stablecoin pools with high yield on specific networks
|
|
145
|
+
success, data = await adapter.find_high_yield_pools(
|
|
146
|
+
min_apy=0.05, # 5% minimum APY
|
|
147
|
+
min_tvl=1000000, # $1M minimum TVL
|
|
148
|
+
stablecoin_only=True, # Only stablecoin pools
|
|
149
|
+
network_codes=["base", "arbitrum"] # Specific networks
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if success:
|
|
153
|
+
pools = data.get("pools", [])
|
|
154
|
+
# Pools are automatically sorted by APY (highest first)
|
|
155
|
+
best_pool = pools[0] if pools else None
|
|
156
|
+
if best_pool:
|
|
157
|
+
print(f"Best pool: {best_pool.get('id')} - APY: {best_pool.get('llama_apy_pct')}%")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Comprehensive Pool Analysis
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Get detailed analytics for specific pools
|
|
164
|
+
success, data = await adapter.get_pool_analytics(["pool-123"])
|
|
165
|
+
|
|
166
|
+
if success:
|
|
167
|
+
analytics = data.get("analytics", [])
|
|
168
|
+
for pool_analytics in analytics:
|
|
169
|
+
pool = pool_analytics.get("pool", {})
|
|
170
|
+
llama_data = pool_analytics.get("llama_data", {})
|
|
171
|
+
|
|
172
|
+
print(f"Pool: {pool.get('name')}")
|
|
173
|
+
print(f" Combined APY: {pool_analytics.get('combined_apy', 0):.2%}")
|
|
174
|
+
print(f" TVL: ${pool_analytics.get('tvl_usd', 0):,.0f}")
|
|
175
|
+
print(f" Llama APY: {llama_data.get('llama_apy_pct', 0)}%")
|
|
176
|
+
print(f" Stablecoin: {llama_data.get('llama_stablecoin', False)}")
|
|
177
|
+
print(f" IL Risk: {llama_data.get('llama_il_risk', 'unknown')}")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## API Endpoints
|
|
181
|
+
|
|
182
|
+
The adapter uses the following Wayfinder API endpoints:
|
|
183
|
+
|
|
184
|
+
- `GET /api/v1/public/pools/?pool_ids=X` - Get pools by IDs
|
|
185
|
+
- `GET /api/v1/public/pools/` - Get all pools
|
|
186
|
+
- `GET /api/v1/public/pools/llama/matches/` - Get Llama matches
|
|
187
|
+
- `GET /api/v1/public/pools/llama/reports/` - Get Llama reports
|
|
188
|
+
|
|
189
|
+
## Error Handling
|
|
190
|
+
|
|
191
|
+
All methods return a tuple of `(success: bool, data: Any)` where:
|
|
192
|
+
- `success` is `True` if the operation succeeded
|
|
193
|
+
- `data` contains the response data on success or error message on failure
|
|
194
|
+
|
|
195
|
+
## Testing
|
|
196
|
+
|
|
197
|
+
Run the adapter tests:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pytest wayfinder_paths/adapters/pool_adapter/test_adapter.py -v
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Dependencies
|
|
204
|
+
|
|
205
|
+
- `PoolClient` - Low-level API client for pool operations
|
|
206
|
+
- `BaseAdapter` - Base adapter class with common functionality
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
4
|
+
from wayfinder_paths.core.clients.PoolClient import (
|
|
5
|
+
LlamaMatch,
|
|
6
|
+
LlamaReport,
|
|
7
|
+
PoolClient,
|
|
8
|
+
PoolList,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PoolAdapter(BaseAdapter):
|
|
13
|
+
"""
|
|
14
|
+
Pool adapter for DeFi pool data and analytics operations.
|
|
15
|
+
|
|
16
|
+
Provides high-level operations for:
|
|
17
|
+
- Fetching pool information and metadata
|
|
18
|
+
- Getting pool analytics and reports
|
|
19
|
+
- Accessing Llama protocol data
|
|
20
|
+
- Pool discovery and filtering
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
adapter_type: str = "POOL"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
config: dict[str, Any] | None = None,
|
|
28
|
+
pool_client: PoolClient | None = None,
|
|
29
|
+
):
|
|
30
|
+
super().__init__("pool_adapter", config)
|
|
31
|
+
self.pool_client = pool_client or PoolClient()
|
|
32
|
+
|
|
33
|
+
async def get_pools_by_ids(
|
|
34
|
+
self, pool_ids: list[str], merge_external: bool | None = None
|
|
35
|
+
) -> tuple[bool, PoolList | str]:
|
|
36
|
+
"""
|
|
37
|
+
Get pool information by pool IDs.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
pool_ids: List of pool identifiers
|
|
41
|
+
merge_external: Whether to merge external data
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple of (success, data) where data is pool information or error message
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
pool_ids_str = ",".join(pool_ids)
|
|
48
|
+
data = await self.pool_client.get_pools_by_ids(
|
|
49
|
+
pool_ids=pool_ids_str, merge_external=merge_external
|
|
50
|
+
)
|
|
51
|
+
return (True, data)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.logger.error(f"Error fetching pools by IDs: {e}")
|
|
54
|
+
return (False, str(e))
|
|
55
|
+
|
|
56
|
+
async def get_all_pools(
|
|
57
|
+
self, merge_external: bool | None = None
|
|
58
|
+
) -> tuple[bool, PoolList | str]:
|
|
59
|
+
"""
|
|
60
|
+
Get all available pools.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
merge_external: Whether to merge external data
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of (success, data) where data is all pools or error message
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
data = await self.pool_client.get_all_pools(merge_external=merge_external)
|
|
70
|
+
return (True, data)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.logger.error(f"Error fetching all pools: {e}")
|
|
73
|
+
return (False, str(e))
|
|
74
|
+
|
|
75
|
+
async def get_llama_matches(self) -> tuple[bool, dict[str, LlamaMatch] | str]:
|
|
76
|
+
"""
|
|
77
|
+
Get Llama protocol matches for pools.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple of (success, data) where data is Llama matches or error message
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
data = await self.pool_client.get_llama_matches()
|
|
84
|
+
return (True, data)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
self.logger.error(f"Error fetching Llama matches: {e}")
|
|
87
|
+
return (False, str(e))
|
|
88
|
+
|
|
89
|
+
async def get_llama_reports(
|
|
90
|
+
self, identifiers: list[str]
|
|
91
|
+
) -> tuple[bool, dict[str, LlamaReport] | str]:
|
|
92
|
+
"""
|
|
93
|
+
Get Llama reports for specific identifiers.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
identifiers: List of identifiers (token IDs, addresses, pool IDs)
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of (success, data) where data is Llama reports or error message
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
identifiers_str = ",".join(identifiers)
|
|
103
|
+
data = await self.pool_client.get_llama_reports(identifiers=identifiers_str)
|
|
104
|
+
return (True, data)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
self.logger.error(f"Error fetching Llama reports: {e}")
|
|
107
|
+
return (False, str(e))
|
|
108
|
+
|
|
109
|
+
async def find_high_yield_pools(
|
|
110
|
+
self,
|
|
111
|
+
min_apy: float = 0.01,
|
|
112
|
+
min_tvl: float = 1000000,
|
|
113
|
+
stablecoin_only: bool = True,
|
|
114
|
+
network_codes: list[str] | None = None,
|
|
115
|
+
) -> tuple[bool, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Find high-yield pools based on criteria.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
min_apy: Minimum APY threshold (as decimal, e.g., 0.01 for 1%)
|
|
121
|
+
min_tvl: Minimum TVL threshold in USD
|
|
122
|
+
stablecoin_only: Whether to filter for stablecoin pools only
|
|
123
|
+
network_codes: List of network codes to filter by
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tuple of (success, data) where data is filtered pools or error message
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
# Get Llama matches for yield data
|
|
130
|
+
success, llama_data = await self.get_llama_matches()
|
|
131
|
+
if not success:
|
|
132
|
+
return (False, f"Failed to fetch Llama data: {llama_data}")
|
|
133
|
+
|
|
134
|
+
matches = llama_data.get("matches", [])
|
|
135
|
+
filtered_pools = []
|
|
136
|
+
|
|
137
|
+
for pool in matches:
|
|
138
|
+
# Apply filters
|
|
139
|
+
if stablecoin_only and not pool.get("llama_stablecoin", False):
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
if pool.get("llama_tvl_usd", 0) < min_tvl:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
pool.get("llama_apy_pct", 0) < min_apy * 100
|
|
147
|
+
): # Convert to percentage
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if network_codes and pool.get("network", "").lower() not in [
|
|
151
|
+
nc.lower() for nc in network_codes
|
|
152
|
+
]:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
filtered_pools.append(pool)
|
|
156
|
+
|
|
157
|
+
# Sort by APY descending
|
|
158
|
+
filtered_pools.sort(key=lambda x: x.get("llama_apy_pct", 0), reverse=True)
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
True,
|
|
162
|
+
{
|
|
163
|
+
"pools": filtered_pools,
|
|
164
|
+
"total_found": len(filtered_pools),
|
|
165
|
+
"filters_applied": {
|
|
166
|
+
"min_apy": min_apy,
|
|
167
|
+
"min_tvl": min_tvl,
|
|
168
|
+
"stablecoin_only": stablecoin_only,
|
|
169
|
+
"network_codes": network_codes,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
self.logger.error(f"Error finding high yield pools: {e}")
|
|
175
|
+
return (False, str(e))
|
|
176
|
+
|
|
177
|
+
async def get_pool_analytics(self, pool_ids: list[str]) -> tuple[bool, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Get comprehensive analytics for specific pools.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
pool_ids: List of pool identifiers
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tuple of (success, data) where data is pool analytics or error message
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
# Get pool data
|
|
189
|
+
success, pool_data = await self.get_pools_by_ids(pool_ids)
|
|
190
|
+
if not success:
|
|
191
|
+
return (False, f"Failed to fetch pool data: {pool_data}")
|
|
192
|
+
|
|
193
|
+
# Get Llama reports
|
|
194
|
+
success, llama_data = await self.get_llama_reports(pool_ids)
|
|
195
|
+
if not success:
|
|
196
|
+
self.logger.warning(f"Failed to fetch Llama data: {llama_data}")
|
|
197
|
+
llama_data = {}
|
|
198
|
+
|
|
199
|
+
pools = pool_data.get("pools", [])
|
|
200
|
+
llama_reports = llama_data
|
|
201
|
+
|
|
202
|
+
# Combine data
|
|
203
|
+
analytics = []
|
|
204
|
+
for pool in pools:
|
|
205
|
+
pool_id = pool.get("id")
|
|
206
|
+
llama_report = llama_reports.get(pool_id.lower()) if pool_id else None
|
|
207
|
+
|
|
208
|
+
analytics.append(
|
|
209
|
+
{
|
|
210
|
+
"pool": pool,
|
|
211
|
+
"llama_data": llama_report,
|
|
212
|
+
"combined_apy": (
|
|
213
|
+
llama_report.get("llama_combined_apy_pct", 0) / 100
|
|
214
|
+
if llama_report
|
|
215
|
+
and llama_report.get("llama_combined_apy_pct") is not None
|
|
216
|
+
else pool.get("apy", 0)
|
|
217
|
+
),
|
|
218
|
+
"tvl_usd": (
|
|
219
|
+
llama_report.get("llama_tvl_usd", 0)
|
|
220
|
+
if llama_report and llama_report.get("llama_tvl_usd")
|
|
221
|
+
else pool.get("tvl", 0)
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return (True, {"analytics": analytics, "total_pools": len(analytics)})
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self.logger.error(f"Error getting pool analytics: {e}")
|
|
229
|
+
return (False, str(e))
|
|
230
|
+
|
|
231
|
+
async def search_pools(self, query: str, limit: int = 10) -> tuple[bool, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Search pools by name, symbol, or other criteria.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
query: Search query string
|
|
237
|
+
limit: Maximum number of results
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple of (success, data) where data is search results or error message
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
success, all_pools_data = await self.get_all_pools()
|
|
244
|
+
if not success:
|
|
245
|
+
return (False, f"Failed to fetch pools: {all_pools_data}")
|
|
246
|
+
|
|
247
|
+
pools = all_pools_data.get("pools", [])
|
|
248
|
+
query_lower = query.lower()
|
|
249
|
+
|
|
250
|
+
# Simple text search
|
|
251
|
+
matching_pools = []
|
|
252
|
+
for pool in pools:
|
|
253
|
+
name = pool.get("name", "").lower()
|
|
254
|
+
symbol = pool.get("symbol", "").lower()
|
|
255
|
+
description = pool.get("description", "").lower()
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
query_lower in name
|
|
259
|
+
or query_lower in symbol
|
|
260
|
+
or query_lower in description
|
|
261
|
+
):
|
|
262
|
+
matching_pools.append(pool)
|
|
263
|
+
|
|
264
|
+
# Sort by relevance (exact matches first)
|
|
265
|
+
matching_pools.sort(
|
|
266
|
+
key=lambda x: (
|
|
267
|
+
query_lower not in x.get("name", "").lower(),
|
|
268
|
+
query_lower not in x.get("symbol", "").lower(),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
True,
|
|
274
|
+
{
|
|
275
|
+
"pools": matching_pools[:limit],
|
|
276
|
+
"total_found": len(matching_pools),
|
|
277
|
+
"query": query,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
self.logger.error(f"Error searching pools: {e}")
|
|
282
|
+
return (False, str(e))
|