Repository navigation
Releases: Conrad-sudo/langchain-erc20
Release list
v0.3.1 — Badge Update
Release v0.3.1
What's Changed
- Documentation & PyPI Improvements: Added status badges to the
README.mdand PyPI project description to display supported Python versions and project licensing.
Badges Added
Full Changelog: https://github.com/Conrad-sudo/langchain-erc20/commits/v0.3.1
v0.3.0 — gas_estimated moves onto the plan
What changed
Breaking: gas_estimated has moved out of each transaction dict and onto the plan.
render_eoa used to write a gas_estimated bool into every transaction. eth_account validates
the dict it is handed and rejects any key it does not recognise, so the transactions a plan returned
could not be signed as returned:
acct.sign_transaction(plan["transactions"][0])
# TypeError: Unknown kwargs: ['gas_estimated']The README worked around it in prose, telling you to pop the key first. That put a preparation step
in front of the first thing every EOA consumer does, and pointed the traceback at eth_account
rather than at this package.
"Did this limit come from a live estimate" is not a field an Ethereum transaction has. It is
provenance about the plan, so it now travels beside the transactions:
plan["transactions"][i] # signs exactly as returned, no preparation
plan["gas_estimated"][i] # same index, same answerplan["gas_estimated"] is a list of one bool per transaction, and None in calls mode — nothing
was estimated there, so [] or [False, ...] would both assert something untrue.
Migrating
Read plan["gas_estimated"][i] instead of plan["transactions"][i]["gas_estimated"]. If you
followed the old README and pop the key, use the tolerant form — the strict tx.pop("gas_estimated")
now raises KeyError:
tx.pop("gas_estimated", None) # or just delete the lineConsumers in tx_mode="calls" are unaffected: they read plan["calls"] and never rendered
transactions.
Also in this release
- Reverts are reported as sentences. A refused plan now says
Transfer 1000000.0 USDC to 0x7a25... would revert: execution reverted: ERC20: transfer amount exceeds balance.Previously this was web3's exception repr, which for a custom error printed the
same hex string twice and named neither the call nor the error. The six OpenZeppelin v5 ERC-20
custom errors decode by name; an unregistered selector says so rather than guessing. tx_mode="calls"now simulates the first call with oneeth_call, so "a plan certain to
revert is never built" holds for smart accounts and not only for EOAs, which get it free from gas
estimation. A sender with no code deployed yet is skipped, andestimate_gas=Falseturns
simulation off in both modes. This catches what thepreflightflag's balance and allowance reads
cannot: blocklists, pauses and transfer hooks.ERC20_ERRORS_ABIis exported for consumers decoding reverts of their own.
Compatibility
The same plans.py is mirrored in langchain-erc8004 and langchain-uniswap-v2; both already
carry this change, so the plan shape is now identical across all three.
v0.2.0 — Unichain, Monad, X Layer, World Chain and Zora
Five chains join the wrapped-native registry, taking it from 8 entries to 13. This is purely
additive: no signature changes, and a caller who never touches these chain ids sees nothing
different.
Added
ERC20Toolkit.for_chain() — and with it wrap_native / unwrap_native — now works on:
| chain id | name | wrapped | ticker |
|---|---|---|---|
| 130 | Unichain | WETH | ETH |
| 143 | Monad | WMON | MON |
| 196 | X Layer | WOKB | OKB |
| 480 | World Chain | WETH | ETH |
| 7777777 | Zora | WETH | ETH |
Every address was verified against the live chain before it landed, using the package's own
criteria: contract bytecode present, symbol() matches the expected wrapped ticker, decimals()
is 18, and both deposit() and withdraw(uint256) are present in the deployed code. None is a
proxy. Nothing was copied from a token list or a block explorer.
EOA callers (tx_mode="eoa") and smart-account callers (tx_mode="calls" — ERC-4337, Safe,
ERC-7579) gain these equally.
Changed
The bundled RPC endpoints are now documented for what they are. They are free public 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 are trying the library out. Anything beyond that should
pass its own endpoint and treat the bundled value as a fallback:
ERC20Toolkit.for_chain(1, rpc_url="https://your-node")
ERC20Toolkit(rpc_url="https://your-node")for_chain() now says so when a bundled endpoint is down. Falling back to an unreachable
bundled gateway raises a ToolException naming the endpoint and the fix, instead of a bare
ConnectionError that identified neither. A caller who passed their own rpc_url is unaffected
and still receives the original error.
scripts/verify_networks.py now fails an entry that carries no wrapped-native address, and retries
an endpoint before reporting it unreachable — a single-attempt probe produced two false negatives
during this work, on endpoints that were healthy and recovered on their own.
Notes
Unichain, World Chain and Zora share 0x4200000000000000000000000000000000000006 with Base and
Optimism. That is not a copy-paste error: all five are OP Stack chains using the same
deterministic predeploy, and each was verified independently at a different block height and total
supply.
Celo (42220) remains 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 inventing an
address. This package still ships no general token registry, for the same reason it never has: a
wrong address in one is a silent, unrecoverable loss of funds. Pass your own via tokens={...}.
Full changelog: v0.1.0...v0.2.0
v0.1.0 — LangChain tools for ERC-20 primitives as execution
LangChain tools for ERC-20 primitives, usable by an EOA or by any smart-contract wallet.
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/fees, an ERC-7579 Execution is exactly that, and an ERC-4337 UserOp wraps a batch of them.
Install
pip install langchain-erc20Added
15 tools. Reads: get_token_metadata, get_balance, get_native_balance, get_allowance, is_balance_sufficient, is_allowance_sufficient, supports_permit. Writes: transfer, transfer_all, transfer_from, batch_transfer, approve, revoke_approval, wrap_native, unwrap_native.
Two execution modes. tx_mode="eoa" (default) also renders signable transactions with sequential nonces. tx_mode="calls" returns account-agnostic calls only and makes zero nonce/gas/fee RPC calls — a 4337 nonce is EntryPoint.getNonce(sender, key), not an EOA transaction count, and eth_estimateGas with from set to a smart account simulates the account calling itself as an EOA, so the number is wrong even when it succeeds.
Token references. Every token argument accepts an address or a symbol from a registry you supply. No token addresses are bundled: a wrong address in one is a silent, unrecoverable loss of funds.
Amounts. Accepted as float or str and converted through Decimal. Strings are preferred and documented as such, because a float cannot hold 18 decimal places. Conversion truncates toward zero and never rounds up, and reports amount_truncated when precision was actually lost. Every write tool also accepts amount_base for exact base units.
Token compatibility
The standard is a suggestion in practice. Each of these is a real token that breaks a naive implementation, and each is handled and tested:
- USDT, BNB, OMG —
transfer/approvereturn nothing. Write functions are only ever encoded, never called, so empty returndata is never decoded.ERC20_NO_RETURN_ABIis exported for consumers who dry-run writes themselves. - USDT —
approvereverts while a non-zero allowance stands.zero_first_approvals="auto"reads the allowance and emits[approve_reset, approve]when needed, which also closes the generic front-running window. - MKR and most pre-2018 tokens —
name/symbolreturnbytes32. Retried against abytes32ABI; unreadable metadata becomesnullrather than failing the call. - Tokens without
decimals()— never assumed to be 18. Raises and points atdecimals_overrides, because guessing 18 on a 6-decimal token sends 10¹² times the intended amount. - UNI — stores allowances in 96 bits.
unlimited=Trueencodes exactly2**256 - 1; anything above the uint256 ceiling raises before encoding rather than surfacing web3'sMismatchedABI. - Fee-on-transfer and rebasing tokens — summaries say
amount_sent, neveramount_received.transfer_allrecordsbalance_read_at_blockand warns its fixed amount can go stale.
Supported chains for wrapped-native
Ethereum, Sepolia, Optimism, BSC, Polygon, Base, Arbitrum, Avalanche.
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. Everything except wrap_native/unwrap_native works on any EVM chain.
Celo is deliberately absent: CELO is natively an ERC-20 with no wrapping step, so for_chain(42220) fails loudly rather than guessing.
Known limitations
- Preflight is not a guarantee. It checks balances and allowances only. Blocklists, pauses, transfer hooks and reentrancy are not detectable in advance.
- EIP-2612 permit is detected but not yet implemented.
supports_permitreportseip2612,dai,unknownornullso a DAI-style token fails loudly instead of being silently encoded against the wrong signature. The signing flow lands in a later release. Permit is also effectively EOA-only, sinceecrecovercannot validate a smart-contract signature. - No live-network test suite yet. All 337 tests run against mocks configured to behave like the real tokens named above.
Verification
337 tests on Python 3.10–3.13, with ruff and pyright clean in CI.