Skip to main content

langchain-erc20

CI PyPI

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.

0.3.0 (alpha). The public API may still change before 1.0 — 0.3.0 moved gas_estimated out of each transaction dict onto the plan, see the changelog. EIP-2612 permit is detected by supports_permit but the signing flow is not implemented yet.

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 Execution is exactly that
  • a Safe MultiSend entry 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"]:
    signed = account.sign_transaction(tx)
    w3.eth.send_raw_transaction(signed.raw_transaction)

Pass your own rpc_url, as both examples here do. for_chain falls back to a bundled public endpoint when you don't, and those are free gateways shared by every user of this package, offered with no availability guarantee. They exist so for_chain(1) works in one line while you try the library out. Anything beyond that should supply its own endpoint — ERC20Toolkit.for_chain(1, rpc_url=...) or ERC20Toolkit(rpc_url=...) — and treat the bundled value purely as a fallback. If a bundled endpoint is unreachable, for_chain says so and tells you this.

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
gas_estimated one bool per transaction None
Nonce / gas / fee RPC calls yes zero
Simulation free, from the gas estimate one eth_call, deployed senders only

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.

It does still simulate the first call, with one deliberate eth_call preceded by an eth_getCode, so that "a plan certain to revert is never built" holds for smart accounts too and not just for EOAs, which get it free from gas estimation. A sender with no code yet is skipped: its balance and allowances belong to an address that does not hold them until the same UserOp deploys it. estimate_gas=False turns simulation off in both modes.

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
    "gas_estimated": [...],  # per tx: live estimate or static table; None in calls mode
    "chain_id": 1,
    "summary": {...},  # whole-unit amounts, safe to show a user
}

A transaction dict holds transaction fields and nothing else, so it signs exactly as returned — acct.sign_transaction(plan["transactions"][0]), no preparation step. Anything else would break the first thing an EOA consumer does: eth_account validates its input and rejects an unrecognised key with TypeError: Unknown kwargs. Whether each gas limit came from a live estimate or the static fallback is a real question, so it is answered in plan["gas_estimated"], index for index, rather than smuggled into the dict.

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.

Simulation is the stronger check, and separate: it runs the call rather than reading balances, so it does catch a blocklist or a pause. Every plan gets it — for free from the gas estimate in EOA mode, and from one deliberate eth_call in calls mode — but only against the chain as it is now, and only for the first call in a plan.

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), Unichain (130), Monad (143), X Layer (196), World Chain (480), BSC (56), Polygon (137), Base (8453), Arbitrum (42161), Avalanche (43114) and Zora (7777777).

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. Unichain, World Chain, Zora, Base and Optimism all share 0x4200000000000000000000000000000000000006 — that is the OP Stack's deterministic predeploy, not a copy-paste error, and each was verified separately.

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

Release files for langchain-erc20 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for langchain-erc20 0.3.0
File Size Uploaded
langchain_erc20-0.3.0.tar.gz 42.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for langchain-erc20 0.3.0
File Interpreter ABI Platform
langchain_erc20-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 81.0 kB

Release files / langchain_erc20-0.3.0.tar.gz

Download URL langchain_erc20-0.3.0.tar.gz
Size 42.5 kB
Tags Source
SHA-256 checksum
How to use checksums
71ee36aa29f49201059bbee66ef63548d25ad475f8e14b7282896a11ea89c673
BLAKE2b-256 checksum
How to use checksums
1b36dbfb0793e6889de9a2ba4b1976619d6ffbbe3a5d3341dc05e6b9f5b0f13d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / langchain_erc20-0.3.0-py3-none-any.whl

Download URL langchain_erc20-0.3.0-py3-none-any.whl
Size 38.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
64d78492f18d86889ce9346e605aa6d3cde61bbd1c5389b09cbe93041148ae02
BLAKE2b-256 checksum
How to use checksums
93827994a734355d68fb2d3abb63a0b6110aeefe9608ad3a5193440d06238637
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

0.3.1

2 release files

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page