langchain-erc20 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 conrad.japhet@gmail.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ # Without this file, setuptools' sdist falls back to legacy distutils
2
+ # defaults that partially/incorrectly grab tests/test*.py (but not
3
+ # conftest.py, web3_mocks.py, or tests/__init__.py). Tests aren't part of
4
+ # the installed package, so prune the whole directory instead.
5
+ #
6
+ # (setuptools also unconditionally writes a minimal setup.cfg into every
7
+ # sdist for python-setup.py-based tooling backward-compat, even though this
8
+ # project has none -- that's expected/harmless and not something MANIFEST.in
9
+ # controls.)
10
+ prune tests
11
+ prune scripts
@@ -0,0 +1,275 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-erc20
3
+ Version: 0.1.0
4
+ Summary: LangChain tools for ERC20 primitives: balances, allowances, transfers, approvals, and native wrap/unwrap, as execution plans an EOA can sign or a smart-contract wallet can batch.
5
+ Author-email: conrad.japhet@gmail.com
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/Conrad-sudo/langchain-erc20
8
+ Project-URL: Issues, https://github.com/Conrad-sudo/langchain-erc20/issues
9
+ Keywords: langchain,erc20,web3,ethereum,evm,tokens,agent,tools,erc4337,smart-wallet
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: web3>=7.0
22
+ Requires-Dist: langchain-core>=0.3
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Requires-Dist: ruff==0.16.1; extra == "dev"
26
+ Requires-Dist: pyright==1.1.411; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # langchain-erc20
30
+
31
+ LangChain tools for ERC-20 primitives: balances, allowances, transfers, approvals, and native
32
+ wrap/unwrap, as execution plans an EOA can sign or a smart-contract wallet can batch.
33
+
34
+ > **Status: pre-release (0.1.0, in development).** Not yet on PyPI.
35
+
36
+ ## What it is
37
+
38
+ A standalone toolkit for the ERC-20 surface and its real-world variants, usable by EOAs and by
39
+ any smart-contract wallet (ERC-4337, ERC-7579, ERC-6900, Safe, or bespoke).
40
+
41
+ Every write tool returns an ordered **execution plan** rather than a bare transaction, because
42
+ `(to, value, data)` is the last point at which every account type still agrees:
43
+
44
+ - an EOA transaction is that plus nonce, gas and fees
45
+ - an ERC-7579 `Execution` is exactly that
46
+ - a Safe `MultiSend` entry is that plus an operation byte
47
+ - an ERC-4337 UserOp wraps a batch of them in the account's own `callData`
48
+
49
+ ## What it is not
50
+
51
+ - **Not a DEX.** No routers, pools, quotes or price logic — that is
52
+ [langchain-uniswap-v2](https://github.com/Conrad-sudo/langchain-uniswap-v2).
53
+ - **It never signs, holds keys, or broadcasts.** That is the consumer's job.
54
+ - **It ships no bundled token registry.** A wrong address in one is a silent, unrecoverable loss
55
+ of funds, and keeping such a table correct across chains, bridged variants and redeployments is
56
+ a full-time job. You pass your own.
57
+ - **No token discovery or approval auditing.** Both need an indexer, not an RPC.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install langchain-erc20
63
+ ```
64
+
65
+ ## Quick start
66
+
67
+ Both modes are shown together deliberately: this package serves both, and an EOA-only example
68
+ would give the wrong impression.
69
+
70
+ ### EOA
71
+
72
+ ```python
73
+ from langchain_erc20 import ERC20Toolkit
74
+
75
+ toolkit = ERC20Toolkit.for_chain(
76
+ 1,
77
+ rpc_url="https://your-node",
78
+ tokens={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
79
+ )
80
+ tools = {t.name: t for t in toolkit.get_tools()}
81
+
82
+ plan = tools["transfer"].invoke(
83
+ {
84
+ "token": "usdc",
85
+ "to": "0x...recipient",
86
+ "from_address": "0x...your_eoa",
87
+ "amount": "25.5",
88
+ }
89
+ )
90
+
91
+ for tx in plan["transactions"]:
92
+ tx.pop("gas_estimated") # metadata, not a transaction field
93
+ signed = account.sign_transaction(tx)
94
+ w3.eth.send_raw_transaction(signed.raw_transaction)
95
+ ```
96
+
97
+ ### Smart-contract wallet
98
+
99
+ ```python
100
+ toolkit = ERC20Toolkit.for_chain(1, rpc_url="https://your-node", tx_mode="calls")
101
+ tools = {t.name: t for t in toolkit.get_tools()}
102
+
103
+ plan = tools["approve"].invoke(
104
+ {
105
+ "token": "0x...token",
106
+ "spender": "0x...spender",
107
+ "from_address": "0x...your_smart_account",
108
+ "amount": "100",
109
+ }
110
+ )
111
+
112
+ executions = [(c["to"], c["value"], bytes.fromhex(c["data"][2:])) for c in plan["calls"]]
113
+ send_batch_user_op(executions) # one atomic transaction
114
+ ```
115
+
116
+ ## Execution modes
117
+
118
+ | | `tx_mode="eoa"` (default) | `tx_mode="calls"` |
119
+ |---|---|---|
120
+ | `calls` | populated | populated |
121
+ | `transactions` | signable, sequential nonces | `None` |
122
+ | Nonce / gas / fee RPC calls | yes | **zero** |
123
+
124
+ `calls` mode makes no `eth_getTransactionCount` and no `eth_estimateGas` on purpose. A 4337 nonce
125
+ is `EntryPoint.getNonce(sender, key)`, a 2D nonce unrelated to an EOA transaction count; and
126
+ `eth_estimateGas` with `from` set to a smart account simulates the account calling itself as an
127
+ EOA, which is not how the EntryPoint invokes it, so the number is wrong even when it succeeds.
128
+
129
+ ### The plan shape
130
+
131
+ ```python
132
+ {
133
+ "calls": [
134
+ {
135
+ "to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
136
+ "value": 0,
137
+ "data": "0x095ea7b3...",
138
+ "role": "approve",
139
+ "description": "Set USDT allowance for 0x7a25... to 0",
140
+ },
141
+ ],
142
+ "transactions": [...], # or None in calls mode
143
+ "chain_id": 1,
144
+ "summary": {...}, # whole-unit amounts, safe to show a user
145
+ }
146
+ ```
147
+
148
+ `role` is `approve`, `approve_reset`, or `action`. Roles let a consumer validate a plan before
149
+ submitting it — for example a wallet with a spending-limit hook checking that no approval is left
150
+ standing — and let a UI describe it.
151
+
152
+ `data` is a hex string rather than bytes so plans stay JSON-serialisable: these are LangChain tool
153
+ returns and must survive being written into an agent transcript.
154
+
155
+ ## Tools
156
+
157
+ ### Read
158
+
159
+ | Tool | Returns |
160
+ |---|---|
161
+ | `get_token_metadata(token)` | `address, name, symbol, decimals, total_supply, total_supply_base` |
162
+ | `get_balance(token, owner)` | `amount, amount_base, decimals, symbol` |
163
+ | `get_native_balance(owner)` | `amount, amount_base, symbol` |
164
+ | `get_allowance(token, owner, spender)` | `amount, amount_base, is_unlimited` |
165
+ | `is_balance_sufficient(token, owner, amount)` | `is_sufficient, balance, required, shortfall` |
166
+ | `is_allowance_sufficient(token, owner, spender, amount)` | as above, for the allowance |
167
+ | `supports_permit(token)` | `supported, standard` — `eip2612`, `dai`, `unknown` or `null` |
168
+
169
+ ### Write — all return plans
170
+
171
+ | Tool | Plan |
172
+ |---|---|
173
+ | `transfer(token, to, from_address, amount)` | `[action]` |
174
+ | `transfer_all(token, to, from_address)` | `[action]`, balance read at build time |
175
+ | `transfer_from(token, owner, to, from_address, amount)` | `[action]` |
176
+ | `batch_transfer(token, transfers, from_address)` | `[action × N]` |
177
+ | `approve(token, spender, from_address, amount / unlimited)` | `[approve]` or `[approve_reset, approve]` |
178
+ | `revoke_approval(token, spender, from_address)` | `[approve_reset]` |
179
+ | `wrap_native(from_address, amount)` | `[action]`, amount carried as `value` |
180
+ | `unwrap_native(from_address, amount)` | `[action]` |
181
+
182
+ Every write tool also accepts `amount_base` for exact base units, and `nonce` to set the starting
183
+ nonce in EOA mode.
184
+
185
+ ### Amounts
186
+
187
+ `amount` accepts a float or a string. **Prefer strings** for large or precise values: a float
188
+ cannot represent 18 decimal places, and `0.1 + 0.2` is the cheapest possible way to send the wrong
189
+ amount. Conversion truncates toward zero and never rounds up, since rounding up can overspend or
190
+ exceed an allowance; when truncation loses precision the summary says `amount_truncated: true`.
191
+
192
+ Reads return both `amount` and `amount_base` so a consumer building its own calldata never has to
193
+ re-derive decimals.
194
+
195
+ ## Token compatibility
196
+
197
+ The ERC-20 standard is, in practice, a suggestion. Each of the following is a real token that
198
+ breaks a naive implementation, and each is handled here and covered by tests in
199
+ `tests/test_compat.py`.
200
+
201
+ | Reality | Token | How this package handles it |
202
+ |---|---|---|
203
+ | `transfer`/`approve` return nothing | USDT, BNB, OMG | Write functions are only ever *encoded*, never called, so empty returndata is never decoded. `ERC20_NO_RETURN_ABI` is exported for consumers who dry-run writes themselves. |
204
+ | `approve` reverts while an allowance stands | USDT | `zero_first_approvals="auto"` reads the allowance and emits `[approve_reset, approve]` when needed. Also closes the generic front-running window. |
205
+ | `name`/`symbol` return `bytes32` | MKR, most pre-2018 tokens | Retried against a `bytes32` ABI, null-stripped and UTF-8 decoded. Unreadable metadata becomes `null` rather than failing the call. |
206
+ | `decimals()` absent | a few | **Never assumed to be 18.** Raises and points at `decimals_overrides`, because guessing 18 on a 6-decimal token sends 10¹² times the intended amount. |
207
+ | Allowance stored in fewer bits | UNI (96 bits) | `unlimited=True` encodes exactly `2**256 - 1`; anything above the uint256 ceiling raises before encoding. Some tokens reject even the maximum and need a concrete amount. |
208
+ | Fee-on-transfer | SafeMoon-likes | Summaries say `amount_sent`, never `amount_received` — the package cannot know how much arrives. |
209
+ | Rebasing | stETH, AMPL | `transfer_all` records `balance_read_at_block` and warns its fixed amount can go stale. |
210
+ | Blocklists, pauses, ERC-777 hooks | USDC, USDT, many | **Not detectable in advance.** See the warning below. |
211
+
212
+ > **Preflight is not a guarantee.** With `preflight=True` (the default) every write tool checks
213
+ > balances and allowances before building, and raises naming the exact shortfall. It cannot see
214
+ > blocklists, pauses, transfer hooks or reentrancy. A passing preflight means the transfer is not
215
+ > obviously impossible — not that it will succeed.
216
+
217
+ ## ERC-4337
218
+
219
+ This package owns exactly one step:
220
+
221
+ | Step | Owner |
222
+ |---|---|
223
+ | 1. Decide the calls | **this package** — `plan["calls"]` |
224
+ | 2. Encode into the account's `callData` | consumer (account-specific) |
225
+ | 3. Fill the UserOp (sender, 2D nonce, initCode, paymaster) | consumer / AA SDK |
226
+ | 4. Estimate gas | bundler (`eth_estimateUserOperationGas`) |
227
+ | 5. Sign `userOpHash` | consumer's signer |
228
+ | 6. Submit and poll | bundler |
229
+
230
+ Step 2 is account-specific: ERC-4337 standardises the UserOperation struct and the EntryPoint, not
231
+ the account's execute interface. ERC-7579, ERC-6900, Safe, Kernel and LightAccount each differ.
232
+
233
+ Counterfactual (not-yet-deployed) accounts work: the address is deterministic and can hold tokens
234
+ before deployment, so balance reads succeed and nothing in `calls` mode requires code at the
235
+ address.
236
+
237
+ Note **permit is effectively EOA-only.** EIP-2612 verifies with `ecrecover`, which cannot validate
238
+ a smart-contract signature. Smart accounts should batch `[approve, action]` atomically instead,
239
+ which achieves the same thing without a signature.
240
+
241
+ ## Supported chains for wrapped-native
242
+
243
+ `for_chain(chain_id)` supplies a wrapped-native address and a public RPC for: Ethereum (1),
244
+ Sepolia (11155111), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161) and
245
+ Avalanche (43114).
246
+
247
+ Every address is verified against the live chain by `scripts/verify_networks.py`, which checks
248
+ bytecode, `symbol()`, `decimals()` and the presence of `deposit()`/`withdraw(uint256)`, following
249
+ EIP-1967 proxies where needed. Public RPCs are rate-limited; pass your own for production.
250
+
251
+ Celo is deliberately absent: CELO is natively an ERC-20 with no wrapping step, so `wrap_native`
252
+ has no meaning there and `for_chain(42220)` fails loudly rather than guessing.
253
+
254
+ Everything except `wrap_native`/`unwrap_native` works on any EVM chain via the main constructor.
255
+
256
+ ## Development
257
+
258
+ ```bash
259
+ python3 -m venv .venv
260
+ .venv/bin/pip install -e ".[dev]"
261
+ .venv/bin/ruff check .
262
+ .venv/bin/ruff format --check .
263
+ .venv/bin/pyright --pythonpath .venv/bin/python
264
+ .venv/bin/python -m pytest -q
265
+ ```
266
+
267
+ Re-verify the wrapped-native addresses against live chains:
268
+
269
+ ```bash
270
+ .venv/bin/python scripts/verify_networks.py
271
+ ```
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,247 @@
1
+ # langchain-erc20
2
+
3
+ LangChain tools for ERC-20 primitives: balances, allowances, transfers, approvals, and native
4
+ wrap/unwrap, as execution plans an EOA can sign or a smart-contract wallet can batch.
5
+
6
+ > **Status: pre-release (0.1.0, in development).** Not yet on PyPI.
7
+
8
+ ## What it is
9
+
10
+ A standalone toolkit for the ERC-20 surface and its real-world variants, usable by EOAs and by
11
+ any smart-contract wallet (ERC-4337, ERC-7579, ERC-6900, Safe, or bespoke).
12
+
13
+ Every write tool returns an ordered **execution plan** rather than a bare transaction, because
14
+ `(to, value, data)` is the last point at which every account type still agrees:
15
+
16
+ - an EOA transaction is that plus nonce, gas and fees
17
+ - an ERC-7579 `Execution` is exactly that
18
+ - a Safe `MultiSend` entry is that plus an operation byte
19
+ - an ERC-4337 UserOp wraps a batch of them in the account's own `callData`
20
+
21
+ ## What it is not
22
+
23
+ - **Not a DEX.** No routers, pools, quotes or price logic — that is
24
+ [langchain-uniswap-v2](https://github.com/Conrad-sudo/langchain-uniswap-v2).
25
+ - **It never signs, holds keys, or broadcasts.** That is the consumer's job.
26
+ - **It ships no bundled token registry.** A wrong address in one is a silent, unrecoverable loss
27
+ of funds, and keeping such a table correct across chains, bridged variants and redeployments is
28
+ a full-time job. You pass your own.
29
+ - **No token discovery or approval auditing.** Both need an indexer, not an RPC.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install langchain-erc20
35
+ ```
36
+
37
+ ## Quick start
38
+
39
+ Both modes are shown together deliberately: this package serves both, and an EOA-only example
40
+ would give the wrong impression.
41
+
42
+ ### EOA
43
+
44
+ ```python
45
+ from langchain_erc20 import ERC20Toolkit
46
+
47
+ toolkit = ERC20Toolkit.for_chain(
48
+ 1,
49
+ rpc_url="https://your-node",
50
+ tokens={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
51
+ )
52
+ tools = {t.name: t for t in toolkit.get_tools()}
53
+
54
+ plan = tools["transfer"].invoke(
55
+ {
56
+ "token": "usdc",
57
+ "to": "0x...recipient",
58
+ "from_address": "0x...your_eoa",
59
+ "amount": "25.5",
60
+ }
61
+ )
62
+
63
+ for tx in plan["transactions"]:
64
+ tx.pop("gas_estimated") # metadata, not a transaction field
65
+ signed = account.sign_transaction(tx)
66
+ w3.eth.send_raw_transaction(signed.raw_transaction)
67
+ ```
68
+
69
+ ### Smart-contract wallet
70
+
71
+ ```python
72
+ toolkit = ERC20Toolkit.for_chain(1, rpc_url="https://your-node", tx_mode="calls")
73
+ tools = {t.name: t for t in toolkit.get_tools()}
74
+
75
+ plan = tools["approve"].invoke(
76
+ {
77
+ "token": "0x...token",
78
+ "spender": "0x...spender",
79
+ "from_address": "0x...your_smart_account",
80
+ "amount": "100",
81
+ }
82
+ )
83
+
84
+ executions = [(c["to"], c["value"], bytes.fromhex(c["data"][2:])) for c in plan["calls"]]
85
+ send_batch_user_op(executions) # one atomic transaction
86
+ ```
87
+
88
+ ## Execution modes
89
+
90
+ | | `tx_mode="eoa"` (default) | `tx_mode="calls"` |
91
+ |---|---|---|
92
+ | `calls` | populated | populated |
93
+ | `transactions` | signable, sequential nonces | `None` |
94
+ | Nonce / gas / fee RPC calls | yes | **zero** |
95
+
96
+ `calls` mode makes no `eth_getTransactionCount` and no `eth_estimateGas` on purpose. A 4337 nonce
97
+ is `EntryPoint.getNonce(sender, key)`, a 2D nonce unrelated to an EOA transaction count; and
98
+ `eth_estimateGas` with `from` set to a smart account simulates the account calling itself as an
99
+ EOA, which is not how the EntryPoint invokes it, so the number is wrong even when it succeeds.
100
+
101
+ ### The plan shape
102
+
103
+ ```python
104
+ {
105
+ "calls": [
106
+ {
107
+ "to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
108
+ "value": 0,
109
+ "data": "0x095ea7b3...",
110
+ "role": "approve",
111
+ "description": "Set USDT allowance for 0x7a25... to 0",
112
+ },
113
+ ],
114
+ "transactions": [...], # or None in calls mode
115
+ "chain_id": 1,
116
+ "summary": {...}, # whole-unit amounts, safe to show a user
117
+ }
118
+ ```
119
+
120
+ `role` is `approve`, `approve_reset`, or `action`. Roles let a consumer validate a plan before
121
+ submitting it — for example a wallet with a spending-limit hook checking that no approval is left
122
+ standing — and let a UI describe it.
123
+
124
+ `data` is a hex string rather than bytes so plans stay JSON-serialisable: these are LangChain tool
125
+ returns and must survive being written into an agent transcript.
126
+
127
+ ## Tools
128
+
129
+ ### Read
130
+
131
+ | Tool | Returns |
132
+ |---|---|
133
+ | `get_token_metadata(token)` | `address, name, symbol, decimals, total_supply, total_supply_base` |
134
+ | `get_balance(token, owner)` | `amount, amount_base, decimals, symbol` |
135
+ | `get_native_balance(owner)` | `amount, amount_base, symbol` |
136
+ | `get_allowance(token, owner, spender)` | `amount, amount_base, is_unlimited` |
137
+ | `is_balance_sufficient(token, owner, amount)` | `is_sufficient, balance, required, shortfall` |
138
+ | `is_allowance_sufficient(token, owner, spender, amount)` | as above, for the allowance |
139
+ | `supports_permit(token)` | `supported, standard` — `eip2612`, `dai`, `unknown` or `null` |
140
+
141
+ ### Write — all return plans
142
+
143
+ | Tool | Plan |
144
+ |---|---|
145
+ | `transfer(token, to, from_address, amount)` | `[action]` |
146
+ | `transfer_all(token, to, from_address)` | `[action]`, balance read at build time |
147
+ | `transfer_from(token, owner, to, from_address, amount)` | `[action]` |
148
+ | `batch_transfer(token, transfers, from_address)` | `[action × N]` |
149
+ | `approve(token, spender, from_address, amount / unlimited)` | `[approve]` or `[approve_reset, approve]` |
150
+ | `revoke_approval(token, spender, from_address)` | `[approve_reset]` |
151
+ | `wrap_native(from_address, amount)` | `[action]`, amount carried as `value` |
152
+ | `unwrap_native(from_address, amount)` | `[action]` |
153
+
154
+ Every write tool also accepts `amount_base` for exact base units, and `nonce` to set the starting
155
+ nonce in EOA mode.
156
+
157
+ ### Amounts
158
+
159
+ `amount` accepts a float or a string. **Prefer strings** for large or precise values: a float
160
+ cannot represent 18 decimal places, and `0.1 + 0.2` is the cheapest possible way to send the wrong
161
+ amount. Conversion truncates toward zero and never rounds up, since rounding up can overspend or
162
+ exceed an allowance; when truncation loses precision the summary says `amount_truncated: true`.
163
+
164
+ Reads return both `amount` and `amount_base` so a consumer building its own calldata never has to
165
+ re-derive decimals.
166
+
167
+ ## Token compatibility
168
+
169
+ The ERC-20 standard is, in practice, a suggestion. Each of the following is a real token that
170
+ breaks a naive implementation, and each is handled here and covered by tests in
171
+ `tests/test_compat.py`.
172
+
173
+ | Reality | Token | How this package handles it |
174
+ |---|---|---|
175
+ | `transfer`/`approve` return nothing | USDT, BNB, OMG | Write functions are only ever *encoded*, never called, so empty returndata is never decoded. `ERC20_NO_RETURN_ABI` is exported for consumers who dry-run writes themselves. |
176
+ | `approve` reverts while an allowance stands | USDT | `zero_first_approvals="auto"` reads the allowance and emits `[approve_reset, approve]` when needed. Also closes the generic front-running window. |
177
+ | `name`/`symbol` return `bytes32` | MKR, most pre-2018 tokens | Retried against a `bytes32` ABI, null-stripped and UTF-8 decoded. Unreadable metadata becomes `null` rather than failing the call. |
178
+ | `decimals()` absent | a few | **Never assumed to be 18.** Raises and points at `decimals_overrides`, because guessing 18 on a 6-decimal token sends 10¹² times the intended amount. |
179
+ | Allowance stored in fewer bits | UNI (96 bits) | `unlimited=True` encodes exactly `2**256 - 1`; anything above the uint256 ceiling raises before encoding. Some tokens reject even the maximum and need a concrete amount. |
180
+ | Fee-on-transfer | SafeMoon-likes | Summaries say `amount_sent`, never `amount_received` — the package cannot know how much arrives. |
181
+ | Rebasing | stETH, AMPL | `transfer_all` records `balance_read_at_block` and warns its fixed amount can go stale. |
182
+ | Blocklists, pauses, ERC-777 hooks | USDC, USDT, many | **Not detectable in advance.** See the warning below. |
183
+
184
+ > **Preflight is not a guarantee.** With `preflight=True` (the default) every write tool checks
185
+ > balances and allowances before building, and raises naming the exact shortfall. It cannot see
186
+ > blocklists, pauses, transfer hooks or reentrancy. A passing preflight means the transfer is not
187
+ > obviously impossible — not that it will succeed.
188
+
189
+ ## ERC-4337
190
+
191
+ This package owns exactly one step:
192
+
193
+ | Step | Owner |
194
+ |---|---|
195
+ | 1. Decide the calls | **this package** — `plan["calls"]` |
196
+ | 2. Encode into the account's `callData` | consumer (account-specific) |
197
+ | 3. Fill the UserOp (sender, 2D nonce, initCode, paymaster) | consumer / AA SDK |
198
+ | 4. Estimate gas | bundler (`eth_estimateUserOperationGas`) |
199
+ | 5. Sign `userOpHash` | consumer's signer |
200
+ | 6. Submit and poll | bundler |
201
+
202
+ Step 2 is account-specific: ERC-4337 standardises the UserOperation struct and the EntryPoint, not
203
+ the account's execute interface. ERC-7579, ERC-6900, Safe, Kernel and LightAccount each differ.
204
+
205
+ Counterfactual (not-yet-deployed) accounts work: the address is deterministic and can hold tokens
206
+ before deployment, so balance reads succeed and nothing in `calls` mode requires code at the
207
+ address.
208
+
209
+ Note **permit is effectively EOA-only.** EIP-2612 verifies with `ecrecover`, which cannot validate
210
+ a smart-contract signature. Smart accounts should batch `[approve, action]` atomically instead,
211
+ which achieves the same thing without a signature.
212
+
213
+ ## Supported chains for wrapped-native
214
+
215
+ `for_chain(chain_id)` supplies a wrapped-native address and a public RPC for: Ethereum (1),
216
+ Sepolia (11155111), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161) and
217
+ Avalanche (43114).
218
+
219
+ Every address is verified against the live chain by `scripts/verify_networks.py`, which checks
220
+ bytecode, `symbol()`, `decimals()` and the presence of `deposit()`/`withdraw(uint256)`, following
221
+ EIP-1967 proxies where needed. Public RPCs are rate-limited; pass your own for production.
222
+
223
+ Celo is deliberately absent: CELO is natively an ERC-20 with no wrapping step, so `wrap_native`
224
+ has no meaning there and `for_chain(42220)` fails loudly rather than guessing.
225
+
226
+ Everything except `wrap_native`/`unwrap_native` works on any EVM chain via the main constructor.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ python3 -m venv .venv
232
+ .venv/bin/pip install -e ".[dev]"
233
+ .venv/bin/ruff check .
234
+ .venv/bin/ruff format --check .
235
+ .venv/bin/pyright --pythonpath .venv/bin/python
236
+ .venv/bin/python -m pytest -q
237
+ ```
238
+
239
+ Re-verify the wrapped-native addresses against live chains:
240
+
241
+ ```bash
242
+ .venv/bin/python scripts/verify_networks.py
243
+ ```
244
+
245
+ ## License
246
+
247
+ MIT
@@ -0,0 +1,26 @@
1
+ from .abis import (
2
+ DAI_PERMIT_ABI,
3
+ ERC20_ABI,
4
+ ERC20_BYTES32_ABI,
5
+ ERC20_NO_RETURN_ABI,
6
+ ERC20_PERMIT_ABI,
7
+ WETH9_ABI,
8
+ )
9
+ from .networks import KNOWN_NETWORKS
10
+ from .plans import DEFAULT_GAS
11
+ from .toolkit import ERC20Toolkit
12
+
13
+ __all__ = [
14
+ "ERC20Toolkit",
15
+ "KNOWN_NETWORKS",
16
+ "DEFAULT_GAS",
17
+ # Exported for consumers building or simulating their own calls. In
18
+ # particular ERC20_NO_RETURN_ABI is what a dry-run against USDT needs;
19
+ # this package never calls a write function, so it does not need it itself.
20
+ "ERC20_ABI",
21
+ "ERC20_NO_RETURN_ABI",
22
+ "ERC20_BYTES32_ABI",
23
+ "ERC20_PERMIT_ABI",
24
+ "DAI_PERMIT_ABI",
25
+ "WETH9_ABI",
26
+ ]