langchain-uniswap-v2
LangChain tools for Uniswap V2 (and Uniswap-V2-shaped forks, e.g. PancakeSwap): live swap quotes, liquidity previews, LP token balances, and execution plans for swaps, approvals, and add/remove liquidity — ready for an EOA to sign or a smart-contract wallet to batch, on any EVM chain, given just an RPC URL and contract addresses.
LangChain's existing blockchain integrations are Coinbase's CDP AgentKit (execution-oriented — transfers, trades, deployments — tied to Coinbase's own wallet infrastructure) and the Compass DeFi Toolkit (a paid third-party API that returns unsigned transactions across several protocols, including Uniswap). Neither is a free, permissionless toolkit that works against any EVM chain given just an RPC URL — no API key, no third-party service, no wallet required. This package fills that specific gap for the Uniswap V2 AMM shape.
Scope
This package never holds a private key, a signer, or any wallet state, and it never signs or submits a transaction. Read tools (quotes, liquidity previews, balances) only make RPC calls. Write tools (approvals, swaps, add/remove liquidity) build and return an execution plan — an ordered list of account-agnostic contract calls, ready for an EOA to sign or a smart-contract wallet to batch. See Execution modes and Write tools below.
Install
pip install langchain-uniswap-v2
For local development (editable install + test dependencies):
pip install -e ".[dev]"
Quick start
from langchain_uniswap_v2 import UniswapV2Toolkit
# Any Uniswap-V2-shaped DEX, on any chain, via explicit addresses:
toolkit = UniswapV2Toolkit(
rpc_url="https://eth.llamarpc.com",
router_address="0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
native_wrapped_address="0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH
)
# Or, for a chain already in the built-in registry, just pass a chain_id:
toolkit = UniswapV2Toolkit.for_chain(chain_id=1) # mainnet
# toolkit = UniswapV2Toolkit.for_chain(chain_id=56) # bsc (PancakeSwap)
tools = toolkit.get_tools()
# -> [get_quote_in, get_quote_out, get_pool_quote, get_lp_amounts,
# get_liquidity_token_balance, is_token_balance_sufficient,
# is_native_balance_sufficient, is_derived_token_input_sufficient,
# is_derived_native_input_sufficient, is_liquidity_sufficient,
# is_liquidity_sufficient_eth, is_liquidity_removal_sufficient,
# approve_token,
# swap_exact_tokens_for_tokens, swap_tokens_for_exact_tokens,
# swap_exact_eth_for_tokens, swap_eth_for_exact_tokens,
# swap_exact_tokens_for_eth, swap_tokens_for_exact_eth,
# add_liquidity, add_liquidity_eth,
# remove_liquidity, remove_liquidity_eth]
# Pass tools directly into an agent:
# agent = create_agent(model, tools=tools)
# Or call a tool directly:
result = tools[1].invoke(
{ # get_quote_out
"token_in": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH
"token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC
"amount_in": 1,
}
)
# -> {"amount_in": 1.0, "amount_out": <live price>, "path": [...]}
UniswapV2Toolkit.for_chain(...) accepts an optional rpc_url= override —
the registry's default is a free public endpoint, fine for prototyping but
rate-limited. Pass your own (Alchemy, Infura, etc.) for production use.
A second example, for a smart-contract wallet consuming calls directly instead of signing EOA transactions:
scw_toolkit = UniswapV2Toolkit.for_chain(chain_id=1, tx_mode="calls")
scw_tools = {t.name: t for t in scw_toolkit.get_tools()}
plan = scw_tools["swap_exact_tokens_for_tokens"].invoke(
{
"token_in": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", # WETH
"token_out": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC
"amount_in": 1,
"from_address": "0xYourSmartAccount...",
}
)
# plan["transactions"] is None -- zero nonce/gas/fee RPC calls were made.
executions = [(c["to"], c["value"], bytes.fromhex(c["data"][2:])) for c in plan["calls"]]
# feed `executions` into your account's own batch executor, e.g. an
# ERC-7579 execute(mode, abi.encode(executions)) call.
Execution modes
Every write tool returns a plan, not a bare transaction:
{
"calls": [ # always present, ordered -- execute all of them
{"to": "0x...", "value": 0, "data": "0x...", "role": "approve", "description": "..."},
{
"to": "0x...router",
"value": 0,
"data": "0x...",
"role": "swap",
"description": "...",
},
],
"transactions": [...] | None, # EOA mode only; same calls, signable, nonces assigned
"chain_id": 1,
"summary": {...}, # amounts in whole units, safe to show a user
}
tx_mode="eoa"(the default) also rendersplan["transactions"]: the same calls as unsigned, signable transactions with sequential nonces already assigned. Sign and broadcast them in order.tx_mode="calls"returnscallsonly (transactionsisNone) and makes zero nonce/gas/fee RPC calls — for a smart-contract wallet's own batch executor.
calls is the smallest primitive both audiences agree on: an EOA
transaction is (to, value, data) plus nonce/gas/fees; an ERC-7579
Execution is exactly (to, value, data). Execute every call in a plan, in
order — if your account supports batching, execute them atomically in one
transaction. That atomicity is what makes the approve → action → reset
sequence safe for accounts that reject standing approvals.
gas_estimated caveat (EOA mode): a call whose prerequisite approval is
earlier in the same plan can't be simulated yet — the allowance isn't
on-chain until that earlier call is mined. For those, the transaction's
gas field falls back to a static per-role default (DEFAULT_GAS) and
gas_estimated is set to False, flagging that the limit wasn't derived
from a live simulation and should be sanity-checked before broadcast. Only
the first call in a plan has its revert treated as fatal — if that one
fails to estimate, the tool raises ToolException instead of returning a
plan that would fail on-chain. gas_estimated is metadata, not a
transaction field — pop it (or move it into a parallel list) before
signing.
Constructor options, all keyword-only:
| Param | Default | Meaning |
|---|---|---|
tx_mode |
"eoa" |
"eoa" also renders transactions. "calls" returns calls only. |
estimate_gas |
True |
EOA mode only. False uses default_gas for every call, skipping live eth_estimateGas. |
gas_buffer |
1.25 |
Multiplier applied to a successful gas estimate. |
default_gas |
None |
Per-role fallback gas limits, merged over the module's DEFAULT_GAS. |
reset_residual_approvals |
None |
Append a trailing zero-approval where needed (see Write tools). Defaults to True in "calls" mode, False in "eoa" mode. |
preflight |
True |
Run the matching balance-sufficiency check before building, raising ToolException (naming the shortfall) if it fails. |
UniswapV2Toolkit.for_chain(chain_id, rpc_url=None, **kwargs) forwards
**kwargs to the constructor, so e.g.
UniswapV2Toolkit.for_chain(1, tx_mode="calls", preflight=False) works.
router_address, factory_address and native_wrapped_address can be
passed the same way, each overriding the registry's value for that chain —
the escape hatch for a stale entry, or a fork or testnet reusing a known
chain id.
Tools
All tools take plain contract addresses as arguments — no ticker→address registry is assumed.
Read tools
| Tool | Purpose |
|---|---|
get_quote_in(token_in, token_out, amount_out) |
How much token_in is needed to receive an exact amount_out of token_out. |
get_quote_out(token_in, token_out, amount_in) |
How much token_out will be received for an exact amount_in of token_in. |
get_pool_quote(token_a, token_b, amount_a) |
Proportional token_b deposit required to match a given token_a deposit, before adding liquidity. |
get_lp_amounts(token_a, token_b, lp_amount) |
Expected token amounts redeemable for a given amount of LP tokens, before removing liquidity. |
get_liquidity_token_balance(owner_address, token_a, token_b) |
An address's LP token balance for a given pair. |
For a token_in/token_out pair, the direct pool and — when
native_wrapped_address is configured — the route through the wrapped
native are both quoted, and the better fill wins: more token_out for an
exact-input quote, less token_in for an exact-output one. An exact tie
goes to the direct pool, since one hop costs less gas than two. A route that
reverts is skipped; if none is routable the tool raises ToolException.
Comparing beats assuming in both directions. Routing a liquid USDC/DAI pair
through WETH would pay the 0.3% fee twice and take two price impacts instead
of one, so the direct pool usually wins — but a direct pool holding dust
loses badly, and picking it on the strength of merely existing is a silent
error, because amount_out_min is derived from the same quote and the swap
then fills "within tolerance" at a much worse price.
Balance-sufficiency checks answer "does this address hold enough" before a
write tool would actually be called, given an explicit owner_address —
useful for an agent to check before spending gas building/submitting a
transaction that would just revert. Every write tool below already runs its
matching check automatically before building (see preflight above); these
tools are for checking ahead of time or with preflight=False. Each mirrors
the balance requirement of one or two write tools below:
| Tool | Checks balance for |
|---|---|
is_token_balance_sufficient(token_address, amount, owner_address) |
An exact ERC20 amount — e.g. before approve_token or an exact-input swap. |
is_native_balance_sufficient(amount, owner_address) |
An exact native-asset amount. |
is_derived_token_input_sufficient(token_in, token_out, amount_out, owner_address, slippage_bps=50) |
swap_tokens_for_exact_tokens / swap_tokens_for_exact_eth — required input is derived from a live quote plus slippage. |
is_derived_native_input_sufficient(token_out, amount_out, owner_address, slippage_bps=50) |
swap_eth_for_exact_tokens. |
is_liquidity_sufficient(token_a, amount_a, token_b, owner_address) |
add_liquidity — both token amounts, the second derived from live reserves. |
is_liquidity_sufficient_eth(token, amount_token, owner_address) |
add_liquidity_eth. |
is_liquidity_removal_sufficient(token_a, token_b, lp_amount, owner_address) |
remove_liquidity / remove_liquidity_eth — LP token balance. |
Write tools (execution plans)
Every write tool below includes its own approval call(s) in the plan
automatically, sized to what that call actually needs — you no longer need
to call approve_token first. approve_token remains available for
explicit/manual control (e.g. granting an allowance outside of any swap or
deposit flow, or to a spender other than this toolkit's router), and takes
an unlimited=True option for the standard effectively-unlimited approval
instead of passing an arbitrary large amount.
| Tool | Approves | Reset needed |
|---|---|---|
approve_token(token_address, spender_address, from_address, amount=0, unlimited=False) |
is itself the approval | — |
swap_exact_tokens_for_tokens / swap_exact_tokens_for_eth |
token_in, the exact amount sold |
no — exact pull |
swap_tokens_for_exact_tokens / swap_tokens_for_exact_eth |
token_in, the derived max |
yes, when the router pulls less |
swap_exact_eth_for_tokens / swap_eth_for_exact_tokens |
— (native value, no approval needed) | — |
add_liquidity / add_liquidity_eth |
the token(s) deposited | yes — pool ratio may consume less |
remove_liquidity / remove_liquidity_eth |
the pair's own LP token, the exact amount burned | no — exact pull |
"Reset needed" means: when the router may pull less than it was approved
for, the plan appends a trailing approve(spender, 0) call — but only when
reset_residual_approvals is enabled (see Execution modes).
Tools that always pull their exact approved amount never append one,
regardless of that setting, since there is nothing left to clear.
All amount-based write tools also take slippage_bps (default 50 = 0.5%)
and derive amountOutMin/amountInMax/equivalent from a live on-chain
quote, and deadline_secs (default 600) for the plan's on-chain expiry —
both are always explicit, never silently applied. Every write tool also
takes an optional recipient (defaults to from_address) for the output of
the call — the account executing the plan and the account receiving the
result don't have to be the same address — and an optional nonce: the
starting nonce for the plan in EOA mode (later calls increment from it
automatically), ignored entirely in "calls" mode.
"ETH" in tool/parameter names is a generic internal label for the chain's native asset (ETH, BNB, etc.) — it works identically on every supported network.
Supported chains (built-in registry)
All addresses are Uniswap Labs' own official V2 redeployments, except BSC, which deliberately uses PancakeSwap — see the note below.
| chain_id | name | native_token | wrapped native |
|---|---|---|---|
| 1 | mainnet | ETH | WETH |
| 11155111 | sepolia | ETH | WETH |
| 130 | unichain | ETH | WETH |
| 42161 | arbitrum | ETH | WETH |
| 43114 | avalanche | AVAX | WAVAX |
| 56 | bsc (PancakeSwap V2) | BNB | WBNB |
| 8453 | base | ETH | WETH |
| 10 | optimism | ETH | WETH |
| 137 | polygon | POL | WPOL |
| 7777777 | zora | ETH | WETH |
| 480 | worldchain | ETH | WETH |
| 143 | monad | MON | WMON |
| 196 | x-layer | OKB | WOKB |
Every chain has a wrapped-native address registered, so the native-asset
swap and liquidity tools work on all of them. Each address is the one that
chain's own router returns from WETH() — the router rejects any other
address in its *ETH-suffixed functions with UniswapV2Router: INVALID_PATH, so the router is the only authority on this, not a token list
or explorer page. scripts/verify_native_wrapped.py re-checks every entry
against its live router (weekly in CI, and runnable locally).
Why BSC uses PancakeSwap, not Uniswap Labs' own BSC redeployment: the official Uniswap Labs BSC contracts exist and respond to calls, but were live-tested and found to have near-zero liquidity (returning wildly incorrect quotes). PancakeSwap has the actual liquidity on BSC, so the registry points there instead.
For any chain not listed here, instantiate UniswapV2Toolkit(...) directly
with explicit router_address / factory_address / native_wrapped_address.
Migration (0.3.0 → 0.4.0)
No API change: nothing renamed, no signature removed, no return shape altered. Existing code keeps working as written. It is a minor rather than a patch because observable behaviour changes for existing callers.
Added
native_wrappedis now populated for all supported chains (previouslyNoneon 10 of 13), enabling the native-asset swap and liquidity tools and wrapped-native multi-hop routing on Optimism, Polygon, Base, Arbitrum, Avalanche, Unichain, Monad, X Layer, World Chain and Zora. Every value is verified equal to the chain router's ownWETH().
Fixed
for_chain()no longer raisesTypeErrorwhen a caller overridesrouter_address,factory_addressornative_wrapped_address.- Route selection no longer prefers a direct pair holding negligible reserves over a wrapped-native route that quotes better. Previously this could select a materially worse price with no error at all.
What this means in practice:
| behaviour | 0.3.0 | 0.4.0 |
|---|---|---|
| eight native-asset tools on 10 of the 13 chains | raise ToolException when invoked |
build plans |
| token→token pairs with no direct pool, on those chains | raise "no liquidity path" | route through the wrapped native |
| direct pool holding dust, better route available | silently takes the dust pool | takes the better route |
for_chain(cid, native_wrapped_address=...) |
TypeError |
override applied |
If you pinned around any of that — for example, passing
native_wrapped_address explicitly to work around the missing registry
values, or hard-coding a path you expected the toolkit to produce — the
workarounds still function, but are no longer needed.
Migration (0.2.0 → 0.3.0)
Read tools are unchanged. Write tools now return an execution plan instead of a bare transaction dict:
| 0.2.0 | 0.3.0 |
|---|---|
tx = tool.invoke(...) → bare tx dict |
plan = tool.invoke(...) → plan dict |
sign(tx) |
for tx in plan["transactions"]: sign(tx) |
caller calls approve_token separately |
approval is plan["calls"][0], already sized |
caller manages nonce collisions |
nonces pre-assigned across the plan |
| caller re-quotes to describe the swap | plan["summary"] |
| build raises when allowance missing | builds fine; gas_estimated: False flags fallbacks |
Minimal EOA diff:
-tx = tools["swap_exact_tokens_for_tokens"].invoke({...})
-signed = acct.sign_transaction(tx)
-w3.eth.send_raw_transaction(signed.raw_transaction)
+plan = tools["swap_exact_tokens_for_tokens"].invoke({...})
+for tx in plan["transactions"]:
+ tx.pop("gas_estimated", None)
+ signed = acct.sign_transaction(tx)
+ w3.eth.send_raw_transaction(signed.raw_transaction)
Development
pip install -e ".[dev]"
pytest
ruff check .
ruff format --check .
The test suite mocks all contract calls (see tests/web3_mocks.py) — it
makes no live network calls, so it runs fast and doesn't depend on any RPC
endpoint being up.
langchain_uniswap_v2/abis.py is machine-generated from the original ABI
JSON and verified byte-for-byte against it — it's excluded from Ruff so
lint/format never churns it.
CI (.github/workflows/ci.yml) runs ruff check/ruff format --check and
the test suite (Python 3.10–3.12) on every push and pull request.
scripts/verify_native_wrapped.py checks the registry against the live
chains — router bytecode present, WETH() and factory() matching the
table — and reports each wrapped native's symbol and decimals. It runs
weekly via .github/workflows/verify-networks.yml rather than on pull
requests, so PR CI stays offline and deterministic. An unreachable endpoint
is a skip; a mismatch fails. Point it at your own endpoints with
UNISWAP_V2_RPC_<chain_id>:
python scripts/verify_native_wrapped.py # every entry
python scripts/verify_native_wrapped.py --chain 1 # just one
UNISWAP_V2_RPC_137=https://your-endpoint python scripts/verify_native_wrapped.py --chain 137
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langchain_uniswap_v2-0.4.0.tar.gz.
File metadata
- Download URL: langchain_uniswap_v2-0.4.0.tar.gz
- Upload date:
- Size: 40.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d09082c4ae545628b15ee22c57eef5eeccda8efb4d943e90562476615aafb7f7
|
|
| MD5 |
6940cf33979c6e9e7df974f99ef95871
|
|
| BLAKE2b-256 |
10664ab1d3e5d7f4299c42be25817d0fef5f7302356009348203349e86a9bb7d
|
File details
Details for the file langchain_uniswap_v2-0.4.0-py3-none-any.whl.
File metadata
- Download URL: langchain_uniswap_v2-0.4.0-py3-none-any.whl
- Upload date:
- Size: 33.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9eb291730df455f5c5c2e38bf1495af4a52a224fc7c604d27396c3b6ae6e97b
|
|
| MD5 |
21d7f2b01553a160236fd07b48ac862a
|
|
| BLAKE2b-256 |
4e22ab73c45b9d3d9d3eee7b93e8a5383c2550fddad9cbd7ebc5601b2c3e6360
|