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.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
@@ -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
  ]
@@ -93,7 +93,6 @@ config = {
93
93
 
94
94
  ## Dependencies
95
95
 
96
- - `WalletClient` - For balance queries
97
96
  - `TokenClient` - For token metadata
98
97
  - `LedgerAdapter` - For transaction recording
99
98
  - `TokenAdapter` - For price lookups
@@ -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.clients.WalletClient import WalletClient
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, str | int]:
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 chain_id is None:
59
- token_info = await self.token_client.get_token_details(resolved)
60
- if not token_info:
61
- return (False, f"Token not found: {resolved}")
62
- resolved_chain_id = resolve_chain_id(token_info, self.logger)
63
- if resolved_chain_id is None:
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 (False, str(e))
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, Any]:
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.get("main_wallet"),
92
- to_wallet=self.config.get("strategy_wallet"),
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, Any]:
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.get("strategy_wallet"),
110
- to_wallet=self.config.get("main_wallet"),
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] | None,
99
+ from_wallet: dict[str, Any],
122
100
  to_address: str,
123
- signing_callback=None,
124
- skip_ledger: bool = True,
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
- if not token_info:
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=from_address,
106
+ from_address=from_wallet["address"],
145
107
  to_address=to_address,
146
- token_address=token_address,
108
+ token_address=token_info["address"],
147
109
  chain_id=chain_id,
148
- amount=int(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] | None,
163
- to_wallet: dict[str, Any] | None,
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
- skip_ledger: bool,
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
- if not token_info:
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
- tx = await build_send_transaction(
186
- from_address=from_address,
187
- to_address=to_address,
188
- token_address=token_info.get("address"),
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
- if broadcast_result[0] and not skip_ledger and ledger_method is not None:
195
- wallet_for_ledger = from_address if ledger_wallet == "from" else to_address
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=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 broadcast_result
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
- token_id = token_info.get("token_id") or token_info.get("id")
234
- success, response = await ledger_method(
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
- if not success:
248
- self.logger.warning(
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
- async def _token_amount_usd(
264
- self, token_info: dict[str, Any], amount: float
265
- ) -> float:
266
- token_id = token_info.get("token_id")
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
@@ -0,0 +1,8 @@
1
+ schema_version: "0.1"
2
+ entrypoint: "adapters.balance_adapter.adapter.BalanceAdapter"
3
+ capabilities:
4
+ - "balance.read"
5
+ - "transfer.main_to_strategy"
6
+ - "transfer.strategy_to_main"
7
+ - "transfer.send"
8
+ dependencies: []