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,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from eth_utils import to_checksum_address
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from web3 import AsyncWeb3
|
|
8
|
+
|
|
9
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
10
|
+
from wayfinder_paths.core.clients.TransactionClient import TransactionClient
|
|
11
|
+
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
12
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_APPROVAL_ABI
|
|
13
|
+
from wayfinder_paths.core.services.base import EvmTxn, TokenTxn
|
|
14
|
+
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LocalTokenTxnService(TokenTxn):
|
|
18
|
+
"""Default transaction builder used by adapters."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
config: dict[str, Any] | None,
|
|
23
|
+
*,
|
|
24
|
+
wallet_provider: EvmTxn,
|
|
25
|
+
simulation: bool = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
del config, simulation
|
|
28
|
+
self.wallet_provider = wallet_provider
|
|
29
|
+
self.logger = logger.bind(service="DefaultEvmTransactionService")
|
|
30
|
+
self.token_client = TokenClient()
|
|
31
|
+
self.builder = _EvmTransactionBuilder()
|
|
32
|
+
|
|
33
|
+
async def build_send(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
token_id: str,
|
|
37
|
+
amount: float,
|
|
38
|
+
from_address: str,
|
|
39
|
+
to_address: str,
|
|
40
|
+
token_info: dict[str, Any] | None = None,
|
|
41
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
42
|
+
"""Build the transaction dict for sending tokens between wallets."""
|
|
43
|
+
token_meta = token_info
|
|
44
|
+
if token_meta is None:
|
|
45
|
+
token_meta = await self.token_client.get_token_details(token_id)
|
|
46
|
+
if not token_meta:
|
|
47
|
+
return False, f"Token not found: {token_id}"
|
|
48
|
+
|
|
49
|
+
chain_id = resolve_chain_id(token_meta, self.logger)
|
|
50
|
+
if chain_id is None:
|
|
51
|
+
return False, f"Token {token_id} is missing a chain id"
|
|
52
|
+
|
|
53
|
+
token_address = (token_meta or {}).get("address") or ZERO_ADDRESS
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
tx = await self.builder.build_send_transaction(
|
|
57
|
+
from_address=from_address,
|
|
58
|
+
to_address=to_address,
|
|
59
|
+
token_address=token_address,
|
|
60
|
+
amount=amount,
|
|
61
|
+
chain_id=int(chain_id),
|
|
62
|
+
)
|
|
63
|
+
except Exception as exc: # noqa: BLE001
|
|
64
|
+
return False, f"Failed to build send transaction: {exc}"
|
|
65
|
+
|
|
66
|
+
return True, tx
|
|
67
|
+
|
|
68
|
+
def build_erc20_approve(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
chain_id: int,
|
|
72
|
+
token_address: str,
|
|
73
|
+
from_address: str,
|
|
74
|
+
spender: str,
|
|
75
|
+
amount: int,
|
|
76
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
77
|
+
"""Build the transaction dictionary for an ERC20 approval."""
|
|
78
|
+
try:
|
|
79
|
+
web3 = self.wallet_provider.get_web3(chain_id)
|
|
80
|
+
token_checksum = to_checksum_address(token_address)
|
|
81
|
+
from_checksum = to_checksum_address(from_address)
|
|
82
|
+
spender_checksum = to_checksum_address(spender)
|
|
83
|
+
amount_int = int(amount)
|
|
84
|
+
except (TypeError, ValueError) as exc:
|
|
85
|
+
return False, str(exc)
|
|
86
|
+
|
|
87
|
+
approve_tx = self.builder.build_erc20_approval_transaction(
|
|
88
|
+
chain_id=chain_id,
|
|
89
|
+
token_address=token_checksum,
|
|
90
|
+
from_address=from_checksum,
|
|
91
|
+
spender=spender_checksum,
|
|
92
|
+
amount=amount_int,
|
|
93
|
+
web3=web3,
|
|
94
|
+
)
|
|
95
|
+
return True, approve_tx
|
|
96
|
+
|
|
97
|
+
async def read_erc20_allowance(
|
|
98
|
+
self, chain: Any, token_address: str, from_address: str, spender_address: str
|
|
99
|
+
) -> dict[str, Any]:
|
|
100
|
+
try:
|
|
101
|
+
chain_id = self._chain_id(chain)
|
|
102
|
+
except (TypeError, ValueError) as exc:
|
|
103
|
+
return {"error": str(exc), "allowance": 0}
|
|
104
|
+
|
|
105
|
+
w3 = self.get_web3(chain_id)
|
|
106
|
+
try:
|
|
107
|
+
contract = w3.eth.contract(
|
|
108
|
+
address=to_checksum_address(token_address), abi=ERC20_APPROVAL_ABI
|
|
109
|
+
)
|
|
110
|
+
allowance = await contract.functions.allowance(
|
|
111
|
+
to_checksum_address(from_address),
|
|
112
|
+
to_checksum_address(spender_address),
|
|
113
|
+
).call()
|
|
114
|
+
return (True, {"allowance": int(allowance)})
|
|
115
|
+
except Exception as exc: # noqa: BLE001
|
|
116
|
+
self.logger.error(f"Failed to read allowance: {exc}")
|
|
117
|
+
return {"error": f"Allowance query failed: {exc}", "allowance": 0}
|
|
118
|
+
finally:
|
|
119
|
+
await self._close_web3(w3)
|
|
120
|
+
|
|
121
|
+
def _chain_id(self, chain: Any) -> int:
|
|
122
|
+
if isinstance(chain, dict):
|
|
123
|
+
chain_id = chain.get("id") or chain.get("chain_id")
|
|
124
|
+
else:
|
|
125
|
+
chain_id = getattr(chain, "id", None)
|
|
126
|
+
if chain_id is None:
|
|
127
|
+
raise ValueError("Chain ID is required")
|
|
128
|
+
return int(chain_id)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _EvmTransactionBuilder:
|
|
132
|
+
"""Helpers that only build transaction dictionaries for sends and approvals."""
|
|
133
|
+
|
|
134
|
+
def __init__(self) -> None:
|
|
135
|
+
self.transaction_client = TransactionClient()
|
|
136
|
+
|
|
137
|
+
async def build_send_transaction(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
from_address: str,
|
|
141
|
+
to_address: str,
|
|
142
|
+
token_address: str | None,
|
|
143
|
+
amount: float,
|
|
144
|
+
chain_id: int,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Build the transaction dict for sending native or ERC20 tokens."""
|
|
147
|
+
payload = await self.transaction_client.build_send(
|
|
148
|
+
from_address=from_address,
|
|
149
|
+
to_address=to_address,
|
|
150
|
+
token_address=token_address or "",
|
|
151
|
+
amount=float(amount),
|
|
152
|
+
chain_id=int(chain_id),
|
|
153
|
+
)
|
|
154
|
+
return self._payload_to_tx(
|
|
155
|
+
payload=payload,
|
|
156
|
+
from_address=from_address,
|
|
157
|
+
is_native=not token_address or token_address.lower() == ZERO_ADDRESS,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def build_erc20_approval_transaction(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
chain_id: int,
|
|
164
|
+
token_address: str,
|
|
165
|
+
from_address: str,
|
|
166
|
+
spender: str,
|
|
167
|
+
amount: int,
|
|
168
|
+
web3: AsyncWeb3,
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""Build an ERC20 approval transaction dict."""
|
|
171
|
+
token_checksum = to_checksum_address(token_address)
|
|
172
|
+
spender_checksum = to_checksum_address(spender)
|
|
173
|
+
from_checksum = to_checksum_address(from_address)
|
|
174
|
+
amount_int = int(amount)
|
|
175
|
+
|
|
176
|
+
contract = web3.eth.contract(address=token_checksum, abi=ERC20_APPROVAL_ABI)
|
|
177
|
+
data = contract.encodeABI(
|
|
178
|
+
fn_name="approve", args=[spender_checksum, amount_int]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"chainId": int(chain_id),
|
|
183
|
+
"from": from_checksum,
|
|
184
|
+
"to": token_checksum,
|
|
185
|
+
"data": data,
|
|
186
|
+
"value": 0,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def _payload_to_tx(
|
|
190
|
+
self, payload: dict[str, Any], from_address: str, is_native: bool
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
data_root = payload.get("data", payload)
|
|
193
|
+
tx_src = data_root.get("transaction") or data_root
|
|
194
|
+
|
|
195
|
+
chain_id = tx_src.get("chainId") or data_root.get("chain_id")
|
|
196
|
+
if chain_id is None:
|
|
197
|
+
raise ValueError("Transaction payload missing chainId")
|
|
198
|
+
|
|
199
|
+
tx: dict[str, Any] = {"chainId": int(chain_id)}
|
|
200
|
+
tx["from"] = to_checksum_address(from_address)
|
|
201
|
+
|
|
202
|
+
if tx_src.get("to"):
|
|
203
|
+
tx["to"] = to_checksum_address(tx_src["to"])
|
|
204
|
+
if tx_src.get("data"):
|
|
205
|
+
data = tx_src["data"]
|
|
206
|
+
tx["data"] = data if str(data).startswith("0x") else f"0x{data}"
|
|
207
|
+
|
|
208
|
+
val = tx_src.get("value", 0)
|
|
209
|
+
tx["value"] = self._normalize_value(val) if is_native else 0
|
|
210
|
+
|
|
211
|
+
if tx_src.get("gas"):
|
|
212
|
+
tx["gas"] = int(tx_src["gas"])
|
|
213
|
+
if tx_src.get("maxFeePerGas"):
|
|
214
|
+
tx["maxFeePerGas"] = int(tx_src["maxFeePerGas"])
|
|
215
|
+
if tx_src.get("maxPriorityFeePerGas"):
|
|
216
|
+
tx["maxPriorityFeePerGas"] = int(tx_src["maxPriorityFeePerGas"])
|
|
217
|
+
if tx_src.get("gasPrice"):
|
|
218
|
+
tx["gasPrice"] = int(tx_src["gasPrice"])
|
|
219
|
+
if tx_src.get("nonce") is not None:
|
|
220
|
+
tx["nonce"] = int(tx_src["nonce"])
|
|
221
|
+
|
|
222
|
+
return tx
|
|
223
|
+
|
|
224
|
+
def _normalize_value(self, value: Any) -> int:
|
|
225
|
+
if isinstance(value, str):
|
|
226
|
+
if value.startswith("0x"):
|
|
227
|
+
return int(value, 16)
|
|
228
|
+
return int(float(value))
|
|
229
|
+
if isinstance(value, (int, float)):
|
|
230
|
+
return int(value)
|
|
231
|
+
return 0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.services.base import EvmTxn, TokenTxn, Web3Service
|
|
4
|
+
from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
|
|
5
|
+
from wayfinder_paths.core.services.local_token_txn import (
|
|
6
|
+
LocalTokenTxnService,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DefaultWeb3Service(Web3Service):
|
|
11
|
+
"""Default implementation that simply wires the provided dependencies together."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
config: dict | None = None,
|
|
16
|
+
*,
|
|
17
|
+
wallet_provider: EvmTxn | None = None,
|
|
18
|
+
evm_transactions: TokenTxn | None = None,
|
|
19
|
+
simulation: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initialize the service with optional dependency injection.
|
|
23
|
+
|
|
24
|
+
Strategies that already constructed wallet providers or transaction helpers
|
|
25
|
+
can pass them in directly. Otherwise we fall back to the legacy behavior of
|
|
26
|
+
building a LocalWalletProvider + DefaultEvmTransactionService from config.
|
|
27
|
+
"""
|
|
28
|
+
cfg = config or {}
|
|
29
|
+
self._wallet_provider = wallet_provider or LocalEvmTxn(cfg)
|
|
30
|
+
if evm_transactions is not None:
|
|
31
|
+
self._evm_transactions = evm_transactions
|
|
32
|
+
else:
|
|
33
|
+
self._evm_transactions = LocalTokenTxnService(
|
|
34
|
+
config=cfg,
|
|
35
|
+
wallet_provider=self._wallet_provider,
|
|
36
|
+
simulation=simulation,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def evm_transactions(self) -> EvmTxn:
|
|
41
|
+
return self._wallet_provider
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def token_transactions(self) -> TokenTxn:
|
|
45
|
+
return self._evm_transactions
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CoreSettings(BaseSettings):
|
|
9
|
+
"""
|
|
10
|
+
Core settings for Wayfinder Paths Engine
|
|
11
|
+
These are minimal settings required by the core engine
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
model_config = SettingsConfigDict(
|
|
15
|
+
env_file=".env",
|
|
16
|
+
env_file_encoding="utf-8",
|
|
17
|
+
case_sensitive=False,
|
|
18
|
+
extra="ignore", # Ignore extra environment variables (e.g., from Django)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Core API Configuration
|
|
22
|
+
API_ENV: str = Field("development", env="API_ENV")
|
|
23
|
+
|
|
24
|
+
def _compute_default_api_url() -> str:
|
|
25
|
+
"""
|
|
26
|
+
Determine default API base URL from config.json if present, otherwise fallback.
|
|
27
|
+
Do not mutate the value (consistent with rpc_urls resolution).
|
|
28
|
+
"""
|
|
29
|
+
cfg_path = os.getenv("WAYFINDER_CONFIG_PATH", "config.json")
|
|
30
|
+
base = None
|
|
31
|
+
try:
|
|
32
|
+
with open(cfg_path) as f:
|
|
33
|
+
cfg = json.load(f)
|
|
34
|
+
system = cfg.get("system", {}) if isinstance(cfg, dict) else {}
|
|
35
|
+
candidate = system.get("api_base_url")
|
|
36
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
37
|
+
base = candidate.strip()
|
|
38
|
+
except Exception:
|
|
39
|
+
# Config is optional; ignore errors and use fallback
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if not base:
|
|
43
|
+
# Provide a sensible default that includes the full API root
|
|
44
|
+
base = "https://wayfinder.ai/api/v1"
|
|
45
|
+
return base
|
|
46
|
+
|
|
47
|
+
WAYFINDER_API_URL: str = Field(_compute_default_api_url(), env="WAYFINDER_API_URL")
|
|
48
|
+
|
|
49
|
+
# Network Configuration
|
|
50
|
+
NETWORK: str = Field("testnet", env="NETWORK") # mainnet, testnet, devnet
|
|
51
|
+
|
|
52
|
+
# Logging
|
|
53
|
+
LOG_LEVEL: str = Field("INFO", env="LOG_LEVEL")
|
|
54
|
+
LOG_FILE: str = Field("logs/strategy.log", env="LOG_FILE")
|
|
55
|
+
|
|
56
|
+
# Safety
|
|
57
|
+
DRY_RUN: bool = Field(False, env="DRY_RUN")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Core settings instance
|
|
61
|
+
settings = CoreSettings()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Any, TypedDict
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from wayfinder_paths.core.clients.TokenClient import TokenDetails
|
|
12
|
+
from wayfinder_paths.core.services.base import Web3Service
|
|
13
|
+
from wayfinder_paths.core.strategies.descriptors import StratDescriptor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StatusDict(TypedDict):
|
|
17
|
+
portfolio_value: float
|
|
18
|
+
net_deposit: float
|
|
19
|
+
strategy_status: Any
|
|
20
|
+
gas_available: float
|
|
21
|
+
gassed_up: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
StatusTuple = tuple[bool, str]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WalletConfig(TypedDict, total=False):
|
|
28
|
+
"""Wallet configuration structure - allows additional fields for flexibility"""
|
|
29
|
+
|
|
30
|
+
address: str
|
|
31
|
+
private_key: str | None
|
|
32
|
+
private_key_hex: str | None
|
|
33
|
+
wallet_type: str | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StrategyConfig(TypedDict, total=False):
|
|
37
|
+
"""Base strategy configuration structure - allows additional fields for flexibility"""
|
|
38
|
+
|
|
39
|
+
main_wallet: WalletConfig | None
|
|
40
|
+
strategy_wallet: WalletConfig | None
|
|
41
|
+
wallet_type: str | None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LiquidationResult(TypedDict):
|
|
45
|
+
usd_value: float
|
|
46
|
+
token: TokenDetails
|
|
47
|
+
amt: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Strategy(ABC):
|
|
51
|
+
name: str | None = None
|
|
52
|
+
INFO: StratDescriptor | None = None
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
config: StrategyConfig | dict[str, Any] | None = None,
|
|
57
|
+
*,
|
|
58
|
+
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
|
+
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
60
|
+
simulation: bool = False,
|
|
61
|
+
web3_service: Web3Service | None = None,
|
|
62
|
+
api_key: str | None = None,
|
|
63
|
+
):
|
|
64
|
+
self.adapters = {}
|
|
65
|
+
self.ledger_adapter = None
|
|
66
|
+
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
67
|
+
if api_key:
|
|
68
|
+
os.environ["WAYFINDER_API_KEY"] = api_key
|
|
69
|
+
|
|
70
|
+
self.config = config
|
|
71
|
+
|
|
72
|
+
async def setup(self) -> None:
|
|
73
|
+
"""Initialize strategy-specific setup after construction"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
async def log(self, msg: str) -> None:
|
|
77
|
+
"""Log messages - can be overridden by subclasses"""
|
|
78
|
+
self.logger.info(msg)
|
|
79
|
+
|
|
80
|
+
async def quote(self) -> None:
|
|
81
|
+
"""Get quotes for potential trades - optional for strategies"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def _get_strategy_wallet_address(self) -> str:
|
|
85
|
+
"""Get strategy wallet address with validation."""
|
|
86
|
+
strategy_wallet = self.config.get("strategy_wallet")
|
|
87
|
+
if not strategy_wallet or not isinstance(strategy_wallet, dict):
|
|
88
|
+
raise ValueError("strategy_wallet not configured in strategy config")
|
|
89
|
+
address = strategy_wallet.get("address")
|
|
90
|
+
if not address:
|
|
91
|
+
raise ValueError("strategy_wallet address not found in config")
|
|
92
|
+
return str(address)
|
|
93
|
+
|
|
94
|
+
def _get_main_wallet_address(self) -> str:
|
|
95
|
+
"""Get main wallet address with validation."""
|
|
96
|
+
main_wallet = self.config.get("main_wallet")
|
|
97
|
+
if not main_wallet or not isinstance(main_wallet, dict):
|
|
98
|
+
raise ValueError("main_wallet not configured in strategy config")
|
|
99
|
+
address = main_wallet.get("address")
|
|
100
|
+
if not address:
|
|
101
|
+
raise ValueError("main_wallet address not found in config")
|
|
102
|
+
return str(address)
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
async def deposit(self, **kwargs) -> StatusTuple:
|
|
106
|
+
"""
|
|
107
|
+
Deposit funds into the strategy.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
**kwargs: Strategy-specific deposit parameters. Common parameters include:
|
|
111
|
+
- main_token_amount: Amount of main token to deposit (float)
|
|
112
|
+
- gas_token_amount: Amount of gas token to deposit (float)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (success: bool, message: str)
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: If required parameters are missing or invalid.
|
|
119
|
+
"""
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
async def withdraw(self, **kwargs) -> StatusTuple:
|
|
123
|
+
"""
|
|
124
|
+
Withdraw funds from the strategy.
|
|
125
|
+
Default implementation unwinds all operations.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
**kwargs: Strategy-specific withdrawal parameters (optional).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (success: bool, message: str)
|
|
132
|
+
|
|
133
|
+
Note:
|
|
134
|
+
Subclasses may override this method to add validation or custom
|
|
135
|
+
withdrawal logic. The base implementation unwinds all ledger operations.
|
|
136
|
+
"""
|
|
137
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
138
|
+
while self.ledger_adapter.positions.operations:
|
|
139
|
+
node = self.ledger_adapter.positions.operations[-1]
|
|
140
|
+
adapter = self.adapters.get(node.adapter)
|
|
141
|
+
if adapter and hasattr(adapter, "unwind_op"):
|
|
142
|
+
await adapter.unwind_op(node)
|
|
143
|
+
self.ledger_adapter.positions.operations.pop()
|
|
144
|
+
|
|
145
|
+
await self.ledger_adapter.save()
|
|
146
|
+
|
|
147
|
+
return (True, "Withdrawal complete")
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def update(self) -> StatusTuple:
|
|
151
|
+
"""
|
|
152
|
+
Update strategy positions/rebalance
|
|
153
|
+
Returns: (success: bool, message: str)
|
|
154
|
+
"""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
async def policies() -> list[str]:
|
|
159
|
+
"""Return policy strings for this strategy (Django-compatible)."""
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
async def _status(self) -> StatusDict:
|
|
164
|
+
"""
|
|
165
|
+
Return status payload. Subclasses should implement this.
|
|
166
|
+
Should include Django-compatible keys (portfolio_value, net_deposit, strategy_status).
|
|
167
|
+
Backward-compatible keys (active_amount, total_earned) may also be included.
|
|
168
|
+
"""
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
async def status(self) -> StatusDict:
|
|
172
|
+
"""
|
|
173
|
+
Wrapper to compute and return strategy status. In Django, this also snapshots.
|
|
174
|
+
Here we simply delegate to _status for compatibility.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
status = await self._status()
|
|
178
|
+
await self.ledger_adapter.record_strategy_snapshot(
|
|
179
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
180
|
+
strategy_status=status,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return status
|
|
184
|
+
|
|
185
|
+
def register_adapters(self, adapters: list[Any]) -> None:
|
|
186
|
+
"""Register adapters for use by the strategy"""
|
|
187
|
+
self.adapters = {}
|
|
188
|
+
for adapter in adapters:
|
|
189
|
+
if hasattr(adapter, "adapter_type"):
|
|
190
|
+
self.adapters[adapter.adapter_type] = adapter
|
|
191
|
+
elif hasattr(adapter, "__class__"):
|
|
192
|
+
self.adapters[adapter.__class__.__name__] = adapter
|
|
193
|
+
|
|
194
|
+
def unwind_on_error(
|
|
195
|
+
self, func: Callable[..., Awaitable[StatusTuple]]
|
|
196
|
+
) -> Callable[..., Awaitable[StatusTuple]]:
|
|
197
|
+
"""
|
|
198
|
+
Decorator to unwind operations on error
|
|
199
|
+
Useful for deposit operations that need cleanup on failure
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
async def wrapper(*args: Any, **kwargs: Any) -> StatusTuple:
|
|
203
|
+
try:
|
|
204
|
+
return await func(*args, **kwargs)
|
|
205
|
+
except Exception:
|
|
206
|
+
trace = traceback.format_exc()
|
|
207
|
+
try:
|
|
208
|
+
await self.withdraw()
|
|
209
|
+
return (
|
|
210
|
+
False,
|
|
211
|
+
f"Strategy failed during operation and was unwound. Failure: {trace}",
|
|
212
|
+
)
|
|
213
|
+
except Exception:
|
|
214
|
+
trace2 = traceback.format_exc()
|
|
215
|
+
return (
|
|
216
|
+
False,
|
|
217
|
+
f"Strategy failed and unwinding also failed. Operation error: {trace}. Unwind error: {trace2}",
|
|
218
|
+
)
|
|
219
|
+
finally:
|
|
220
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
221
|
+
await self.ledger_adapter.save()
|
|
222
|
+
|
|
223
|
+
return wrapper
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def get_metadata(cls) -> dict[str, Any]:
|
|
227
|
+
"""
|
|
228
|
+
Return metadata about this strategy.
|
|
229
|
+
Can be overridden to provide discovery information.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dictionary containing strategy metadata. The following keys are optional
|
|
233
|
+
and will be None if not defined on the class:
|
|
234
|
+
- name: Strategy name
|
|
235
|
+
- description: Strategy description
|
|
236
|
+
- summary: Strategy summary
|
|
237
|
+
"""
|
|
238
|
+
return {
|
|
239
|
+
"name": getattr(cls, "name", None),
|
|
240
|
+
"description": getattr(cls, "description", None),
|
|
241
|
+
"summary": getattr(cls, "summary", None),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async def health_check(self) -> dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Check strategy health and dependencies
|
|
247
|
+
"""
|
|
248
|
+
health = {"status": "healthy", "strategy": self.name, "adapters": {}}
|
|
249
|
+
|
|
250
|
+
for name, adapter in self.adapters.items():
|
|
251
|
+
if hasattr(adapter, "health_check"):
|
|
252
|
+
health["adapters"][name] = await adapter.health_check()
|
|
253
|
+
else:
|
|
254
|
+
health["adapters"][name] = {"status": "unknown"}
|
|
255
|
+
|
|
256
|
+
return health
|
|
257
|
+
|
|
258
|
+
async def partial_liquidate(
|
|
259
|
+
self, usd_value: float
|
|
260
|
+
) -> tuple[bool, LiquidationResult]:
|
|
261
|
+
"""
|
|
262
|
+
Partially liquidate strategy positions by USD value.
|
|
263
|
+
Optional method that can be overridden by subclasses.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
usd_value: USD value to liquidate (must be positive).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Tuple of (success: bool, message: str)
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If usd_value is not positive.
|
|
273
|
+
|
|
274
|
+
Note:
|
|
275
|
+
Base implementation returns failure. Subclasses should override
|
|
276
|
+
to implement partial liquidation logic.
|
|
277
|
+
"""
|
|
278
|
+
if usd_value <= 0:
|
|
279
|
+
raise ValueError(f"usd_value must be positive, got {usd_value}")
|
|
280
|
+
return (False, "Partial liquidation not implemented for this strategy")
|