Skip to content

Releases: Conrad-sudo/langchain-erc20

v0.3.1 — Badge Update

Choose a tag to compare

@Conrad-sudo Conrad-sudo released this 20 Aug 10:14

Release v0.3.1

What's Changed

  • Documentation & PyPI Improvements: Added status badges to the README.md and PyPI project description to display supported Python versions and project licensing.

Badges Added

Python
License


Full Changelog: https://github.com/Conrad-sudo/langchain-erc20/commits/v0.3.1

v0.3.0 — gas_estimated moves onto the plan

Choose a tag to compare

@Conrad-sudo Conrad-sudo released this 20 Aug 07:32

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 answer

plan["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 line

Consumers 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 one eth_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, and estimate_gas=False turns
    simulation off in both modes. This catches what the preflight flag's balance and allowance reads
    cannot: blocklists, pauses and transfer hooks.
  • ERC20_ERRORS_ABI is 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

Choose a tag to compare

@Conrad-sudo Conrad-sudo released this 12 Aug 13:38

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

Choose a tag to compare

@Conrad-sudo Conrad-sudo released this 07 Aug 08:00

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

Added

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/approve return nothing. 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.
  • USDT — approve reverts 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/symbol return bytes32. Retried against a bytes32 ABI; unreadable metadata becomes null rather than failing the call.
  • Tokens without decimals() — 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.
  • UNI — stores allowances in 96 bits. unlimited=True encodes exactly 2**256 - 1; anything above the uint256 ceiling raises before encoding rather than surfacing web3's MismatchedABI.
  • Fee-on-transfer and rebasing tokens — summaries say amount_sent, never amount_received. transfer_all records balance_read_at_block and 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_permit reports eip2612, dai, unknown or null so 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, since ecrecover cannot 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.