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.
- langchain_erc20-0.1.0/LICENSE +21 -0
- langchain_erc20-0.1.0/MANIFEST.in +11 -0
- langchain_erc20-0.1.0/PKG-INFO +275 -0
- langchain_erc20-0.1.0/README.md +247 -0
- langchain_erc20-0.1.0/langchain_erc20/__init__.py +26 -0
- langchain_erc20-0.1.0/langchain_erc20/abis.py +302 -0
- langchain_erc20-0.1.0/langchain_erc20/amounts.py +141 -0
- langchain_erc20-0.1.0/langchain_erc20/networks.py +86 -0
- langchain_erc20-0.1.0/langchain_erc20/plans.py +256 -0
- langchain_erc20-0.1.0/langchain_erc20/tokens.py +167 -0
- langchain_erc20-0.1.0/langchain_erc20/toolkit.py +1448 -0
- langchain_erc20-0.1.0/langchain_erc20.egg-info/PKG-INFO +275 -0
- langchain_erc20-0.1.0/langchain_erc20.egg-info/SOURCES.txt +16 -0
- langchain_erc20-0.1.0/langchain_erc20.egg-info/dependency_links.txt +1 -0
- langchain_erc20-0.1.0/langchain_erc20.egg-info/requires.txt +7 -0
- langchain_erc20-0.1.0/langchain_erc20.egg-info/top_level.txt +1 -0
- langchain_erc20-0.1.0/pyproject.toml +71 -0
- langchain_erc20-0.1.0/setup.cfg +4 -0
|
@@ -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
|
+
]
|