Metadata-Version: 2.4
Name: langchain-erc20
Version: 0.1.0
Summary: LangChain tools for ERC20 primitives: balances, allowances, transfers, approvals, and native wrap/unwrap, as execution plans an EOA can sign or a smart-contract wallet can batch.
Author-email: conrad.japhet@gmail.com
License-Expression: MIT
Project-URL: Repository, https://github.com/Conrad-sudo/langchain-erc20
Project-URL: Issues, https://github.com/Conrad-sudo/langchain-erc20/issues
Keywords: langchain,erc20,web3,ethereum,evm,tokens,agent,tools,erc4337,smart-wallet
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: web3>=7.0
Requires-Dist: langchain-core>=0.3
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: ruff==0.16.1; extra == "dev"
Requires-Dist: pyright==1.1.411; extra == "dev"
Dynamic: license-file

# 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](https://github.com/Conrad-sudo/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

```bash
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

```python
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

```python
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

```python
{
    "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), 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

```bash
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:

```bash
.venv/bin/python scripts/verify_networks.py
```

## License

MIT
