wayfinder-paths 0.1.14__py3-none-any.whl → 0.1.16__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 (58) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +19 -20
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
  4. wayfinder_paths/adapters/brap_adapter/README.md +22 -19
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
  6. wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
  7. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
  8. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
  9. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  11. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  12. wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
  15. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  16. wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
  17. wayfinder_paths/core/config.py +8 -47
  18. wayfinder_paths/core/constants/base.py +0 -1
  19. wayfinder_paths/core/constants/erc20_abi.py +13 -24
  20. wayfinder_paths/core/engine/StrategyJob.py +3 -1
  21. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  22. wayfinder_paths/core/strategies/Strategy.py +22 -4
  23. wayfinder_paths/core/utils/erc20_service.py +100 -0
  24. wayfinder_paths/core/utils/evm_helpers.py +1 -8
  25. wayfinder_paths/core/utils/transaction.py +191 -0
  26. wayfinder_paths/core/utils/web3.py +66 -0
  27. wayfinder_paths/policies/erc20.py +1 -1
  28. wayfinder_paths/run_strategy.py +42 -6
  29. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
  30. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
  31. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
  32. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
  33. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
  34. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
  35. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
  37. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
  40. wayfinder_paths/templates/adapter/README.md +1 -1
  41. wayfinder_paths/templates/strategy/README.md +1 -5
  42. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
  43. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
  44. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
  45. wayfinder_paths/abis/generic/erc20.json +0 -383
  46. wayfinder_paths/core/clients/sdk_example.py +0 -125
  47. wayfinder_paths/core/engine/__init__.py +0 -5
  48. wayfinder_paths/core/services/__init__.py +0 -0
  49. wayfinder_paths/core/services/base.py +0 -130
  50. wayfinder_paths/core/services/local_evm_txn.py +0 -334
  51. wayfinder_paths/core/services/local_token_txn.py +0 -242
  52. wayfinder_paths/core/services/web3_service.py +0 -43
  53. wayfinder_paths/core/wallets/README.md +0 -88
  54. wayfinder_paths/core/wallets/WalletManager.py +0 -56
  55. wayfinder_paths/core/wallets/__init__.py +0 -7
  56. wayfinder_paths/scripts/run_strategy.py +0 -152
  57. wayfinder_paths/strategies/config.py +0 -85
  58. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
@@ -41,7 +41,7 @@ class TestPoolAdapter:
41
41
  pool_ids=["pool-123", "pool-456"]
42
42
  )
43
43
 
44
- assert success is True
44
+ assert success
45
45
  assert data == mock_response
46
46
  mock_pool_client.get_pools_by_ids.assert_called_once_with(
47
47
  pool_ids=["pool-123", "pool-456"]
@@ -66,7 +66,7 @@ class TestPoolAdapter:
66
66
 
67
67
  success, data = await adapter.get_pools()
68
68
 
69
- assert success is True
69
+ assert success
70
70
  assert data == mock_response
71
71
 
72
72
  @pytest.mark.asyncio
@@ -33,7 +33,7 @@ class TestTokenAdapter:
33
33
  ):
34
34
  success, data = await adapter.get_token("0x1234...")
35
35
 
36
- assert success is True
36
+ assert success
37
37
  assert data == mock_token_data
38
38
 
39
39
  @pytest.mark.asyncio
@@ -55,7 +55,7 @@ class TestTokenAdapter:
55
55
  ):
56
56
  success, data = await adapter.get_token("token-123")
57
57
 
58
- assert success is True
58
+ assert success
59
59
  assert data == mock_token_data
60
60
 
61
61
  def test_adapter_type(self, adapter):
@@ -80,7 +80,7 @@ class TestTokenAdapter:
80
80
  ):
81
81
  success, data = await adapter.get_token_price("test-token")
82
82
 
83
- assert success is True
83
+ assert success
84
84
  assert data["current_price"] == 1.50
85
85
  assert data["symbol"] == "TEST"
86
86
  assert data["name"] == "Test Token"
@@ -113,7 +113,7 @@ class TestTokenAdapter:
113
113
  ):
