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.

Files changed (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. 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,7 @@
1
+ """
2
+ Pool Adapter Package
3
+ """
4
+
5
+ from .adapter import PoolAdapter
6
+
7
+ __all__ = ["PoolAdapter"]
@@ -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))