> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Launch lifecycle

> End to end for builders: read the registry, create a launch, quote and trade through the router, detect graduation, read the pool price and follow events, with viem and cast.

This walkthrough takes a launch from creation to its locked Uniswap v4 pool using `@launchpad/abis` and viem, with `cast` equivalents where they are short. Addresses are Robinhood Chain (4663); swap in another chain's [addresses](/protocol/addresses) for the testnets. Every amount convention used here is defined in [Units](/protocol/units).

## Setup

<CodeGroup>
  ```ts viem theme={"system"}
  import { createPublicClient, createWalletClient, defineChain, http, parseUnits, stringToHex, parseEventLogs } from "viem";
  import { privateKeyToAccount } from "viem/accounts";
  import { currencyRegistryAbi, launchFactoryAbi, bondingCurveAbi, routerAbi, fxVaultAbi, stateViewAbi, erc20Abi, getChain } from "@launchpad/abis";
  import deployment from "@launchpad/abis/deployments/4663.json";

  const info = getChain(4663);
  const chain = defineChain({
    id: info.id, name: info.name, nativeCurrency: info.nativeCurrency,
    rpcUrls: { default: { http: [info.rpc] } },
    blockExplorers: { default: { name: "Blockscout", url: info.explorer } },
  });
  const publicClient = createPublicClient({ chain, transport: http() });
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
  const walletClient = createWalletClient({ account, chain, transport: http() });

  const { registry, factory, router, stateView, usdc: usdg } = deployment; // usdc = the chain's dollar (USDG here)
  const INR = stringToHex("INR", { size: 32 });
  ```

  ```bash cast theme={"system"}
  export RPC=https://rpc.mainnet.chain.robinhood.com
  export REGISTRY=0xc3A0ac0eF92392A37287AfE99D401679BA00988a
  export FACTORY=0x3057566F58742B113Db615b2cb0E1085E766Ac54
  export ROUTER=0xae4E359895B7a232eEA55238256dcbDa9851c1CF
  export STATE_VIEW=0xF3334192D15450CdD385c8B70e03f9A6bD9E673b
  export USDG=0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
  export INR=$(cast --format-bytes32-string INR)
  # sign with an encrypted keystore: cast wallet import windrose --interactive
  ```
</CodeGroup>

`packages/abis/deployments/<chainId>.json` is written by the deploy script; the same data is served at `https://windrose.market/api/deployment?chainId=4663`. Gas: the registry's `gas` field gives `maxFeeGwei` / `priorityGwei` to spread into writes (Arc needs at least 20 gwei; Robinhood Chain is happy with the defaults).

## 1. Read the currency

<CodeGroup>
  ```ts viem theme={"system"}
  const cc = await publicClient.readContract({ address: registry, abi: currencyRegistryAbi, functionName: "requireEnabled", args: [INR] });
  // cc.quoteAsset = wINR, cc.quoteDecimals = 18, cc.tier = 2, cc.vault = the wINR FxVault, cc.enabled = true

  const oracle = await publicClient.readContract({ address: registry, abi: currencyRegistryAbi, functionName: "oracle" });
  const [rate, publishTime] = await publicClient.readContract({
    address: oracle, abi: [{ type: "function", name: "peekRate", stateMutability: "view", inputs: [{ type: "bytes32" }], outputs: [{ type: "uint256" }, { type: "uint256" }] }],
    functionName: "peekRate", args: [INR],
  }); // rate = INR per USD, 1e18
  ```

  ```bash cast theme={"system"}
  cast call $REGISTRY "requireEnabled(bytes32)((address,uint8,uint8,address,bool))" $INR --rpc-url $RPC
  ORACLE=$(cast call $REGISTRY "oracle()(address)" --rpc-url $RPC)
  cast call $ORACLE "peekRate(bytes32)(uint256,uint256)" $INR --rpc-url $RPC
  ```
</CodeGroup>

`keeperFxOracleAbi` from `@launchpad/abis` has `peekRate` and `rate` too; the inline ABI above works against any `IFxOracle`.

## 2. Create a launch

A launch needs a name, a symbol, a metadata URI, the currency code and, optionally, the creator's first buy (`initialIn`). With `payInUsdc = true` on a synthetic currency the factory mints the synthetic from dollars inside the call; otherwise `initialIn` is in the quote asset.

To set `minTokensOut` for that first buy, reproduce the curve maths off-chain from the factory config and the oracle rate (the curve does not exist yet):

```ts theme={"system"}
const cfg = await publicClient.readContract({ address: factory, abi: launchFactoryAbi, functionName: "config" });
const virtualQuote = (cfg.virtualQuoteUsd * rate) / 10n ** 18n;                    // 18-dec quote units
const quoteScale = 10n ** BigInt(18 - cc.quoteDecimals);
const initialIn = parseUnits("50", 6);                                            // 50 USDG
const [synthOut] = await publicClient.readContract({ address: cc.vault, abi: fxVaultAbi, functionName: "previewMint", args: [initialIn] });
const netRaw = synthOut - (synthOut * BigInt(cfg.tradeFeeBps)) / 10000n;
const net18 = netRaw * quoteScale;
const x = cfg.virtualToken + cfg.curveSupply;
const tokensOut = (x * net18) / (virtualQuote + net18);
const minTokensOut = (tokensOut * 98n) / 100n;                                    // 2% tolerance
```