114
114
  success, data = await adapter.get_gas_token("base")
115
115
 
116
- assert success is True
116
+ assert success
117
117
  assert data["symbol"] == "ETH"
118
118
  assert data["name"] == "Ethereum"
119
119
 
@@ -5,13 +5,6 @@ from typing import Any
5
5
 
6
6
  from loguru import logger
7
7
 
8
- from wayfinder_paths.core.constants.base import (
9
- ADAPTER_BALANCE,
10
- ADAPTER_BRAP,
11
- ADAPTER_HYPERLIQUID,
12
- ADAPTER_MOONWELL,
13
- )
14
-
15
8
 
16
9
  def _load_config_file() -> dict[str, Any]:
17
10
  path = Path("config.json")
@@ -25,6 +18,14 @@ def _load_config_file() -> dict[str, Any]:
25
18
 
26
19
 
27
20
  CONFIG = _load_config_file()
21
+ SUPPORTED_CHAINS = [
22
+ 1,
23
+ 8453,
24
+ 56,
25
+ 42161,
26
+ 137,
27
+ 999,
28
+ ]
28
29
 
29
30
 
30
31
  @dataclass
@@ -193,46 +194,6 @@ class StrategyJobConfig:
193
194
  strategy_config=strategy_config,
194
195
  )
195
196
 
196
- def get_adapter_config(self, adapter_name: str) -> dict[str, Any]:
197
- config = {
198
- "api_base_url": self.system.api_base_url,
199
- }
200
-
201
- if adapter_name in [
202
- ADAPTER_BALANCE,
203
- ADAPTER_BRAP,
204
- ADAPTER_MOONWELL,
205
- ADAPTER_HYPERLIQUID,
206
- ]:
207
- strategy_wallet = self.strategy_config.get("strategy_wallet")
208
- main_wallet = self.strategy_config.get("main_wallet")
209
- config["strategy_wallet"] = (
210
- {"address": strategy_wallet["address"]}
211
- if strategy_wallet
212
- and isinstance(strategy_wallet, dict)
213
- and strategy_wallet.get("address")
214
- else {}
215
- )
216
- config["main_wallet"] = (
217
- {"address": main_wallet["address"]}
218
- if main_wallet
219
- and isinstance(main_wallet, dict)
220
- and main_wallet.get("address")
221
- else {}
222
- )
223
- config["user_wallet"] = (
224
- config.get("strategy_wallet") or config.get("main_wallet") or {}
225
- )
226
-
227
- if adapter_name == ADAPTER_BRAP:
228
- config["default_slippage"] = self.user.default_slippage
229
- config["gas_multiplier"] = self.user.gas_multiplier
230
-
231
- if adapter_name in self.strategy_config.get("adapters", {}):
232
- config.update(self.strategy_config["adapters"][adapter_name])
233
-
234
- return config
235
-
236
197
 
237
198
  def get_api_base_url() -> str:
238
199
  system = CONFIG.get("system", {}) if isinstance(CONFIG, dict) else {}
@@ -29,7 +29,6 @@ DEFAULT_SLIPPAGE = 0.005
29
29
  # Base L2 (and some RPC providers) can occasionally take >2 minutes to index/return receipts,
30
30
  # even if the transaction is eventually mined. A longer timeout reduces false negatives that
31
31
  # can lead to unsafe retry behavior (nonce gaps, duplicate swaps, etc.).
32
- DEFAULT_TRANSACTION_TIMEOUT = 300 # Transaction receipt wait timeout
33
32
  DEFAULT_HTTP_TIMEOUT = 30.0 # HTTP client timeout
34
33
 
35
34
  # Adapter type identifiers
