Skip to main content

langchain-uniswap-v2

CI PyPI

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 renders plan["transactions"]: the same calls as unsigned, signable transactions with sequential nonces already assigned. Sign and broadcast them in order.
  • tx_mode="calls" returns calls only (transactions is None) 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_wrapped is now populated for all supported chains (previously None on 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 own WETH().

Fixed

  • for_chain() no longer raises TypeError when a caller overrides router_address, factory_address or native_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

langchain_uniswap_v2-0.4.0.tar.gz (40.3 kB view details)

Uploaded Source

Built Distribution

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

langchain_uniswap_v2-0.4.0-py3-none-any.whl (33.1 kB view details)

Uploaded Python 3

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

Hashes for langchain_uniswap_v2-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d09082c4ae545628b15ee22c57eef5eeccda8efb4d943e90562476615aafb7f7
MD5 6940cf33979c6e9e7df974f99ef95871
BLAKE2b-256 10664ab1d3e5d7f4299c42be25817d0fef5f7302356009348203349e86a9bb7d

See more details on using hashes here.

File details

Details for the file langchain_uniswap_v2-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for langchain_uniswap_v2-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f9eb291730df455f5c5c2e38bf1495af4a52a224fc7c604d27396c3b6ae6e97b
MD5 21d7f2b01553a160236fd07b48ac862a
BLAKE2b-256 4e22ab73c45b9d3d9d3eee7b93e8a5383c2550fddad9cbd7ebc5601b2c3e6360

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.1

2 files

0.5.0

2 files

This release

0.4.0 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

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