langchain-erc20
LangChain tools for ERC-20 primitives: balances, allowances, transfers, approvals, and native wrap/unwrap, as execution plans an EOA can sign or a smart-contract wallet can batch.
Status: pre-release (0.1.0, in development). Not yet on PyPI.
What it is
A standalone toolkit for the ERC-20 surface and its real-world variants, usable by EOAs and by any smart-contract wallet (ERC-4337, ERC-7579, ERC-6900, Safe, or bespoke).
Every write tool returns an ordered execution plan rather than a bare transaction, because
(to, value, data) is the last point at which every account type still agrees:
- an EOA transaction is that plus nonce, gas and fees
- an ERC-7579
Executionis exactly that - a Safe
MultiSendentry is that plus an operation byte - an ERC-4337 UserOp wraps a batch of them in the account's own
callData
What it is not
- Not a DEX. No routers, pools, quotes or price logic — that is langchain-uniswap-v2.
- It never signs, holds keys, or broadcasts. That is the consumer's job.
- It ships no bundled token registry. A wrong address in one is a silent, unrecoverable loss of funds, and keeping such a table correct across chains, bridged variants and redeployments is a full-time job. You pass your own.
- No token discovery or approval auditing. Both need an indexer, not an RPC.
Install
pip install langchain-erc20
Quick start
Both modes are shown together deliberately: this package serves both, and an EOA-only example would give the wrong impression.
EOA
from langchain_erc20 import ERC20Toolkit
toolkit = ERC20Toolkit.for_chain(
1,
rpc_url="https://your-node",
tokens={"usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},
)
tools = {t.name: t for t in toolkit.get_tools()}
plan = tools["transfer"].invoke(
{
"token": "usdc",
"to": "0x...recipient",
"from_address": "0x...your_eoa",
"amount": "25.5",
}
)
for tx in plan["transactions"]:
tx.pop("gas_estimated") # metadata, not a transaction field
signed = account.sign_transaction(tx)
w3.eth.send_raw_transaction(signed.raw_transaction)
Smart-contract wallet
toolkit = ERC20Toolkit.for_chain(1, rpc_url="https://your-node", tx_mode="calls")
tools = {t.name: t for t in toolkit.get_tools()}
plan = tools["approve"].invoke(
{
"token": "0x...token",
"spender": "0x...spender",
"from_address": "0x...your_smart_account",
"amount": "100",
}
)
executions = [(c["to"], c["value"], bytes.fromhex(c["data"][2:])) for c in plan["calls"]]
send_batch_user_op(executions) # one atomic transaction
Execution modes
tx_mode="eoa" (default) |
tx_mode="calls" |
|
|---|---|---|
calls |
populated | populated |
transactions |
signable, sequential nonces | None |
| Nonce / gas / fee RPC calls | yes | zero |
calls mode makes no eth_getTransactionCount and no eth_estimateGas on purpose. A 4337 nonce
is EntryPoint.getNonce(sender, key), a 2D nonce unrelated to an EOA transaction count; and
eth_estimateGas with from set to a smart account simulates the account calling itself as an
EOA, which is not how the EntryPoint invokes it, so the number is wrong even when it succeeds.
The plan shape
{
"calls": [
{
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"value": 0,
"data": "0x095ea7b3...",
"role": "approve",
"description": "Set USDT allowance for 0x7a25... to 0",
},
],
"transactions": [...], # or None in calls mode
"chain_id": 1,
"summary": {...}, # whole-unit amounts, safe to show a user
}
role is approve, approve_reset, or action. Roles let a consumer validate a plan before
submitting it — for example a wallet with a spending-limit hook checking that no approval is left
standing — and let a UI describe it.
data is a hex string rather than bytes so plans stay JSON-serialisable: these are LangChain tool
returns and must survive being written into an agent transcript.
Tools
Read
| Tool | Returns |
|---|---|
get_token_metadata(token) |
address, name, symbol, decimals, total_supply, total_supply_base |
get_balance(token, owner) |
amount, amount_base, decimals, symbol |
get_native_balance(owner) |
amount, amount_base, symbol |
get_allowance(token, owner, spender) |
amount, amount_base, is_unlimited |
is_balance_sufficient(token, owner, amount) |
is_sufficient, balance, required, shortfall |
is_allowance_sufficient(token, owner, spender, amount) |
as above, for the allowance |
supports_permit(token) |
supported, standard — eip2612, dai, unknown or null |
Write — all return plans
| Tool | Plan |
|---|---|
transfer(token, to, from_address, amount) |
[action] |
transfer_all(token, to, from_address) |
[action], balance read at build time |
transfer_from(token, owner, to, from_address, amount) |
[action] |
batch_transfer(token, transfers, from_address) |
[action × N] |
approve(token, spender, from_address, amount / unlimited) |
[approve] or [approve_reset, approve] |
revoke_approval(token, spender, from_address) |
[approve_reset] |
wrap_native(from_address, amount) |
[action], amount carried as value |
unwrap_native(from_address, amount) |
[action] |
Every write tool also accepts amount_base for exact base units, and nonce to set the starting
nonce in EOA mode.
Amounts
amount accepts a float or a string. Prefer strings for large or precise values: a float
cannot represent 18 decimal places, and 0.1 + 0.2 is the cheapest possible way to send the wrong
amount. Conversion truncates toward zero and never rounds up, since rounding up can overspend or
exceed an allowance; when truncation loses precision the summary says amount_truncated: true.
Reads return both amount and amount_base so a consumer building its own calldata never has to
re-derive decimals.
Token compatibility
The ERC-20 standard is, in practice, a suggestion. Each of the following is a real token that
breaks a naive implementation, and each is handled here and covered by tests in
tests/test_compat.py.
| Reality | Token | How this package handles it |
|---|---|---|
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. |
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. |
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. |
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. |
| 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. |
| Fee-on-transfer | SafeMoon-likes | Summaries say amount_sent, never amount_received — the package cannot know how much arrives. |
| Rebasing | stETH, AMPL | transfer_all records balance_read_at_block and warns its fixed amount can go stale. |
| Blocklists, pauses, ERC-777 hooks | USDC, USDT, many | Not detectable in advance. See the warning below. |
Preflight is not a guarantee. With
preflight=True(the default) every write tool checks balances and allowances before building, and raises naming the exact shortfall. It cannot see blocklists, pauses, transfer hooks or reentrancy. A passing preflight means the transfer is not obviously impossible — not that it will succeed.
ERC-4337
This package owns exactly one step:
| Step | Owner |
|---|---|
| 1. Decide the calls | this package — plan["calls"] |
2. Encode into the account's callData |
consumer (account-specific) |
| 3. Fill the UserOp (sender, 2D nonce, initCode, paymaster) | consumer / AA SDK |
| 4. Estimate gas | bundler (eth_estimateUserOperationGas) |
5. Sign userOpHash |
consumer's signer |
| 6. Submit and poll | bundler |
Step 2 is account-specific: ERC-4337 standardises the UserOperation struct and the EntryPoint, not the account's execute interface. ERC-7579, ERC-6900, Safe, Kernel and LightAccount each differ.
Counterfactual (not-yet-deployed) accounts work: the address is deterministic and can hold tokens
before deployment, so balance reads succeed and nothing in calls mode requires code at the
address.
Note permit is effectively EOA-only. EIP-2612 verifies with ecrecover, which cannot validate
a smart-contract signature. Smart accounts should batch [approve, action] atomically instead,
which achieves the same thing without a signature.
Supported chains for wrapped-native
for_chain(chain_id) supplies a wrapped-native address and a public RPC for: Ethereum (1),
Sepolia (11155111), Optimism (10), BSC (56), Polygon (137), Base (8453), Arbitrum (42161) and
Avalanche (43114).
Every address is verified against the live chain by scripts/verify_networks.py, which checks
bytecode, symbol(), decimals() and the presence of deposit()/withdraw(uint256), following
EIP-1967 proxies where needed. Public RPCs are rate-limited; pass your own for production.
Celo is deliberately absent: CELO is natively an ERC-20 with no wrapping step, so wrap_native
has no meaning there and for_chain(42220) fails loudly rather than guessing.
Everything except wrap_native/unwrap_native works on any EVM chain via the main constructor.
Development
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/ruff check .
.venv/bin/ruff format --check .
.venv/bin/pyright --pythonpath .venv/bin/python
.venv/bin/python -m pytest -q
Re-verify the wrapped-native addresses against live chains:
.venv/bin/python scripts/verify_networks.py
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langchain_erc20-0.1.0.tar.gz.
File metadata
- Download URL: langchain_erc20-0.1.0.tar.gz
- Upload date:
- Size: 35.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3066fe37c7a37e8d84b694b21d79aebdcf22a77e7e8be60df5a230eb8415dbb9
|
|
| MD5 |
700a5b8440021c3df7de7fbe3d2c75db
|
|
| BLAKE2b-256 |
49456585b1a34aee34b57f6e4bb82c28f7d076be78902a49576b92bb3c33cd77
|
File details
Details for the file langchain_erc20-0.1.0-py3-none-any.whl.
File metadata
- Download URL: langchain_erc20-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25df99a0d265e60325fdf5368a933c9af774643222e4281827d11dd7543e7a42
|
|
| MD5 |
70612be2ad692e2e8c3da66bf8f87984
|
|
| BLAKE2b-256 |
e5b7823c396e55c4f6fe28bdef8475b0ccadd71afe72db02b12e0d8fc66b5bc8
|