<CodeGroup>
  ```ts viem theme={"system"}
  const metadataURI = "data:application/json;base64," + Buffer.from(JSON.stringify({
    name: "Chai Coin", description: "Tea money", image: "data:image/webp;base64,...", // the app keeps the image under 20 KB
  })).toString("base64");

  await walletClient.writeContract({ address: usdg, abi: erc20Abi, functionName: "approve", args: [factory, initialIn] });

  const hash = await walletClient.writeContract({
    address: factory, abi: launchFactoryAbi, functionName: "createLaunch",
    args: [{ name: "Chai Coin", symbol: "CHAI", metadataURI, code: INR, initialIn, minTokensOut, payInUsdc: true }, []],
    value: 0n, // keeper oracle: no update fee
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  const [created] = parseEventLogs({ abi: launchFactoryAbi, eventName: "LaunchCreated", logs: receipt.logs });
  const { token, curve, index } = created.args;
  ```

  ```bash cast theme={"system"}
  cast send $USDG "approve(address,uint256)" $FACTORY 50000000 --rpc-url $RPC --account windrose
  cast send $FACTORY "createLaunch((string,string,string,bytes32,uint256,uint256,bool),bytes[])" \
    "(Chai Coin,CHAI,data:application/json;base64,e30=,$INR,50000000,$MIN_TOKENS_OUT,true)" "[]" \
    --rpc-url $RPC --account windrose
  # the LaunchCreated log carries index, token and curve; or:
  cast call $FACTORY "launchCount()(uint256)" --rpc-url $RPC
  cast call $FACTORY "getLaunch(uint256)((address,address,address,bytes32,uint64,string))" $INDEX --rpc-url $RPC
  ```
</CodeGroup>

Skip the first buy with `initialIn = 0` and `minTokensOut = 0` (no approval needed). A EUR launch on Arc pays its first buy in EURC with `payInUsdc = false`.

## 3. Quote and trade on the curve

Read the curve state once and keep it fresh from events or polling:

```ts theme={"system"}
const curveState = await publicClient.multicall({ contracts: [
  { address: curve, abi: bondingCurveAbi, functionName: "spotPrice" },
  { address: curve, abi: bondingCurveAbi, functionName: "progressBps" },
  { address: curve, abi: bondingCurveAbi, functionName: "tokensRemaining" },
  { address: curve, abi: bondingCurveAbi, functionName: "realQuote" },
  { address: curve, abi: bondingCurveAbi, functionName: "graduationQuote" },
  { address: curve, abi: bondingCurveAbi, functionName: "ready" },
  { address: curve, abi: bondingCurveAbi, functionName: "graduated" },
]});
```

### Buy with dollars (any synthetic launch, or a USD launch)

<CodeGroup>
  ```ts viem theme={"system"}
  const usdcIn = parseUnits("25", 6);
  const [tokensOut] = await publicClient.readContract({ address: router, abi: routerAbi, functionName: "quoteBuyWithUSDC", args: [curve, usdcIn] });
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);

  await walletClient.writeContract({ address: usdg, abi: erc20Abi, functionName: "approve", args: [router, usdcIn] });
  await walletClient.writeContract({
    address: router, abi: routerAbi, functionName: "buyWithUSDC",
    args: [curve, usdcIn, (tokensOut * 98n) / 100n, deadline, []], value: 0n,
  });
  ```

  ```bash cast theme={"system"}
  cast call $ROUTER "quoteBuyWithUSDC(address,uint256)(uint256,uint256,uint256)" $CURVE 25000000 --rpc-url $RPC
  cast send $USDG "approve(address,uint256)" $ROUTER 25000000 --rpc-url $RPC --account windrose
  cast send $ROUTER "buyWithUSDC(address,uint256,uint256,uint256,bytes[])" $CURVE 25000000 $MIN_OUT $(( $(date +%s) + 600 )) "[]" --rpc-url $RPC --account windrose
  ```
</CodeGroup>

### Buy with the quote asset (wINR held in the wallet, or EURC)

```ts theme={"system"}
const quoteIn = parseUnits("2000", cc.quoteDecimals); // 2,000 wINR
const [tokensOut2] = await publicClient.readContract({ address: curve, abi: bondingCurveAbi, functionName: "quoteBuy", args: [quoteIn] });
await walletClient.writeContract({ address: cc.quoteAsset, abi: erc20Abi, functionName: "approve", args: [router, quoteIn] });
await walletClient.writeContract({ address: router, abi: routerAbi, functionName: "buy", args: [curve, quoteIn, (tokensOut2 * 98n) / 100n, deadline] });
```

### Sell

