wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.24__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/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
wayfinder_paths/__init__.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
__version__ = "0.1.0"
|
|
2
2
|
|
|
3
|
-
# Re-export commonly used items for convenience
|
|
4
3
|
from wayfinder_paths.core import (
|
|
5
4
|
BaseAdapter,
|
|
6
|
-
LiquidationResult,
|
|
7
5
|
StatusDict,
|
|
8
6
|
StatusTuple,
|
|
9
7
|
Strategy,
|
|
10
|
-
StrategyJob,
|
|
11
8
|
)
|
|
12
9
|
|
|
13
10
|
__all__ = [
|
|
@@ -16,5 +13,4 @@ __all__ = [
|
|
|
16
13
|
"Strategy",
|
|
17
14
|
"StatusDict",
|
|
18
15
|
"StatusTuple",
|
|
19
|
-
"StrategyJob",
|
|
20
16
|
]
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
5
|
+
from wayfinder_paths.adapters.multicall_adapter.adapter import MulticallAdapter
|
|
4
6
|
from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
|
|
5
7
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
6
8
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
7
|
-
from wayfinder_paths.core.
|
|
9
|
+
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
10
|
+
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
8
11
|
from wayfinder_paths.core.utils.evm_helpers import resolve_chain_id
|
|
9
|
-
from wayfinder_paths.core.utils.tokens import build_send_transaction
|
|
12
|
+
from wayfinder_paths.core.utils.tokens import build_send_transaction, get_token_balance
|
|
10
13
|
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
14
|
+
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
11
15
|
|
|
12
16
|
|
|
13
17
|
class BalanceAdapter(BaseAdapter):
|
|
@@ -22,61 +26,33 @@ class BalanceAdapter(BaseAdapter):
|
|
|
22
26
|
super().__init__("balance", config)
|
|
23
27
|
self.main_wallet_signing_callback = main_wallet_signing_callback
|
|
24
28
|
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
25
|
-
self.wallet_client = WalletClient()
|
|
26
29
|
self.token_client = TokenClient()
|
|
27
30
|
self.token_adapter = TokenAdapter()
|
|
28
31
|
self.ledger_adapter = LedgerAdapter()
|
|
29
32
|
|
|
30
|
-
def _parse_balance(self, raw: Any) -> int:
|
|
31
|
-
if raw is None:
|
|
32
|
-
return 0
|
|
33
|
-
try:
|
|
34
|
-
return int(raw)
|
|
35
|
-
except (ValueError, TypeError):
|
|
36
|
-
try:
|
|
37
|
-
return int(float(raw))
|
|
38
|
-
except (ValueError, TypeError):
|
|
39
|
-
return 0
|
|
40
|
-
|
|
41
33
|
async def get_balance(
|
|
42
34
|
self,
|
|
43
35
|
*,
|
|
44
|
-
query: str | dict[str, Any] | None = None,
|
|
45
|
-
token_id: str | None = None,
|
|
46
36
|
wallet_address: str,
|
|
37
|
+
token_id: str | None = None,
|
|
38
|
+
token_address: str | None = None,
|
|
47
39
|
chain_id: int | None = None,
|
|
48
|
-
) -> tuple[bool,
|
|
49
|
-
effective_query = query if query is not None else token_id
|
|
50
|
-
resolved = (
|
|
51
|
-
effective_query
|
|
52
|
-
if isinstance(effective_query, str)
|
|
53
|
-
else (effective_query or {}).get("token_id")
|
|
54
|
-
)
|
|
55
|
-
if not resolved:
|
|
56
|
-
return (False, "missing query")
|
|
40
|
+
) -> tuple[bool, int | str]:
|
|
57
41
|
try:
|
|
58
|
-
if
|
|
59
|
-
token_info = await self.token_client.get_token_details(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return (False, f"Token {resolved} is missing a chain id")
|
|
65
|
-
chain_id = resolved_chain_id
|
|
66
|
-
|
|
67
|
-
data = await self.wallet_client.get_token_balance_for_address(
|
|
68
|
-
wallet_address=wallet_address,
|
|
69
|
-
query=resolved,
|
|
70
|
-
chain_id=int(chain_id),
|
|
71
|
-
)
|
|
72
|
-
raw = (
|
|
73
|
-
data.get("balance_raw") or data.get("balance")
|
|
74
|
-
if isinstance(data, dict)
|
|
75
|
-
else None
|
|
76
|
-
)
|
|
77
|
-
return (True, self._parse_balance(raw))
|
|
42
|
+
if token_id and not token_address:
|
|
43
|
+
token_info = await self.token_client.get_token_details(token_id)
|
|
44
|
+
token_address = token_info["address"]
|
|
45
|
+
chain_id = chain_id or resolve_chain_id(token_info)
|
|
46
|
+
balance = await get_token_balance(token_address, chain_id, wallet_address)
|
|
47
|
+
return True, balance
|
|
78
48
|
except Exception as e:
|
|
79
|
-
return
|
|
49
|
+
return False, str(e)
|
|
50
|
+
|
|
51
|
+
async def get_vault_wallet_balance(self, token_id: str) -> tuple[bool, int | str]:
|
|
52
|
+
addr = self._wallet_address(self.config.get("strategy_wallet", {}))
|
|
53
|
+
if not addr:
|
|
54
|
+
return False, "No strategy_wallet configured"
|
|
55
|
+
return await self.get_balance(wallet_address=addr, token_id=token_id)
|
|
80
56
|
|
|
81
57
|
async def move_from_main_wallet_to_strategy_wallet(
|
|
82
58
|
self,
|
|
@@ -84,16 +60,17 @@ class BalanceAdapter(BaseAdapter):
|
|
|
84
60
|
amount: float,
|
|
85
61
|
strategy_name: str = "unknown",
|
|
86
62
|
skip_ledger: bool = False,
|
|
87
|
-
) -> tuple[bool,
|
|
63
|
+
) -> tuple[bool, str]:
|
|
88
64
|
return await self._move_between_wallets(
|
|
89
65
|
token_id=token_id,
|
|
90
66
|
amount=amount,
|
|
91
|
-
from_wallet=self.config
|
|
92
|
-
to_wallet=self.config
|
|
93
|
-
ledger_method=self.ledger_adapter.record_deposit
|
|
67
|
+
from_wallet=self.config["main_wallet"],
|
|
68
|
+
to_wallet=self.config["strategy_wallet"],
|
|
69
|
+
ledger_method=self.ledger_adapter.record_deposit
|
|
70
|
+
if not skip_ledger
|
|
71
|
+
else None,
|
|
94
72
|
ledger_wallet="to",
|
|
95
73
|
strategy_name=strategy_name,
|
|
96
|
-
skip_ledger=skip_ledger,
|
|
97
74
|
)
|
|
98
75
|
|
|
99
76
|
async def move_from_strategy_wallet_to_main_wallet(
|
|
@@ -102,55 +79,36 @@ class BalanceAdapter(BaseAdapter):
|
|
|
102
79
|
amount: float,
|
|
103
80
|
strategy_name: str = "unknown",
|
|
104
81
|
skip_ledger: bool = False,
|
|
105
|
-
) -> tuple[bool,
|
|
82
|
+
) -> tuple[bool, str]:
|
|
106
83
|
return await self._move_between_wallets(
|
|
107
84
|
token_id=token_id,
|
|
108
85
|
amount=amount,
|
|
109
|
-
from_wallet=self.config
|
|
110
|
-
to_wallet=self.config
|
|
111
|
-
ledger_method=self.ledger_adapter.record_withdrawal
|
|
86
|
+
from_wallet=self.config["strategy_wallet"],
|
|
87
|
+
to_wallet=self.config["main_wallet"],
|
|
88
|
+
ledger_method=self.ledger_adapter.record_withdrawal
|
|
89
|
+
if not skip_ledger
|
|
90
|
+
else None,
|
|
112
91
|
ledger_wallet="from",
|
|
113
92
|
strategy_name=strategy_name,
|
|
114
|
-
skip_ledger=skip_ledger,
|
|
115
93
|
)
|
|
116
94
|
|
|
117
95
|
async def send_to_address(
|
|
118
96
|
self,
|
|
119
97
|
token_id: str,
|
|
120
98
|
amount: int,
|
|
121
|
-
from_wallet: dict[str, Any]
|
|
99
|
+
from_wallet: dict[str, Any],
|
|
122
100
|
to_address: str,
|
|
123
|
-
signing_callback
|
|
124
|
-
|
|
125
|
-
) -> tuple[bool, Any]:
|
|
126
|
-
from_address = self._wallet_address(from_wallet)
|
|
127
|
-
if not from_address:
|
|
128
|
-
return False, "from_wallet missing or invalid"
|
|
129
|
-
|
|
130
|
-
if not to_address:
|
|
131
|
-
return False, "to_address is required"
|
|
132
|
-
|
|
101
|
+
signing_callback,
|
|
102
|
+
) -> tuple[bool, str]:
|
|
133
103
|
token_info = await self.token_client.get_token_details(token_id)
|
|
134
|
-
|
|
135
|
-
return False, f"Token not found: {token_id}"
|
|
136
|
-
|
|
137
|
-
chain_id = resolve_chain_id(token_info, self.logger)
|
|
138
|
-
if chain_id is None:
|
|
139
|
-
return False, f"Token {token_id} is missing chain_id"
|
|
140
|
-
|
|
141
|
-
token_address = token_info.get("address")
|
|
142
|
-
|
|
104
|
+
chain_id = resolve_chain_id(token_info)
|
|
143
105
|
tx = await build_send_transaction(
|
|
144
|
-
from_address=
|
|
106
|
+
from_address=from_wallet["address"],
|
|
145
107
|
to_address=to_address,
|
|
146
|
-
token_address=
|
|
108
|
+
token_address=token_info["address"],
|
|
147
109
|
chain_id=chain_id,
|
|
148
|
-
amount=
|
|
110
|
+
amount=amount,
|
|
149
111
|
)
|
|
150
|
-
|
|
151
|
-
if not signing_callback:
|
|
152
|
-
return False, "signing_callback is required"
|
|
153
|
-
|
|
154
112
|
tx_hash = await send_transaction(tx, signing_callback)
|
|
155
113
|
return True, tx_hash
|
|
156
114
|
|
|
@@ -159,79 +117,63 @@ class BalanceAdapter(BaseAdapter):
|
|
|
159
117
|
*,
|
|
160
118
|
token_id: str,
|
|
161
119
|
amount: float,
|
|
162
|
-
from_wallet: dict[str, Any]
|
|
163
|
-
to_wallet: dict[str, Any]
|
|
120
|
+
from_wallet: dict[str, Any],
|
|
121
|
+
to_wallet: dict[str, Any],
|
|
164
122
|
ledger_method,
|
|
165
123
|
ledger_wallet: str,
|
|
166
124
|
strategy_name: str,
|
|
167
|
-
|
|
168
|
-
) -> tuple[bool, Any]:
|
|
169
|
-
from_address = self._wallet_address(from_wallet)
|
|
170
|
-
to_address = self._wallet_address(to_wallet)
|
|
171
|
-
if not from_address or not to_address:
|
|
172
|
-
return False, "main_wallet or strategy_wallet missing"
|
|
173
|
-
|
|
125
|
+
) -> tuple[bool, str]:
|
|
174
126
|
token_info = await self.token_client.get_token_details(token_id)
|
|
175
|
-
|
|
176
|
-
return False, f"Token not found: {token_id}"
|
|
177
|
-
|
|
178
|
-
chain_id = resolve_chain_id(token_info, self.logger)
|
|
179
|
-
if chain_id is None:
|
|
180
|
-
return False, f"Token {token_id} is missing chain_id"
|
|
181
|
-
|
|
127
|
+
chain_id = resolve_chain_id(token_info)
|
|
182
128
|
decimals = token_info.get("decimals", 18)
|
|
183
129
|
raw_amount = int(amount * (10**decimals))
|
|
184
130
|
|
|
185
|
-
|
|
186
|
-
from_address=
|
|
187
|
-
to_address=
|
|
188
|
-
token_address=token_info
|
|
131
|
+
transaction = await build_send_transaction(
|
|
132
|
+
from_address=from_wallet["address"],
|
|
133
|
+
to_address=to_wallet["address"],
|
|
134
|
+
token_address=token_info["address"],
|
|
189
135
|
chain_id=chain_id,
|
|
190
136
|
amount=raw_amount,
|
|
191
137
|
)
|
|
192
|
-
broadcast_result = await self._send_tx(tx, from_address)
|
|
193
138
|
|
|
194
|
-
|
|
195
|
-
|
|
139
|
+
main_address = self.config.get("main_wallet", {}).get("address", "").lower()
|
|
140
|
+
callback = (
|
|
141
|
+
self.main_wallet_signing_callback
|
|
142
|
+
if from_wallet["address"].lower() == main_address
|
|
143
|
+
else self.strategy_wallet_signing_callback
|
|
144
|
+
)
|
|
145
|
+
tx_hash = await send_transaction(transaction, callback)
|
|
146
|
+
|
|
147
|
+
if ledger_method:
|
|
148
|
+
wallet_for_ledger = (
|
|
149
|
+
from_wallet["address"]
|
|
150
|
+
if ledger_wallet == "from"
|
|
151
|
+
else to_wallet["address"]
|
|
152
|
+
)
|
|
196
153
|
await self._record_ledger_entry(
|
|
197
|
-
ledger_method
|
|
198
|
-
wallet_address=wallet_for_ledger,
|
|
199
|
-
token_info=token_info,
|
|
200
|
-
amount=amount,
|
|
201
|
-
strategy_name=strategy_name,
|
|
154
|
+
ledger_method, wallet_for_ledger, token_info, amount, strategy_name
|
|
202
155
|
)
|
|
203
156
|
|
|
204
|
-
return
|
|
205
|
-
|
|
206
|
-
async def _send_tx(self, tx: dict[str, Any], from_address: str) -> tuple[bool, Any]:
|
|
207
|
-
main_wallet = self.config.get("main_wallet") or {}
|
|
208
|
-
main_addr = main_wallet.get("address", "").lower()
|
|
209
|
-
|
|
210
|
-
if from_address.lower() == main_addr:
|
|
211
|
-
callback = self.main_wallet_signing_callback
|
|
212
|
-
else:
|
|
213
|
-
callback = self.strategy_wallet_signing_callback
|
|
214
|
-
|
|
215
|
-
txn_hash = await send_transaction(tx, callback)
|
|
216
|
-
return True, txn_hash
|
|
157
|
+
return True, tx_hash
|
|
217
158
|
|
|
218
159
|
async def _record_ledger_entry(
|
|
219
160
|
self,
|
|
220
|
-
*,
|
|
221
161
|
ledger_method,
|
|
222
162
|
wallet_address: str,
|
|
223
163
|
token_info: dict[str, Any],
|
|
224
164
|
amount: float,
|
|
225
165
|
strategy_name: str,
|
|
226
166
|
) -> None:
|
|
227
|
-
chain_id = resolve_chain_id(token_info, self.logger)
|
|
228
|
-
if chain_id is None:
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
usd_value = await self._token_amount_usd(token_info, amount)
|
|
232
167
|
try:
|
|
233
|
-
|
|
234
|
-
|
|
168
|
+
chain_id = resolve_chain_id(token_info)
|
|
169
|
+
token_id = token_info.get("token_id")
|
|
170
|
+
usd_value = (
|
|
171
|
+
await self.token_adapter.get_amount_usd(
|
|
172
|
+
token_info.get("token_id"), amount, decimals=0
|
|
173
|
+
)
|
|
174
|
+
or 0.0
|
|
175
|
+
)
|
|
176
|
+
await ledger_method(
|
|
235
177
|
wallet_address=wallet_address,
|
|
236
178
|
chain_id=chain_id,
|
|
237
179
|
token_address=token_info.get("address"),
|
|
@@ -244,40 +186,244 @@ class BalanceAdapter(BaseAdapter):
|
|
|
244
186
|
},
|
|
245
187
|
strategy_name=strategy_name,
|
|
246
188
|
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"Ledger entry failed",
|
|
250
|
-
wallet=wallet_address,
|
|
251
|
-
token_id=token_id,
|
|
252
|
-
amount=amount,
|
|
253
|
-
error=response,
|
|
254
|
-
)
|
|
255
|
-
except Exception as exc: # noqa: BLE001
|
|
256
|
-
token_id = token_info.get("token_id") or token_info.get("id")
|
|
257
|
-
self.logger.warning(
|
|
258
|
-
f"Ledger entry raised: {exc}",
|
|
259
|
-
wallet=wallet_address,
|
|
260
|
-
token_id=token_id,
|
|
261
|
-
)
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
self.logger.warning(f"Ledger entry failed: {exc}", wallet=wallet_address)
|
|
262
191
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if not token_id:
|
|
268
|
-
return 0.0
|
|
269
|
-
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
270
|
-
if not success or not price_data:
|
|
271
|
-
return 0.0
|
|
272
|
-
return float(price_data.get("current_price", 0.0)) * float(amount)
|
|
273
|
-
|
|
274
|
-
def _wallet_address(self, wallet: dict[str, Any] | None) -> str | None:
|
|
275
|
-
if not wallet:
|
|
276
|
-
return None
|
|
277
|
-
address = wallet.get("address")
|
|
278
|
-
if address:
|
|
279
|
-
return str(address)
|
|
280
|
-
evm_wallet = wallet.get("evm") if isinstance(wallet, dict) else None
|
|
281
|
-
if isinstance(evm_wallet, dict):
|
|
282
|
-
return evm_wallet.get("address")
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _wallet_address(wallet: dict[str, Any] | None) -> str | None:
|
|
194
|
+
if wallet and isinstance(wallet, dict):
|
|
195
|
+
return wallet.get("address")
|
|
283
196
|
return None
|
|
197
|
+
|
|
198
|
+
async def get_wallet_balances_multicall(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
assets: list[dict[str, Any]],
|
|
202
|
+
wallet_address: str | None = None,
|
|
203
|
+
default_native_decimals: int = 18,
|
|
204
|
+
) -> tuple[bool, list[dict[str, Any]] | str]:
|
|
205
|
+
"""
|
|
206
|
+
Fetch many balances via Multicall3, grouped by chain.
|
|
207
|
+
|
|
208
|
+
Each asset entry supports either:
|
|
209
|
+
- {"token_address": "0x...", "chain_id": 42161}
|
|
210
|
+
- {"token_id": "usd-coin-arbitrum"} (resolved via TokenClient)
|
|
211
|
+
Native token entries can use token_address=None/"native"/ZERO_ADDRESS.
|
|
212
|
+
|
|
213
|
+
Returns a list aligned with the input `assets`, each containing:
|
|
214
|
+
- success: bool
|
|
215
|
+
- chain_id: int | None
|
|
216
|
+
- token_address: str | None
|
|
217
|
+
- token_id: str | None
|
|
218
|
+
- balance_raw: int | None
|
|
219
|
+
- decimals: int | None
|
|
220
|
+
- balance_decimal: float | None
|
|
221
|
+
- error: str | None
|
|
222
|
+
"""
|
|
223
|
+
if not assets:
|
|
224
|
+
return True, []
|
|
225
|
+
|
|
226
|
+
base_wallet = wallet_address
|
|
227
|
+
if base_wallet is None:
|
|
228
|
+
strategy_wallet = self.config.get("strategy_wallet", {})
|
|
229
|
+
base_wallet = self._wallet_address(strategy_wallet)
|
|
230
|
+
|
|
231
|
+
results: list[dict[str, Any]] = [{"success": False} for _ in assets]
|
|
232
|
+
all_success = True
|
|
233
|
+
|
|
234
|
+
normalized: list[dict[str, Any]] = []
|
|
235
|
+
for idx, asset in enumerate(assets):
|
|
236
|
+
token_id = asset.get("token_id")
|
|
237
|
+
token_address = asset.get("token_address")
|
|
238
|
+
chain_id = asset.get("chain_id")
|
|
239
|
+
req_wallet = asset.get("wallet_address") or base_wallet
|
|
240
|
+
|
|
241
|
+
if not req_wallet:
|
|
242
|
+
results[idx] = {
|
|
243
|
+
"success": False,
|
|
244
|
+
"error": "wallet_address not provided and no strategy_wallet configured",
|
|
245
|
+
"token_id": token_id,
|
|
246
|
+
"token_address": token_address,
|
|
247
|
+
"chain_id": chain_id,
|
|
248
|
+
}
|
|
249
|
+
all_success = False
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Optionally resolve missing chain/token address via TokenClient.
|
|
253
|
+
if token_id and (token_address is None or chain_id is None):
|
|
254
|
+
try:
|
|
255
|
+
token_info = await self.token_client.get_token_details(token_id)
|
|
256
|
+
except Exception as exc: # noqa: BLE001
|
|
257
|
+
token_info = None
|
|
258
|
+
self.logger.warning(
|
|
259
|
+
f"TokenClient lookup failed for {token_id}: {exc}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not token_info:
|
|
263
|
+
results[idx] = {
|
|
264
|
+
"success": False,
|
|
265
|
+
"error": f"Token not found: {token_id}",
|
|
266
|
+
"token_id": token_id,
|
|
267
|
+
"token_address": token_address,
|
|
268
|
+
"chain_id": chain_id,
|
|
269
|
+
"wallet_address": req_wallet,
|
|
270
|
+
}
|
|
271
|
+
all_success = False
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
token_address = token_address or token_info.get("address")
|
|
275
|
+
chain_id = chain_id or resolve_chain_id(token_info)
|
|
276
|
+
|
|
277
|
+
if chain_id is None:
|
|
278
|
+
results[idx] = {
|
|
279
|
+
"success": False,
|
|
280
|
+
"error": "chain_id is required",
|
|
281
|
+
"token_id": token_id,
|
|
282
|
+
"token_address": token_address,
|
|
283
|
+
"chain_id": chain_id,
|
|
284
|
+
"wallet_address": req_wallet,
|
|
285
|
+
}
|
|
286
|
+
all_success = False
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
token_addr_str = (
|
|
290
|
+
str(token_address).strip() if token_address is not None else None
|
|
291
|
+
)
|
|
292
|
+
is_native = (
|
|
293
|
+
token_addr_str is None
|
|
294
|
+
or token_addr_str == ""
|
|
295
|
+
or token_addr_str.lower() == "native"
|
|
296
|
+
or token_addr_str.lower() == ZERO_ADDRESS.lower()
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
normalized.append(
|
|
300
|
+
{
|
|
301
|
+
"index": idx,
|
|
302
|
+
"token_id": token_id,
|
|
303
|
+
"token_address": token_addr_str,
|
|
304
|
+
"chain_id": int(chain_id),
|
|
305
|
+
"wallet_address": str(req_wallet),
|
|
306
|
+
"is_native": bool(is_native),
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Group by chain id for separate multicall aggregates.
|
|
311
|
+
by_chain: dict[int, list[dict[str, Any]]] = {}
|
|
312
|
+
for entry in normalized:
|
|
313
|
+
by_chain.setdefault(entry["chain_id"], []).append(entry)
|
|
314
|
+
|
|
315
|
+
async def _process_chain(chain_id: int, entries: list[dict[str, Any]]) -> None:
|
|
316
|
+
nonlocal all_success
|
|
317
|
+
try:
|
|
318
|
+
async with web3_from_chain_id(chain_id) as w3:
|
|
319
|
+
multicall = MulticallAdapter(web3=w3, chain_id=chain_id)
|
|
320
|
+
|
|
321
|
+
# Deduplicate decimals calls per token (per chain).
|
|
322
|
+
token_set: set[str] = {
|
|
323
|
+
w3.to_checksum_address(e["token_address"])
|
|
324
|
+
for e in entries
|
|
325
|
+
if not e["is_native"] and e["token_address"]
|
|
326
|
+
}
|
|
327
|
+
sorted_tokens = sorted(token_set)
|
|
328
|
+
|
|
329
|
+
calls: list[Any] = []
|
|
330
|
+
decimals_call_index: dict[str, int] = {}
|
|
331
|
+
for token in sorted_tokens:
|
|
332
|
+
erc20 = w3.eth.contract(address=token, abi=ERC20_ABI)
|
|
333
|
+
calldata = erc20.encode_abi("decimals")
|
|
334
|
+
decimals_call_index[token] = len(calls)
|
|
335
|
+
calls.append(multicall.build_call(token, calldata))
|
|
336
|
+
|
|
337
|
+
balance_call_index: dict[int, int] = {}
|
|
338
|
+
for entry in entries:
|
|
339
|
+
if entry["is_native"]:
|
|
340
|
+
call = multicall.encode_eth_balance(entry["wallet_address"])
|
|
341
|
+
else:
|
|
342
|
+
token = w3.to_checksum_address(entry["token_address"])
|
|
343
|
+
call = multicall.encode_erc20_balance(
|
|
344
|
+
token, entry["wallet_address"]
|
|
345
|
+
)
|
|
346
|
+
balance_call_index[entry["index"]] = len(calls)
|
|
347
|
+
calls.append(call)
|
|
348
|
+
|
|
349
|
+
mc_res = await multicall.aggregate(calls)
|
|
350
|
+
|
|
351
|
+
decimals_by_token: dict[str, int] = {}
|
|
352
|
+
for token, call_idx in decimals_call_index.items():
|
|
353
|
+
raw_decimals = multicall.decode_uint256(
|
|
354
|
+
mc_res.return_data[call_idx]
|
|
355
|
+
)
|
|
356
|
+
decimals_by_token[token] = int(raw_decimals)
|
|
357
|
+
|
|
358
|
+
for entry in entries:
|
|
359
|
+
out_idx = entry["index"]
|
|
360
|
+
bal_idx = balance_call_index[out_idx]
|
|
361
|
+
raw_balance = multicall.decode_uint256(
|
|
362
|
+
mc_res.return_data[bal_idx]
|
|
363
|
+
)
|
|
364
|
+
if entry["is_native"]:
|
|
365
|
+
decimals = int(default_native_decimals)
|
|
366
|
+
token_address_out = None
|
|
367
|
+
else:
|
|
368
|
+
token = w3.to_checksum_address(entry["token_address"])
|
|
369
|
+
decimals = int(
|
|
370
|
+
decimals_by_token.get(token, default_native_decimals)
|
|
371
|
+
)
|
|
372
|
+
token_address_out = token
|
|
373
|
+
|
|
374
|
+
balance_decimal = (
|
|
375
|
+
float(raw_balance) / (10**decimals)
|
|
376
|
+
if decimals >= 0
|
|
377
|
+
else None
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
results[out_idx] = {
|
|
381
|
+
"success": True,
|
|
382
|
+
"token_id": entry.get("token_id"),
|
|
383
|
+
"token_address": token_address_out,
|
|
384
|
+
"chain_id": chain_id,
|
|
385
|
+
"wallet_address": entry["wallet_address"],
|
|
386
|
+
"balance_raw": int(raw_balance),
|
|
387
|
+
"decimals": int(decimals),
|
|
388
|
+
"balance_decimal": float(balance_decimal)
|
|
389
|
+
if balance_decimal is not None
|
|
390
|
+
else None,
|
|
391
|
+
"block_number": mc_res.block_number,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
except Exception as exc: # noqa: BLE001
|
|
395
|
+
all_success = False
|
|
396
|
+
err = str(exc)
|
|
397
|
+
for entry in entries:
|
|
398
|
+
out_idx = entry["index"]
|
|
399
|
+
results[out_idx] = {
|
|
400
|
+
"success": False,
|
|
401
|
+
"error": err,
|
|
402
|
+
"token_id": entry.get("token_id"),
|
|
403
|
+
"token_address": entry.get("token_address"),
|
|
404
|
+
"chain_id": chain_id,
|
|
405
|
+
"wallet_address": entry.get("wallet_address"),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
await asyncio.gather(
|
|
409
|
+
*[
|
|
410
|
+
_process_chain(chain_id, entries)
|
|
411
|
+
for chain_id, entries in by_chain.items()
|
|
412
|
+
]
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Ensure any leftover placeholder entries are marked failed.
|
|
416
|
+
for idx, out in enumerate(results):
|
|
417
|
+
if out.get("success") is True:
|
|
418
|
+
continue
|
|
419
|
+
if "error" not in out:
|
|
420
|
+
all_success = False
|
|
421
|
+
out.setdefault("error", "Unknown error")
|
|
422
|
+
out.setdefault("token_id", assets[idx].get("token_id"))
|
|
423
|
+
out.setdefault("token_address", assets[idx].get("token_address"))
|
|
424
|
+
out.setdefault("chain_id", assets[idx].get("chain_id"))
|
|
425
|
+
out.setdefault(
|
|
426
|
+
"wallet_address", assets[idx].get("wallet_address") or base_wallet
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return all_success, results
|