Skip to main content

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 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)

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, standardeip2612, 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 packageplan["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

langchain_erc20-0.1.0.tar.gz (35.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langchain_erc20-0.1.0-py3-none-any.whl (33.7 kB view details)

Uploaded Python 3

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

Hashes for langchain_erc20-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3066fe37c7a37e8d84b694b21d79aebdcf22a77e7e8be60df5a230eb8415dbb9
MD5 700a5b8440021c3df7de7fbe3d2c75db
BLAKE2b-256 49456585b1a34aee34b57f6e4bb82c28f7d076be78902a49576b92bb3c33cd77

See more details on using hashes here.

File details

Details for the file langchain_erc20-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for langchain_erc20-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 25df99a0d265e60325fdf5368a933c9af774643222e4281827d11dd7543e7a42
MD5 70612be2ad692e2e8c3da66bf8f87984
BLAKE2b-256 e5b7823c396e55c4f6fe28bdef8475b0ccadd71afe72db02b12e0d8fc66b5bc8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page