> Bull Launch docs. Every page is available as Markdown by adding `.md` to its URL; the index is https://bulllaunch.fun/llms.txt. This page on the web: https://bulllaunch.fun/docs/quotes-and-swaps

# Quotes and swaps

Quote a trade with V4Quoter and execute it through the Universal Router, with a tested example.

Trading a Bull Launch coin is an ordinary Uniswap v4 swap on the coin's pool. Quote it with `V4Quoter`, then send it through the `UniversalRouter` as one `V4_SWAP` command. Both addresses are under [Contract addresses](https://bulllaunch.fun/docs/contracts.md).

> **Note:** Use the UniversalRouter listed for the chain. Each router is connected to one PoolManager, and a router on another PoolManager, even on the same chain, cannot reach a Bull Launch pool.

## 1. Build the PoolKey

Every launch has the same key shape: the two currencies sorted by address, fee `0x800000` (the dynamic-fee flag), tick spacing 60, and the hook. `poolKeyOf` in [Reading token state](https://bulllaunch.fun/docs/reading-state.md) builds it. For a native-USDC coin, `currency0` is always `address(0)`, so a buy is `zeroForOne = true`. For an ERC-20 base, check which slot the base landed in, or read `Hook.baseIsCurrency0(poolId)`.

## 2. Quote

`V4Quoter.quoteExactInputSingle` returns `(amountOut, gasEstimate)`. It is not a view: call it with `eth_call` (viem's `simulateContract`), never in a transaction. It runs the hook, so the swap fee and the creator tax are already in the answer.

## 3. Swap

Encode three v4 actions, `SWAP_EXACT_IN_SINGLE` (`0x06`), `SETTLE_ALL` (`0x0c`) and `TAKE_ALL` (`0x0f`), wrap them in one `V4_SWAP` command (`0x10`), and call `UniversalRouter.execute(commands, inputs, deadline)`. `TAKE_ALL` enforces `amountOutMinimum`, so the router, not your code, is what holds the slippage bound.

> The swap parameters include `minHopPriceX36`, a per-hop price floor that is newer than most Uniswap quickstarts. Encoding the older tuple without it produces calldata the router decodes into garbage. Pass `0` to leave it unset.

```js
// Buy a native-USDC coin: quote, then swap. Tested against Arc Testnet.
import { createPublicClient, createWalletClient, http, encodeAbiParameters, encodePacked,
  parseAbi, parseAbiParameters, parseEther, zeroAddress } from "viem";

const arc = {
  id: 5042002,
  name: "Arc Testnet",
  nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 },
  rpcUrls: { default: { http: ["https://rpc.testnet.arc.io"] } },
  contracts: { multicall3: { address: "0xcA11bde05977b3631167028862bE2a173976CA11" } },
};
const client = createPublicClient({ chain: arc, transport: http() });
// const wallet = createWalletClient({ account, chain: arc, transport: http() });

const HOOK   = "0xe332196A3bf409899E990846373cf47255e7b044";
const QUOTER = "0xb07209ef64E49ef41E6DEC9B01D0dBa3248d09e8";
const ROUTER = "0x1be3dB20c64C02CD97DdD15c3A65C4F461A43eC8";
const TOKEN  = "0x…"; // the coin

const POOL_KEY = "struct PoolKey { address currency0; address currency1; uint24 fee; int24 tickSpacing; address hooks; }";
const quoterAbi = parseAbi([
  POOL_KEY,
  "struct QuoteExactSingleParams { PoolKey poolKey; bool zeroForOne; uint128 exactAmount; bytes hookData; }",
  "function quoteExactInputSingle(QuoteExactSingleParams params) returns (uint256 amountOut, uint256 gasEstimate)",
]);
const routerAbi = parseAbi(["function execute(bytes commands, bytes[] inputs, uint256 deadline) payable"]);
const SWAP_PARAMS = parseAbiParameters([
  "ExactInputSingleParams params",
  "struct ExactInputSingleParams { PoolKey poolKey; bool zeroForOne; uint128 amountIn; uint128 amountOutMinimum; uint256 minHopPriceX36; bytes hookData; }",
  POOL_KEY,
]);

const poolKey = { currency0: zeroAddress, currency1: TOKEN, fee: 0x800000, tickSpacing: 60, hooks: HOOK };
const amountIn = parseEther("0.01");

// 1. Quote.
const { result: [amountOut] } = await client.simulateContract({
  address: QUOTER, abi: quoterAbi, functionName: "quoteExactInputSingle",
  args: [{ poolKey, zeroForOne: true, exactAmount: amountIn, hookData: "0x" }],
});
const amountOutMinimum = (amountOut * 99n) / 100n; // 1% slippage

// 2. Swap: SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL inside one V4_SWAP.
const actions = encodePacked(["uint8", "uint8", "uint8"], [0x06, 0x0c, 0x0f]);
const params = [
  encodeAbiParameters(SWAP_PARAMS, [{ poolKey, zeroForOne: true, amountIn, amountOutMinimum,
    minHopPriceX36: 0n, hookData: account.address }]),
  encodeAbiParameters(parseAbiParameters("address, uint256"), [zeroAddress, amountIn]),
  encodeAbiParameters(parseAbiParameters("address, uint256"), [TOKEN, amountOutMinimum]),
];
const input = encodeAbiParameters(parseAbiParameters("bytes, bytes[]"), [actions, params]);
const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);

await wallet.writeContract({
  address: ROUTER, abi: routerAbi, functionName: "execute",
  args: ["0x10", [input], deadline],
  value: amountIn, // native USDC is paid as msg.value
});
```

## hookData: say who the trader is

The hook reads the first 20 bytes of `hookData` as the trader, and falls back to `tx.origin` when there are none. Behind a smart wallet, a relayer or an aggregator, `tx.origin` is not the trader, so pass the trader's address. It is what the `CurveBuy` and `CurveSell` events, and every feed built on them, attribute the trade to.

## Selling, and ERC-20 bases

- A sell is the same call with `zeroForOne = false` for a native-base coin: `SETTLE_ALL` names the coin, `TAKE_ALL` the base, and no `value` is sent.
- An ERC-20 input is pulled through Permit2. The token must `approve(Permit2)` once, and Permit2 must allow the router: sign a `PermitSingle` and put a `PERMIT2_PERMIT` command (`0x0a`) in front of `V4_SWAP` in the same `execute`, or call `Permit2.approve` on chain.
- A coin priced in one base can be bought with another by routing through the pool between them: `SWAP_EXACT_IN` (`0x07`) with a path. Its parameters are `(currencyIn, path, minHopPriceX36[], amountIn, amountOutMinimum)`, in that order; put the hop prices anywhere else and the router reverts with no data.
