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.1.0 (alpha). The public API may still change before 1.0. 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"]:
    tx.pop("gas_estimated")  # metadata, not a transaction field
    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
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), 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.2.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.2.0
File Size Uploaded
langchain_erc20-0.2.0.tar.gz 37.9 kB Details

Built distribution (wheel)

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

Total release size: 72.9 kB

Release files / langchain_erc20-0.2.0.tar.gz

Download URL langchain_erc20-0.2.0.tar.gz
Size 37.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1856deab384aadcd69e36e7b51474f16c0bf90c0b3a4beb00abff53f1d8de6c3
BLAKE2b-256 checksum
How to use checksums
16cca719bd74ca9bf7cdc715a0f457d5185afd4b8eb6ed7d63f467931380fab6
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.2.0-py3-none-any.whl

Download URL langchain_erc20-0.2.0-py3-none-any.whl
Size 35.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e9e3ea76829b83591b39a77570df509d625192681e3e8f07fb69d1836aa64c21
BLAKE2b-256 checksum
How to use checksums
3a48b2b651640c0d83a32987f54e08695681d8eeb62761cbd5ced7b10788f9a3
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

0.3.0

2 release files

This release

0.2.0 This release

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