@@ -8,8 +8,8 @@ ERC20_ABI = [
8
8
  {
9
9
  "constant": True,
10
10
  "inputs": [
11
- {"name": "_owner", "type": "address"},
12
- {"name": "_spender", "type": "address"},
11
+ {"name": "owner", "type": "address"},
12
+ {"name": "spender", "type": "address"},
13
13
  ],
14
14
  "name": "allowance",
15
15
  "outputs": [{"name": "", "type": "uint256"}],
@@ -18,8 +18,8 @@ ERC20_ABI = [
18
18
  {
19
19
  "constant": False,
20
20
  "inputs": [
21
- {"name": "_spender", "type": "address"},
22
- {"name": "_value", "type": "uint256"},
21
+ {"name": "spender", "type": "address"},
22
+ {"name": "value", "type": "uint256"},
23
23
  ],
24
24
  "name": "approve",
25
25
  "outputs": [{"name": "", "type": "bool"}],
@@ -63,8 +63,8 @@ ERC20_ABI = [
63
63
  {
64
64
  "constant": False,
65
65
  "inputs": [
66
- {"name": "_to", "type": "address"},
67
- {"name": "_value", "type": "uint256"},
66
+ {"name": "to", "type": "address"},
67
+ {"name": "value", "type": "uint256"},
68
68
  ],
69
69
  "name": "transfer",
70
70
  "outputs": [{"name": "", "type": "bool"}],
@@ -73,9 +73,9 @@ ERC20_ABI = [
73
73
  {
74
74
  "constant": False,
75
75
  "inputs": [
76
- {"name": "_from", "type": "address"},
77
- {"name": "_to", "type": "address"},
78
- {"name": "_value", "type": "uint256"},
76
+ {"name": "from", "type": "address"},
77
+ {"name": "to", "type": "address"},
78
+ {"name": "value", "type": "uint256"},
79
79
  ],
80
80
  "name": "transferFrom",
81
81
  "outputs": [{"name": "", "type": "bool"}],
@@ -83,23 +83,12 @@ ERC20_ABI = [
83
83
  },
84
84
  ]
85
85
 
86
- # Minimal ABI for specific use cases (e.g., when you only need certain functions)
87
- ERC20_MINIMAL_ABI = [
88
- {
89
- "constant": True,
90
- "inputs": [{"name": "account", "type": "address"}],
91
- "name": "balanceOf",
92
- "outputs": [{"name": "", "type": "uint256"}],
93
- "type": "function",
94
- }
95
- ]
96
-
97
86
  ERC20_APPROVAL_ABI = [
98
87
  {
99
88
  "constant": True,
100
89
  "inputs": [
101
- {"name": "_owner", "type": "address"},
102
- {"name": "_spender", "type": "address"},
90
+ {"name": "owner", "type": "address"},
91
+ {"name": "spender", "type": "address"},
103
92
  ],
104
93
  "name": "allowance",
105
94
  "outputs": [{"name": "", "type": "uint256"}],
@@ -108,8 +97,8 @@ ERC20_APPROVAL_ABI = [
108
97
  {
109
98
  "constant": False,
110
99
  "inputs": [
111
- {"name": "_spender", "type": "address"},
112
- {"name": "_value", "type": "uint256"},
100
+ {"name": "spender", "type": "address"},
101
+ {"name": "value", "type": "uint256"},
113
102
  ],
114
103
  "name": "approve",
115
104
  "outputs": [{"name": "", "type": "bool"}],
@@ -62,7 +62,7 @@ class StrategyJob:
62
62
  await self.strategy.setup()
63
63
 
64
64
  async def execute_strategy(self, action: str, **kwargs) -> dict[str, Any]:
65
- """Execute a strategy action (deposit, withdraw, update, status, partial_liquidate)"""
65
+ """Execute a strategy action (deposit, withdraw, update, status, exit, partial_liquidate)"""
66
66
  try:
67
67
  if action == "deposit":
68
68
  result = await self.strategy.deposit(**kwargs)
@@ -72,6 +72,8 @@ class StrategyJob:
72
72
  result = await self.strategy.update()
73
73
  elif action == "status":
74
74
  result = await self.strategy.status()
75
+ elif action == "exit":
76
+ result = await self.strategy.exit(**kwargs)
75
77
  elif action == "partial_liquidate":
76
78
  usd_value = kwargs.get("usd_value")
77
79
  if usd_value is None:
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ import pytest
6
+
7
+ from wayfinder_paths.core.services.local_evm_txn import BASE_CHAIN_ID, LocalEvmTxn
8
+
9
+
10
+ class _FakeTxHash:
11
+ def __init__(self, value: str):
12
+ self._value = value
13
+
14
+ def hex(self) -> str:
15
+ return self._value
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_base_defaults_to_two_confirmations():
20
+ txn = LocalEvmTxn(config={})
21
+
22
+ fake_web3 = MagicMock()
23
+ fake_web3.eth = MagicMock()
24
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
25
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
26
+ return_value={
27
+ "status": 1,
28
+ "blockNumber": 100,
29
+ "transactionHash": "0x1",
30
+ "gasUsed": 21_000,
31
+ "logs": [],
32
+ }
33
+ )
34
+
35
+ txn.get_web3 = MagicMock(return_value=fake_web3)
36
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
37
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
38
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
39
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
40
+ txn._sign_transaction = MagicMock(return_value=b"signed")
41
+ txn._close_web3 = AsyncMock()
42
+ txn._wait_for_confirmations = AsyncMock()
43
+
44
+ ok, result = await txn.broadcast_transaction(
45
+ {
46
+ "chainId": BASE_CHAIN_ID,
47
+ "from": "0x0000000000000000000000000000000000000001",
48
+ "to": "0x0000000000000000000000000000000000000002",
49
+ "value": 0,
50
+ },
51
+ wait_for_receipt=True,
52
+ timeout=1,
53
+ )
54
+
55
+ assert ok is True
56
+ txn._wait_for_confirmations.assert_awaited_once_with(fake_web3, 100, 2)
57
+ assert result["confirmations"] == 2
58
+ assert result["confirmed_block_number"] == 102
59
+
60
+
61
+ @pytest.mark.asyncio
62
+ async def test_non_base_defaults_to_zero_confirmations():
63
+ txn = LocalEvmTxn(config={})
64
+
65
+ fake_web3 = MagicMock()
66
+ fake_web3.eth = MagicMock()
67
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
68
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
69
+ return_value={
70
+ "status": 1,
71
+ "blockNumber": 100,
72
+ "transactionHash": "0x1",
73
+ "gasUsed": 21_000,
74
+ "logs": [],
75
+ }
76
+ )
77
+
78
+ txn.get_web3 = MagicMock(return_value=fake_web3)
79
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
80
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
81
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
82
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
83
+ txn._sign_transaction = MagicMock(return_value=b"signed")
84
+ txn._close_web3 = AsyncMock()
85
+ txn._wait_for_confirmations = AsyncMock()
86
+
87
+ ok, result = await txn.broadcast_transaction(
88
+ {
89
+ "chainId": 1,
90
+ "from": "0x0000000000000000000000000000000000000001",
91
+ "to": "0x0000000000000000000000000000000000000002",
92
+ "value": 0,
93
+ },
94
+ wait_for_receipt=True,
95
+ timeout=1,
96
+ )
97
+
98
+ assert ok is True
99
+ txn._wait_for_confirmations.assert_not_awaited()
100
+ assert result["confirmations"] == 0
101
+ assert result["confirmed_block_number"] == 100
102
+
103
+
104
+ @pytest.mark.asyncio
105
+ async def test_explicit_confirmations_override_defaults():
106
+ txn = LocalEvmTxn(config={})
107
+
108
+ fake_web3 = MagicMock()
109
+ fake_web3.eth = MagicMock()
110
+ fake_web3.eth.send_raw_transaction = AsyncMock(return_value=_FakeTxHash("0x1"))
111
+ fake_web3.eth.wait_for_transaction_receipt = AsyncMock(
112
+ return_value={
113
+ "status": 1,
114
+ "blockNumber": 100,
115
+ "transactionHash": "0x1",
116
+ "gasUsed": 21_000,
117
+ "logs": [],
118
+ }
119
+ )
120
+
121
+ txn.get_web3 = MagicMock(return_value=fake_web3)
122
+ txn._validate_transaction = MagicMock(side_effect=lambda tx: tx)
123
+ txn._nonce_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
124
+ txn._gas_limit_transaction = AsyncMock(side_effect=lambda tx, _w3: tx)
125
+ txn._gas_price_transaction = AsyncMock(side_effect=lambda tx, _chain_id, _w3: tx)
126
+ txn._sign_transaction = MagicMock(return_value=b"signed")
127
+ txn._close_web3 = AsyncMock()
128
+ txn._wait_for_confirmations = AsyncMock()
129
+
130
+ ok, result = await txn.broadcast_transaction(
131
+ {
132
+ "chainId": BASE_CHAIN_ID,
133
+ "from": "0x0000000000000000000000000000000000000001",
134
+ "to": "0x0000000000000000000000000000000000000002",
135
+ "value": 0,
136
+ },
137
+ wait_for_receipt=True,
138
+ timeout=1,
139
+ confirmations=0,
140
+ )
141
+
142
+ assert ok is True
143
+ txn._wait_for_confirmations.assert_not_awaited()
144
+ assert result["confirmations"] == 0
145
+ assert result["confirmed_block_number"] == 100
@@ -8,7 +8,6 @@ from typing import Any, TypedDict
8
8
  from loguru import logger
9
9
 
10
10
  from wayfinder_paths.core.clients.TokenClient import TokenDetails
11
- from wayfinder_paths.core.services.base import Web3Service
12
11
  from wayfinder_paths.core.strategies.descriptors import StratDescriptor
13
12
 
14
13
 
@@ -56,12 +55,17 @@ class Strategy(ABC):
56
55
  *,
57
56
  main_wallet: WalletConfig | dict[str, Any] | None = None,
58
57
  strategy_wallet: WalletConfig | dict[str, Any] | None = None,
59
- web3_service: Web3Service | None = None,
58
+ api_key: str | None = None,
59
+ main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
60
+ strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
61
+ | None = None,
60
62
  ):
61
63
  self.adapters = {}
62
64
  self.ledger_adapter = None
63
65
  self.logger = logger.bind(strategy=self.__class__.__name__)
64
66
  self.config = config
67
+ self.main_wallet_signing_callback = main_wallet_signing_callback
68
+ self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
65
69
 
66
70
  async def setup(self) -> None:
67
71
  """Initialize strategy-specific setup after construction"""
@@ -143,8 +147,22 @@ class Strategy(ABC):
143
147
  @abstractmethod
144
148
  async def update(self) -> StatusTuple:
145
149
  """
146
- Update strategy positions/rebalance
147
- Returns: (success: bool, message: str)
150
+ Deploy funds to protocols (no main wallet access).
151
+ Called after deposit() has transferred assets to strategy wallet.
152
+
153
+ Returns:
154
+ Tuple of (success: bool, message: str)
155
+ """
156
+ pass
157
+
158
+ @abstractmethod
159
+ async def exit(self, **kwargs) -> StatusTuple:
160
+ """
161
+ Transfer funds from strategy wallet to main wallet.
162
+ Called after withdraw() has liquidated all positions.
163
+
164
+ Returns:
165
+ Tuple of (success: bool, message: str)
148
166
  """
149
167
  pass
150
168
 
@@ -0,0 +1,100 @@
1
+ from web3 import AsyncWeb3
2
+
3
+ from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
4
+ from wayfinder_paths.core.utils.web3 import web3_from_chain_id
5
+
6
+ NATIVE_CURRENCY_ADDRESSES: set = {
7
+ "0x0000000000000000000000000000000000000000",
8
+ "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
9
+ # TODO: This is not a proper SOL address, this short form is for LIFI only, fix this after fixing lifi
10
+ "11111111111111111111111111111111",
11
+ "0x0000000000000000000000000000000000001010",
12
+ }
13
+
14
+
15
+ async def get_token_balance(
16
+ token_address: str, chain_id: int, wallet_address: str
17
+ ) -> int:
18
+ async with web3_from_chain_id(chain_id) as web3:
19
+ checksum_wallet = AsyncWeb3.to_checksum_address(wallet_address)
20
+ if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
21
+ balance = await web3.eth.get_balance(checksum_wallet)
22
+ return int(balance)
23
+
24
+ checksum_token = AsyncWeb3.to_checksum_address(token_address)
25
+ contract = web3.eth.contract(address=checksum_token, abi=ERC20_ABI)
26
+ balance = await contract.functions.balanceOf(checksum_wallet).call(
27
+ block_identifier="pending"
28
+ )
29
+ return int(balance)
30
+
31
+
32
+ async def get_token_allowance(
33
+ token_address: str, chain_id: int, owner_address: str, spender_address: str
34
+ ):
35
+ async with web3_from_chain_id(chain_id) as web3:
36
+ contract = web3.eth.contract(
37
+ address=web3.to_checksum_address(token_address), abi=ERC20_ABI
38
+ )
39
+ return await contract.functions.allowance(
40
+ web3.to_checksum_address(owner_address),
41
+ web3.to_checksum_address(spender_address),
42
+ ).call(block_identifier="pending")
43
+
44
+
45
+ async def build_approve_transaction(
46
+ from_address: str,
47
+ chain_id: int,
48
+ token_address: str,
49
+ spender_address: str,
50
+ amount: int,
51
+ ) -> dict:
52
+ """Build an ERC20 approve transaction."""
53
+ async with web3_from_chain_id(chain_id) as web3:
54
+ contract = web3.eth.contract(
55
+ address=web3.to_checksum_address(token_address), abi=ERC20_ABI
56
+ )
57
+ data = contract.encode_abi(
58
+ "approve",
59
+ [
60
+ web3.to_checksum_address(spender_address),
61
+ amount,
62
+ ],
63
+ )
64
+ return {
65
+ "to": web3.to_checksum_address(token_address),
66
+ "from": web3.to_checksum_address(from_address),
67
+ "data": data,
68
+ "chainId": chain_id,
69
+ }
70
+
71
+
72
+ async def build_send_transaction(
73
+ from_address: str,
74
+ to_address: str,
75
+ token_address: str | None,
76
+ chain_id: int,
77
+ amount: int,
78
+ ) -> dict:
79
+ async with web3_from_chain_id(chain_id) as web3:
80
+ from_checksum = web3.to_checksum_address(from_address)
81
+ to_checksum = web3.to_checksum_address(to_address)
82
+
83
+ if not token_address or token_address.lower() in NATIVE_CURRENCY_ADDRESSES:
84
+ return {
85
+ "to": to_checksum,
86
+ "from": from_checksum,
87
+ "value": amount,
88
+ "chainId": chain_id,
89
+ }
90
+
91
+ token_checksum = web3.to_checksum_address(token_address)
92
+ contract = web3.eth.contract(address=token_checksum, abi=ERC20_ABI)
93
+ data = contract.encode_abi("transfer", [to_checksum, amount])
94
+
95
+ return {
96
+ "to": token_checksum,
97
+ "from": from_checksum,
98
+ "data": data,
99
+ "chainId": chain_id,
100
+ }
@@ -2,12 +2,11 @@
2
2
  EVM helper utilities for common blockchain operations.
3
3
 
4
4
  This module provides reusable functions for EVM-related operations that are shared
5
- across multiple adapters, extracted from evm_transaction_adapter.
5
+ across multiple adapters.
6
6
  """
7
7
 
8
8
  import json
9
9
  import os
10
- from pathlib import Path
11
10
  from typing import Any
12
11
 
13
12
  from loguru import logger
@@ -170,9 +169,3 @@ async def get_abi_filtered(
170
169
  if item.get("type") == "function" and item.get("name") in function_names
171
170
  ]
172
171
  return filtered_abi
173
-
174
-
175
- with open(Path(__file__).parent.parent.parent.joinpath("abis/generic/erc20.json")) as f:
176
- erc20_abi_raw = f.read()
177
-
178
- ERC20_ABI = json.loads(erc20_abi_raw)