```ts theme={"system"}
const tokensIn = parseUnits("1000000", 18);
await walletClient.writeContract({ address: token, abi: erc20Abi, functionName: "approve", args: [router, tokensIn] });

// for dollars
const [usdcOut] = await publicClient.readContract({ address: router, abi: routerAbi, functionName: "quoteSellForUSDC", args: [curve, tokensIn] });
await walletClient.writeContract({ address: router, abi: routerAbi, functionName: "sellForUSDC", args: [curve, tokensIn, (usdcOut * 98n) / 100n, deadline, []], value: 0n });

// or for the quote asset
const [quoteOut] = await publicClient.readContract({ address: curve, abi: bondingCurveAbi, functionName: "quoteSell", args: [tokensIn] });
await walletClient.writeContract({ address: router, abi: routerAbi, functionName: "sell", args: [curve, tokensIn, (quoteOut * 98n) / 100n, deadline] });
```

The last buy before graduation is capped: the router returns the untaken part in the asset you paid with. Quotes revert with `StalePrice` on the dollar paths when the oracle rate is older than 5 days; the quote-asset paths never touch the oracle. Full semantics: [Router](/protocol/contracts/router).

## 4. Detect readiness and graduate

`ready()` flips inside the buy that reaches `graduationQuote` (or sells out the curve) and `ReadyToGraduate` is emitted. The router immediately tries `graduate()`; if that inner call failed (`graduated()` still false while `ready()` is true), anyone can call it:

<CodeGroup>
  ```ts viem theme={"system"}
  const [ready, graduated] = await Promise.all([
    publicClient.readContract({ address: curve, abi: bondingCurveAbi, functionName: "ready" }),
    publicClient.readContract({ address: curve, abi: bondingCurveAbi, functionName: "graduated" }),
  ]);
  if (ready && !graduated) {
    await walletClient.writeContract({ address: curve, abi: bondingCurveAbi, functionName: "graduate" });
  }
  ```

  ```bash cast theme={"system"}
  cast call $CURVE "ready()(bool)" --rpc-url $RPC
  cast call $CURVE "graduated()(bool)" --rpc-url $RPC
  cast send $CURVE "graduate()" --rpc-url $RPC --account windrose
  ```
</CodeGroup>

`Graduated(poolId, positionTokenId, lpQuote, lpTokens, burned)` records the pool id and the locked position. From then on `buy`, `sell` and the quote functions are closed; the creator can still `claimCreatorFees` and anyone can `sweepProtocolFees`.

## 5. Read the pool after graduation

The pool key is `{ currency0: min(token, quoteAsset), currency1: max(token, quoteAsset), fee: 3000, tickSpacing: 60, hooks: 0x0 }`. `poolId()` on the curve is its id. `StateView.getSlot0(poolId)` returns `sqrtPriceX96`, which encodes `sqrt(rawAmount1 / rawAmount0)`:

```ts theme={"system"}
const poolId = await publicClient.readContract({ address: curve, abi: bondingCurveAbi, functionName: "poolId" });
const [sqrtPriceX96] = await publicClient.readContract({ address: stateView, abi: stateViewAbi, functionName: "getSlot0", args: [poolId] });

const Q192 = 2n ** 192n;
const WAD = 10n ** 18n;
const tokenIsCurrency0 = token.toLowerCase() < cc.quoteAsset.toLowerCase();
const p2 = sqrtPriceX96 * sqrtPriceX96;
// quote units (18-dec) per whole token, 1e18 fixed point: the same unit as spotPrice()
const priceQuote = tokenIsCurrency0 ? (p2 * WAD * quoteScale) / Q192 : (Q192 * WAD * quoteScale) / p2;
const priceUsd = (priceQuote * WAD) / rate;
```

Swaps after graduation go through Uniswap v4 (the UniversalRouter or a direct `PoolManager` unlock); Windrose's router only serves the curve phase. The locked position and fee collection are described in [LiquidityLocker](/protocol/contracts/liquidity-locker).

## 6. Follow events

```ts theme={"system"}
publicClient.watchContractEvent({ address: factory, abi: launchFactoryAbi, eventName: "LaunchCreated", onLogs: (logs) => { /* new launches */ } });
publicClient.watchContractEvent({ address: curve, abi: bondingCurveAbi, eventName: "Buy", onLogs: (logs) => {
  for (const { args } of logs) {
    // args.tokensSold and args.realQuote are the post-trade totals: recompute the spot price without a call
  }
}});
publicClient.watchContractEvent({ address: curve, abi: bondingCurveAbi, eventName: "Graduated", onLogs: () => { /* switch to the pool */ } });
```

For history (trades, candles, holders, rates) query the indexer instead of replaying logs: see the [API](/api) and the [Launches query](/api/queries/launches). Chain reads are the source of truth for the current state; the app falls back to them whenever the indexer is unreachable.

## Checklist for a correct integration

* Never assume quote decimals: read `quoteDecimals` from the registry (6 for the dollar and EURC, 18 for synthetics).
* Apply your own slippage to a fresh quote; the dollar paths have no oracle-side floor.
* Pass `[]` and `value: 0n` for `priceUpdate` while the registry oracle is the keeper oracle.
* Use timestamp deadlines, and on Arc bid at least 20 gwei.
* Treat `ready && !graduated` as a state you may have to resolve yourself with `graduate()`.
* Fees accrue in the quote asset (wINR for an INR launch); claiming them does not convert to dollars.
