Developers
Advanced Swaps
Integration covers the basic flow: quote one pool, sell an exact input, settle and take. This page extends it to the other swaps the app makes: buying an exact output, routing through two pools, and paying or receiving native ETH. The examples import setup.ts from Integration and build the same transactions as the app's own encoder; the viem and ethers v6 tabs send identical transactions.
The app routes up to two hops and sends one route per swap. Both exact input and exact output use the same Universal Router call, execute with an INFI_SWAP command (0x10); only the plan inside it changes.
Exact Output
An exact-output swap fixes the amount you receive and lets the amount you pay float. The app uses it whenever you type into the Buy field.
Quote. CLQuoter.quoteExactOutput takes the output currency as exactCurrency, the output amount as exactAmount, and a path, and returns the input the swap needs. The app calls this path form even for one pool; quoteExactOutputSingle takes a pool key and direction instead and returns the same amount.
Path order. A path is a list of PathKeys, one per pool: the pool's fee, hooks, pool manager, parameters and hook data, plus intermediateCurrency, the currency on the far side of the hop. The pool is the one formed by that currency and its neighbour, so a PathKey never repeats both currencies. For an exact-output path, each intermediateCurrency is the currency the hop takes in, listed from the input end: to buy BTC with USDC, the single entry names USDC.
import { formatUnits, parseAbi, parseUnits } from "viem";
import { BTC, CONTRACTS, POOL_KEY, publicClient, USDC } from "./setup";
const quoterAbi = parseAbi([
"struct PathKey { address intermediateCurrency; uint24 fee; address hooks; address poolManager; bytes hookData; bytes32 parameters; }",
"struct QuoteExactParams { address exactCurrency; PathKey[] path; uint128 exactAmount; }",
"function quoteExactOutput(QuoteExactParams params) returns (uint256 amountIn, uint256 gasEstimate)",
]);
export const amountOut = parseUnits("0.001", 8); // buy exactly 0.001 BTC
// An exact-output path lists each hop by the currency it takes in. One hop: USDC into BTC/USDC.
export const path = [
{
intermediateCurrency: USDC,
fee: POOL_KEY.fee,
hooks: POOL_KEY.hooks,
poolManager: POOL_KEY.poolManager,
hookData: "0x",
parameters: POOL_KEY.parameters,
},
] as const;
// exactCurrency is the output you fix. The quoter reverts internally, so call it, never send it.
const { result } = await publicClient.simulateContract({
address: CONTRACTS.clQuoter,
abi: quoterAbi,
functionName: "quoteExactOutput",
args: [{ exactCurrency: BTC, path, exactAmount: amountOut }],
});
export const amountIn = result[0];
console.log("0.001 BTC costs", formatUnits(amountIn, 6), "USDC");The plan. CL_SWAP_EXACT_OUT (0x09) takes currencyOut, the path, amountOut and amountInMaximum. Slippage moves to the input side: amountInMaximum is the quote plus your tolerance, rounded up, and the swap reverts if it would cost more.
SETTLE_ALL (0x0c) is given the same cap. It pays the Vault the swap's actual debt, reverting if that exceeds the cap, so Permit2 pulls only what the swap cost and nothing needs refunding. The Permit2 allowance must cover the cap, not the quote. TAKE with an amount of 0 withdraws the whole output, which is exactly amountOut.
The app sends exact-output swaps on one route only; it does not split them across pools.
import { encodeAbiParameters, erc20Abi, maxUint256, parseAbi, parseAbiParameters, type Hex } from "viem";
import { BTC, CONTRACTS, publicClient, USDC, walletClient } from "./setup";
import { amountIn, amountOut, path } from "./quote-exact-output";
const permit2Abi = parseAbi([
"function approve(address token, address spender, uint160 amount, uint48 expiration)",
]);
const routerAbi = parseAbi([
"function execute(bytes commands, bytes[] inputs, uint256 deadline) payable",
]);
// The input is what floats, so slippage caps it: the quote plus 0.5%, rounded up.
const slippageBps = 50n;
const amountInMaximum = (amountIn * (10_000n + slippageBps) + 9_999n) / 10_000n;
const block = await publicClient.getBlock();
// 1. Let Permit2 move USDC. Once per token; skip it when the allowance already covers you.
const approveHash = await walletClient.writeContract({
address: USDC,
abi: erc20Abi,
functionName: "approve",
args: [CONTRACTS.permit2, maxUint256],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
// 2. Let the Universal Router pull up to the cap through Permit2, for 30 days.
const permitHash = await walletClient.writeContract({
address: CONTRACTS.permit2,
abi: permit2Abi,
functionName: "approve",
args: [USDC, CONTRACTS.universalRouter, amountInMaximum, Number(block.timestamp + 30n * 86_400n)],
});
await publicClient.waitForTransactionReceipt({ hash: permitHash });
// 3. The plan: swap for an exact output, pay what it actually cost (at most the cap), take the BTC.
const CL_SWAP_EXACT_OUT = "09";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"
const swap = encodeAbiParameters(
parseAbiParameters(
"(address currencyOut, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountOut, uint128 amountInMaximum)",
),
[{ currencyOut: BTC, path, amountOut, amountInMaximum }],
);
const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [USDC, amountInMaximum]);
const take = encodeAbiParameters(parseAbiParameters("address, address, uint256"), [
BTC,
MSG_SENDER,
0n,
]);
const plan = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [
("0x" + CL_SWAP_EXACT_OUT + SETTLE_ALL + TAKE) as Hex,
[swap, settle, take],
]);
// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const hash = await walletClient.writeContract({
address: CONTRACTS.universalRouter,
abi: routerAbi,
functionName: "execute",
args: [INFI_SWAP, [plan], block.timestamp + 1_200n],
});
console.log("swap:", (await publicClient.waitForTransactionReceipt({ hash })).status);Multi-Hop
A multi-hop swap passes through more than one pool in a single swap action. Every seeded pool pairs a token with USDC, so BTC to AAPL routes BTC → USDC through BTC/USDC 0.30%, then USDC → AAPL through AAPL/USDC 0.30%. The app searches routes of up to two hops and quotes each candidate.
Path order. For exact input, each intermediateCurrency is the currency the hop produces, listed from the input end: USDC, then AAPL. The same path array serves quoteExactInput and CL_SWAP_EXACT_IN. For exact output it is the input of each hop, still listed from the input end: buying AAPL with BTC uses BTC, then USDC, with AAPL as exactCurrency. The router walks an exact-output path backwards from the output.
import { formatUnits, parseAbi, parseUnits } from "viem";
import { BTC, CONTRACTS, POOL_KEY, publicClient, USDC } from "./setup";
export const AAPL = "0x410432915aB0c6BA27d3B4A86B1E01b62942219F"; // 18 decimals
// The AAPL/USDC 0.30% pool. AAPL sorts below USDC, so it is currency0 here.
export const AAPL_POOL_KEY = {
currency0: AAPL,
currency1: USDC,
hooks: "0x0000000000000000000000000000000000000000",
poolManager: CONTRACTS.clPoolManager,
fee: 2011,
parameters: "0x00000000000000000000000000000000000000000000000000000000003c0000",
} as const;
const quoterAbi = parseAbi([
"struct PathKey { address intermediateCurrency; uint24 fee; address hooks; address poolManager; bytes hookData; bytes32 parameters; }",
"struct QuoteExactParams { address exactCurrency; PathKey[] path; uint128 exactAmount; }",
"function quoteExactInput(QuoteExactParams params) returns (uint256 amountOut, uint256 gasEstimate)",
]);
export const amountIn = parseUnits("0.001", 8); // sell 0.001 BTC
// An exact-input path lists each hop by the currency it produces: BTC -> USDC, then USDC -> AAPL.
export const path = [
{
intermediateCurrency: USDC,
fee: POOL_KEY.fee,
hooks: POOL_KEY.hooks,
poolManager: POOL_KEY.poolManager,
hookData: "0x",
parameters: POOL_KEY.parameters,
},
{
intermediateCurrency: AAPL,
fee: AAPL_POOL_KEY.fee,
hooks: AAPL_POOL_KEY.hooks,
poolManager: AAPL_POOL_KEY.poolManager,
hookData: "0x",
parameters: AAPL_POOL_KEY.parameters,
},
] as const;
const { result } = await publicClient.simulateContract({
address: CONTRACTS.clQuoter,
abi: quoterAbi,
functionName: "quoteExactInput",
args: [{ exactCurrency: BTC, path, exactAmount: amountIn }],
});
export const amountOut = result[0];
console.log("0.001 BTC ->", formatUnits(amountOut, 18), "AAPL");Settlement. The swap action runs both hops against the Vault's running balances. The first hop credits the plan with USDC and the second hop spends exactly that USDC, so it nets to zero and is never transferred. Only the first currency is settled and the last taken: SETTLE_ALL pays the BTC and TAKE withdraws the AAPL.
Slippage. amountOutMinimum applies to the final output only. Intermediate amounts have no limit of their own; a move in either pool shows up in the AAPL you receive.
import { encodeAbiParameters, erc20Abi, maxUint256, parseAbi, parseAbiParameters, type Hex } from "viem";
import { BTC, CONTRACTS, publicClient, walletClient } from "./setup";
import { AAPL, amountIn, amountOut, path } from "./quote-multi-hop";
const permit2Abi = parseAbi([
"function approve(address token, address spender, uint160 amount, uint48 expiration)",
]);
const routerAbi = parseAbi([
"function execute(bytes commands, bytes[] inputs, uint256 deadline) payable",
]);
const block = await publicClient.getBlock();
// 1. Let Permit2 move BTC. Once per token; skip it when the allowance already covers you.
const approveHash = await walletClient.writeContract({
address: BTC,
abi: erc20Abi,
functionName: "approve",
args: [CONTRACTS.permit2, maxUint256],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
// 2. Let the Universal Router pull this amount through Permit2, for 30 days.
const permitHash = await walletClient.writeContract({
address: CONTRACTS.permit2,
abi: permit2Abi,
functionName: "approve",
args: [BTC, CONTRACTS.universalRouter, amountIn, Number(block.timestamp + 30n * 86_400n)],
});
await publicClient.waitForTransactionReceipt({ hash: permitHash });
// 3. The plan: one swap action walks both hops; pay the BTC, take the AAPL.
// The USDC between the hops nets to zero inside the Vault and is never settled.
const CL_SWAP_EXACT_IN = "07";
const SETTLE_ALL = "0c";
const TAKE = "0e";
const MSG_SENDER = "0x0000000000000000000000000000000000000001"; // the router reads this as "the caller"
const slippageBps = 50n; // 0.5%, on the final output only
const amountOutMinimum = (amountOut * (10_000n - slippageBps)) / 10_000n;
const swap = encodeAbiParameters(
parseAbiParameters(
"(address currencyIn, (address intermediateCurrency, uint24 fee, address hooks, address poolManager, bytes hookData, bytes32 parameters)[] path, uint128 amountIn, uint128 amountOutMinimum)",
),
[{ currencyIn: BTC, path, amountIn, amountOutMinimum }],
);
const settle = encodeAbiParameters(parseAbiParameters("address, uint256"), [BTC, amountIn]);
const take = encodeAbiParameters(parseAbiParameters("address, address, uint256"), [
AAPL,
MSG_SENDER,
0n,
]);
const plan = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [
("0x" + CL_SWAP_EXACT_IN + SETTLE_ALL + TAKE) as Hex,
[swap, settle, take],
]);
// 4. One INFI_SWAP (0x10) command carrying the plan. Reverts after 20 minutes.
const INFI_SWAP = "0x10";
const hash = await walletClient.writeContract({
address: CONTRACTS.universalRouter,
abi: routerAbi,
functionName: "execute",
args: [INFI_SWAP, [plan], block.timestamp + 1_200n],
});
console.log("swap:", (await publicClient.waitForTransactionReceipt({ hash })).status);Native ETH
None of the seeded pools uses native ETH or WETH9, so the examples above trade only ERC-20 tokens. Infinity supports both, and the app's encoder handles both, choosing the plan from the pool's currency.
Native currency. In a pool key, native ETH is Currency 0x0000…0000. It is always currency0, since no address sorts below zero. It never goes through Permit2: you pay it as the transaction's value, and the router settles the Vault from that. The app sends value equal to amountIn for an exact-input swap and amountInMaximum for an exact-output one.
WETH9 is a different pool. WETH9 (0x4200…0006) is an ordinary ERC-20. An ETH/USDC pool and a WETH9/USDC pool have different keys, different IDs and separate liquidity. To pay ETH into a WETH9 pool, the router wraps it first.
What the app sends. Universal Router commands and plan actions are separate ID spaces, so 0x0b and 0x0c below each mean two things.
- ETH in, native pool:
INFI_SWAPwithSETTLE_ALLon0x0, paid fromvalue. For exact output, a trailingSWEEPcommand (0x04) on0x0returns the ETH the swap did not use. - ETH in, WETH9 pool:
WRAP_ETH(0x0b) wrapsvalueinto the router's own balance, then the plan usesSETTLE(0x0b) on WETH9 with amount0(the full debt) andpayerIsUserfalse, paying from that balance. For exact output, a trailingUNWRAP_WETH(0x0c) returns the unused WETH9 as ETH. - ETH out, native pool:
TAKEon0x0sends ETH straight to you. - ETH out, WETH9 pool:
TAKEsends WETH9 to the router (ADDRESS_THIS,0x…02), thenUNWRAP_WETHunwraps it to you, with the minimum output (exact input) or the exact amount (exact output) as its floor.
Without the trailing SWEEP or UNWRAP_WETH, unused ETH stays in the router, where the next caller can sweep it.