Skip to content

TypeScript client

Copy one self-contained Viem client for Clank bonding-curve and Uniswap V4 trades.

Updated View as Markdown

Copy the file below into your application. It resolves the launch from the canonical factory, selects the bonding curve or graduated Uniswap V4 pool, and returns unsigned transaction instructions for your wallet flow.

There is no Clank package to install. The file depends only on Viem.

Copy the client

clank-client.tsts
import {
  encodeAbiParameters,
  encodeFunctionData,
  erc20Abi,
  maxUint256,
  parseAbi,
  zeroAddress,
  type Address,
  type Hex,
  type PublicClient,
} from "viem";

const CLANK_FACTORY = "0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2" as const;
const V4_QUOTER = "0x8dc178efb8111bb0973dd9d722ebeff267c98f94" as const;
const UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904" as const;
const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as const;

const BPS = 10_000n;
const MAX_UINT128 = (1n << 128n) - 1n;
const MAX_UINT160 = (1n << 160n) - 1n;
const MAX_UINT48 = (1n << 48n) - 1n;

const factoryAbi = parseAbi([
  "function getLaunchedToken(address launchToken) view returns ((address launchToken,address curve,address deployer,address creatorFeeRecipient,address pairToken,uint256 graduationThreshold,uint24 poolFee,int24 tickSpacing,uint16 creatorTaxBps,bool buybackEnabled,uint8 phase,uint256 sweptPairToken,uint256 sweptLaunchTokens,uint256 sweptAt,bool exists) launched)",
  "function initializationHook() view returns (address)",
]);

const curveAbi = parseAbi([
  "function state() view returns (uint8)",
  "function quoteBuyFor(address recipient,uint256 grossPairTokenIn) view returns (uint256 grossPairTokenUsed,uint256 netPairTokenIn,uint256 fee,uint256 launchTokensOut,uint256 pairTokenRefund)",
  "function quoteSell(uint256 launchTokensIn) view returns (uint256 grossPairTokenOut,uint256 netPairTokenOut,uint256 fee)",
  "function buy(uint256 pairTokenIn,uint256 minLaunchTokensOut,address recipient) payable returns (uint256 launchTokensOut)",
  "function sell(uint256 launchTokensIn,uint256 minPairTokenOut,address recipient) returns (uint256 pairTokenOut)",
]);

const quoterAbi = parseAbi([
  "function quoteExactInputSingle(((address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks) poolKey,bool zeroForOne,uint128 exactAmount,bytes hookData) params) returns (uint256 amountOut,uint256 gasEstimate)",
]);

const routerAbi = parseAbi([
  "function execute(bytes commands,bytes[] inputs,uint256 deadline) payable",
]);

const permit2Abi = parseAbi([
  "function allowance(address user,address token,address spender) view returns (uint160 amount,uint48 expiration,uint48 nonce)",
  "function approve(address token,address spender,uint160 amount,uint48 expiration)",
]);

export type ClankTradeVenue = "bonding-curve" | "uniswap-v4";
export type ClankTradeSide = "buy" | "sell";

export type ClankTradeQuote = {
  readonly venue: ClankTradeVenue;
  readonly side: ClankTradeSide;
  readonly launchToken: Address;
  readonly pairToken: Address;
  readonly requestedAmountIn: bigint;
  readonly amountIn: bigint;
  readonly amountOut: bigint;
  readonly minimumAmountOut: bigint;
  readonly fee: bigint | null;
  readonly refund: bigint;
  readonly recipient: Address | null;
};

export type ClankTradeInstruction = {
  readonly label: string;
  readonly to: Address;
  readonly data: Hex;
  readonly value?: bigint;
};

export type ClankTradeClient = {
  readonly venue: ClankTradeVenue;
  readonly launchToken: Address;
  readonly pairToken: Address;
  quoteBuy(options: {
    readonly pairTokenIn: bigint;
    readonly recipient: Address;
    readonly slippageBps?: number;
  }): Promise<ClankTradeQuote>;
  buildBuy(options: {
    readonly quote: ClankTradeQuote;
    readonly trader: Address;
    readonly recipient?: Address;
    readonly deadline?: bigint;
  }): Promise<readonly ClankTradeInstruction[]>;
  quoteSell(options: {
    readonly launchTokensIn: bigint;
    readonly trader?: Address;
    readonly slippageBps?: number;
  }): Promise<ClankTradeQuote>;
  buildSell(options: {
    readonly quote: ClankTradeQuote;
    readonly trader: Address;
    readonly recipient?: Address;
    readonly deadline?: bigint;
  }): Promise<readonly ClankTradeInstruction[]>;
};

type PoolKey = {
  readonly currency0: Address;
  readonly currency1: Address;
  readonly fee: number;
  readonly tickSpacing: number;
  readonly hooks: Address;
};

type Market = {
  readonly launchToken: Address;
  readonly pairToken: Address;
  readonly curve: Address;
  readonly state: number;
  readonly poolFee: number;
  readonly tickSpacing: number;
};

const minimumOutput = (amount: bigint, slippageBps = 100): bigint => {
  if (slippageBps < 0 || slippageBps >= Number(BPS)) {
    throw new Error("Slippage must be between 0 and 9,999 basis points");
  }
  return (amount * (BPS - BigInt(slippageBps))) / BPS;
};

const requirePositiveInput = (amount: bigint) => {
  if (amount <= 0n) throw new Error("Input amount must be positive");
};

const requireQuote = ({
  quote,
  venue,
  side,
  launchToken,
}: {
  readonly quote: ClankTradeQuote;
  readonly venue: ClankTradeVenue;
  readonly side: ClankTradeSide;
  readonly launchToken: Address;
}) => {
  if (
    quote.venue !== venue ||
    quote.side !== side ||
    quote.launchToken.toLowerCase() !== launchToken.toLowerCase()
  ) {
    throw new Error(`Expected a ${venue} ${side} quote for this launch`);
  }
};

const resolveMarket = async ({
  publicClient,
  launchToken,
}: {
  readonly publicClient: PublicClient;
  readonly launchToken: Address;
}): Promise<Market> => {
  const launch = await publicClient.readContract({
    address: CLANK_FACTORY,
    abi: factoryAbi,
    functionName: "getLaunchedToken",
    args: [launchToken],
  });
  if (!launch.exists) throw new Error("Not a Clank V2 launch token");

  const state = await publicClient.readContract({
    address: launch.curve,
    abi: curveAbi,
    functionName: "state",
  });

  return {
    launchToken,
    pairToken: launch.pairToken,
    curve: launch.curve,
    state,
    poolFee: launch.poolFee,
    tickSpacing: launch.tickSpacing,
  };
};

const buildCurveClient = ({
  publicClient,
  market,
}: {
  readonly publicClient: PublicClient;
  readonly market: Market;
}): ClankTradeClient => ({
  venue: "bonding-curve",
  launchToken: market.launchToken,
  pairToken: market.pairToken,

  async quoteBuy({ pairTokenIn, recipient, slippageBps = 100 }) {
    requirePositiveInput(pairTokenIn);
    const [amountIn, , fee, amountOut, refund] =
      await publicClient.readContract({
        address: market.curve,
        abi: curveAbi,
        functionName: "quoteBuyFor",
        args: [recipient, pairTokenIn],
      });
    if (amountIn <= 0n || amountOut <= 0n) {
      throw new Error("The bonding curve returned no buy output");
    }

    return {
      venue: "bonding-curve",
      side: "buy",
      launchToken: market.launchToken,
      pairToken: market.pairToken,
      requestedAmountIn: pairTokenIn,
      amountIn,
      amountOut,
      minimumAmountOut: minimumOutput(amountOut, slippageBps),
      fee,
      refund,
      recipient,
    };
  },

  async buildBuy({ quote, trader, recipient = trader }) {
    requireQuote({
      quote,
      venue: "bonding-curve",
      side: "buy",
      launchToken: market.launchToken,
    });
    if (quote.recipient?.toLowerCase() !== recipient.toLowerCase()) {
      throw new Error("Buy recipient differs from the quoted recipient");
    }

    const minimumForOfferedAmount =
      quote.amountIn === quote.requestedAmountIn
        ? quote.minimumAmountOut
        : (quote.minimumAmountOut * quote.requestedAmountIn +
            quote.amountIn -
            1n) /
          quote.amountIn;

    const approvals =
      market.pairToken === zeroAddress
        ? []
        : await buildErc20Approval({
            publicClient,
            owner: trader,
            token: market.pairToken,
            spender: market.curve,
            amount: quote.requestedAmountIn,
            label: "Approve pair token",
          });

    return [
      ...approvals,
      {
        label: "Buy on bonding curve",
        to: market.curve,
        data: encodeFunctionData({
          abi: curveAbi,
          functionName: "buy",
          args: [quote.requestedAmountIn, minimumForOfferedAmount, recipient],
        }),
        ...(market.pairToken === zeroAddress
          ? { value: quote.requestedAmountIn }
          : {}),
      },
    ];
  },

  async quoteSell({ launchTokensIn, slippageBps = 100 }) {
    requirePositiveInput(launchTokensIn);
    const [, amountOut, fee] = await publicClient.readContract({
      address: market.curve,
      abi: curveAbi,
      functionName: "quoteSell",
      args: [launchTokensIn],
    });

    return {
      venue: "bonding-curve",
      side: "sell",
      launchToken: market.launchToken,
      pairToken: market.pairToken,
      requestedAmountIn: launchTokensIn,
      amountIn: launchTokensIn,
      amountOut,
      minimumAmountOut: minimumOutput(amountOut, slippageBps),
      fee,
      refund: 0n,
      recipient: null,
    };
  },

  async buildSell({ quote, trader, recipient = trader }) {
    requireQuote({
      quote,
      venue: "bonding-curve",
      side: "sell",
      launchToken: market.launchToken,
    });
    const approvals = await buildErc20Approval({
      publicClient,
      owner: trader,
      token: market.launchToken,
      spender: market.curve,
      amount: quote.amountIn,
      label: "Approve launch token",
    });

    return [
      ...approvals,
      {
        label: "Sell on bonding curve",
        to: market.curve,
        data: encodeFunctionData({
          abi: curveAbi,
          functionName: "sell",
          args: [quote.amountIn, quote.minimumAmountOut, recipient],
        }),
      },
    ];
  },
});

const buildErc20Approval = async ({
  publicClient,
  owner,
  token,
  spender,
  amount,
  label,
}: {
  readonly publicClient: PublicClient;
  readonly owner: Address;
  readonly token: Address;
  readonly spender: Address;
  readonly amount: bigint;
  readonly label: string;
}): Promise<readonly ClankTradeInstruction[]> => {
  const allowance = await publicClient.readContract({
    address: token,
    abi: erc20Abi,
    functionName: "allowance",
    args: [owner, spender],
  });
  return allowance >= amount
    ? []
    : [
        {
          label,
          to: token,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "approve",
            args: [spender, amount],
          }),
        },
      ];
};

const exactInputSingleType = {
  type: "tuple",
  components: [
    {
      name: "poolKey",
      type: "tuple",
      components: [
        { name: "currency0", type: "address" },
        { name: "currency1", type: "address" },
        { name: "fee", type: "uint24" },
        { name: "tickSpacing", type: "int24" },
        { name: "hooks", type: "address" },
      ],
    },
    { name: "zeroForOne", type: "bool" },
    { name: "amountIn", type: "uint128" },
    { name: "amountOutMinimum", type: "uint128" },
    { name: "minHopPriceX36", type: "uint256" },
    { name: "hookData", type: "bytes" },
  ],
} as const;

const currencyAmountTypes = [
  { name: "currency", type: "address" },
  { name: "amount", type: "uint256" },
] as const;

const encodeV4Route = ({
  poolKey,
  inputToken,
  amountIn,
  minimumAmountOut,
}: {
  readonly poolKey: PoolKey;
  readonly inputToken: Address;
  readonly amountIn: bigint;
  readonly minimumAmountOut: bigint;
}) => {
  const zeroForOne =
    inputToken.toLowerCase() === poolKey.currency0.toLowerCase();
  const swap = encodeAbiParameters(
    [exactInputSingleType],
    [
      {
        poolKey,
        zeroForOne,
        amountIn,
        amountOutMinimum: minimumAmountOut,
        minHopPriceX36: 0n,
        hookData: "0x",
      },
    ]
  );
  const outputToken = zeroForOne ? poolKey.currency1 : poolKey.currency0;
  const settleAll = encodeAbiParameters(currencyAmountTypes, [
    inputToken,
    amountIn,
  ]);
  const takeAll = encodeAbiParameters(currencyAmountTypes, [
    outputToken,
    minimumAmountOut,
  ]);
  const input = encodeAbiParameters(
    [
      { name: "actions", type: "bytes" },
      { name: "params", type: "bytes[]" },
    ],
    ["0x060c0f", [swap, settleAll, takeAll]]
  );
  return { commands: "0x10" as Hex, inputs: [input] as const };
};

const quoteV4 = async ({
  publicClient,
  poolKey,
  inputToken,
  amountIn,
  account,
  slippageBps,
}: {
  readonly publicClient: PublicClient;
  readonly poolKey: PoolKey;
  readonly inputToken: Address;
  readonly amountIn: bigint;
  readonly account: Address;
  readonly slippageBps: number;
}) => {
  requirePositiveInput(amountIn);
  if (amountIn > MAX_UINT128) {
    throw new Error("Input exceeds the Uniswap V4 uint128 limit");
  }
  const zeroForOne =
    inputToken.toLowerCase() === poolKey.currency0.toLowerCase();
  const { result } = await publicClient.simulateContract({
    account,
    address: V4_QUOTER,
    abi: quoterAbi,
    functionName: "quoteExactInputSingle",
    args: [
      {
        poolKey,
        zeroForOne,
        exactAmount: amountIn,
        hookData: "0x",
      },
    ],
  });
  const [amountOut] = result;
  const minimumAmountOut = minimumOutput(amountOut, slippageBps);
  if (minimumAmountOut > MAX_UINT128) {
    throw new Error("Minimum output exceeds the Uniswap V4 uint128 limit");
  }
  return {
    amountOut,
    minimumAmountOut,
  } as const;
};

const buildPermit2Approvals = async ({
  publicClient,
  trader,
  token,
  amount,
}: {
  readonly publicClient: PublicClient;
  readonly trader: Address;
  readonly token: Address;
  readonly amount: bigint;
}): Promise<readonly ClankTradeInstruction[]> => {
  if (token === zeroAddress) return [];
  if (amount > MAX_UINT160) {
    throw new Error("Input exceeds the Permit2 uint160 limit");
  }

  const [tokenAllowance, permitAllowance, block] = await Promise.all([
    publicClient.readContract({
      address: token,
      abi: erc20Abi,
      functionName: "allowance",
      args: [trader, PERMIT2],
    }),
    publicClient.readContract({
      address: PERMIT2,
      abi: permit2Abi,
      functionName: "allowance",
      args: [trader, token, UNIVERSAL_ROUTER],
    }),
    publicClient.getBlock({ blockTag: "latest" }),
  ]);

  const tokenApproval =
    tokenAllowance >= amount
      ? []
      : [
          {
            label: "Approve Permit2",
            to: token,
            data: encodeFunctionData({
              abi: erc20Abi,
              functionName: "approve",
              args: [PERMIT2, maxUint256],
            }),
          },
        ];
  const [permitAmount, permitExpiration] = permitAllowance;
  const routerApproval =
    permitAmount >= amount && permitExpiration > Number(block.timestamp)
      ? []
      : [
          {
            label: "Approve Universal Router",
            to: PERMIT2,
            data: encodeFunctionData({
              abi: permit2Abi,
              functionName: "approve",
              args: [token, UNIVERSAL_ROUTER, MAX_UINT160, Number(MAX_UINT48)],
            }),
          },
        ];
  return [...tokenApproval, ...routerApproval];
};

const resolveDeadline = async (
  publicClient: PublicClient,
  deadline?: bigint
): Promise<bigint> => {
  if (deadline !== undefined) return deadline;
  const block = await publicClient.getBlock({ blockTag: "latest" });
  return block.timestamp + 60n;
};

const buildV4Client = ({
  publicClient,
  market,
  poolKey,
}: {
  readonly publicClient: PublicClient;
  readonly market: Market;
  readonly poolKey: PoolKey;
}): ClankTradeClient => {
  const buildSwap = async ({
    quote,
    trader,
    recipient,
    deadline,
    side,
  }: {
    readonly quote: ClankTradeQuote;
    readonly trader: Address;
    readonly recipient: Address;
    readonly deadline: bigint | undefined;
    readonly side: ClankTradeSide;
  }): Promise<readonly ClankTradeInstruction[]> => {
    requireQuote({
      quote,
      venue: "uniswap-v4",
      side,
      launchToken: market.launchToken,
    });
    if (
      side === "buy" &&
      quote.recipient?.toLowerCase() !== recipient.toLowerCase()
    ) {
      throw new Error("Buy recipient differs from the quoted recipient");
    }
    if (recipient.toLowerCase() !== trader.toLowerCase()) {
      throw new Error("Uniswap V4 output recipient must be the trader");
    }

    const inputToken = side === "buy" ? market.pairToken : market.launchToken;
    const [approvals, transactionDeadline] = await Promise.all([
      buildPermit2Approvals({
        publicClient,
        trader,
        token: inputToken,
        amount: quote.amountIn,
      }),
      resolveDeadline(publicClient, deadline),
    ]);
    const route = encodeV4Route({
      poolKey,
      inputToken,
      amountIn: quote.amountIn,
      minimumAmountOut: quote.minimumAmountOut,
    });

    return [
      ...approvals,
      {
        label: side === "buy" ? "Buy on Uniswap V4" : "Sell on Uniswap V4",
        to: UNIVERSAL_ROUTER,
        data: encodeFunctionData({
          abi: routerAbi,
          functionName: "execute",
          args: [route.commands, [...route.inputs], transactionDeadline],
        }),
        ...(inputToken === zeroAddress ? { value: quote.amountIn } : {}),
      },
    ];
  };

  return {
    venue: "uniswap-v4",
    launchToken: market.launchToken,
    pairToken: market.pairToken,

    async quoteBuy({ pairTokenIn, recipient, slippageBps = 100 }) {
      const { amountOut, minimumAmountOut } = await quoteV4({
        publicClient,
        poolKey,
        inputToken: market.pairToken,
        amountIn: pairTokenIn,
        account: recipient,
        slippageBps,
      });
      return {
        venue: "uniswap-v4",
        side: "buy",
        launchToken: market.launchToken,
        pairToken: market.pairToken,
        requestedAmountIn: pairTokenIn,
        amountIn: pairTokenIn,
        amountOut,
        minimumAmountOut,
        fee: null,
        refund: 0n,
        recipient,
      };
    },

    async buildBuy({ quote, trader, recipient = trader, deadline }) {
      return buildSwap({
        quote,
        trader,
        recipient,
        deadline,
        side: "buy",
      });
    },

    async quoteSell({
      launchTokensIn,
      trader = zeroAddress,
      slippageBps = 100,
    }) {
      const { amountOut, minimumAmountOut } = await quoteV4({
        publicClient,
        poolKey,
        inputToken: market.launchToken,
        amountIn: launchTokensIn,
        account: trader,
        slippageBps,
      });
      return {
        venue: "uniswap-v4",
        side: "sell",
        launchToken: market.launchToken,
        pairToken: market.pairToken,
        requestedAmountIn: launchTokensIn,
        amountIn: launchTokensIn,
        amountOut,
        minimumAmountOut,
        fee: null,
        refund: 0n,
        recipient: null,
      };
    },

    async buildSell({ quote, trader, recipient = trader, deadline }) {
      return buildSwap({
        quote,
        trader,
        recipient,
        deadline,
        side: "sell",
      });
    },
  };
};

export const createClankClient = async ({
  publicClient,
  launchToken,
}: {
  readonly publicClient: PublicClient;
  readonly launchToken: Address;
}): Promise<ClankTradeClient> => {
  const market = await resolveMarket({ publicClient, launchToken });

  if (market.state === 0) {
    return buildCurveClient({ publicClient, market });
  }

  if (market.state === 3) {
    const hooks = await publicClient.readContract({
      address: CLANK_FACTORY,
      abi: factoryAbi,
      functionName: "initializationHook",
    });
    const launchTokenFirst = BigInt(launchToken) < BigInt(market.pairToken);
    const poolKey: PoolKey = {
      currency0: launchTokenFirst ? launchToken : market.pairToken,
      currency1: launchTokenFirst ? market.pairToken : launchToken,
      fee: market.poolFee,
      tickSpacing: market.tickSpacing,
      hooks,
    };
    return buildV4Client({ publicClient, market, poolKey });
  }

  if (market.state === 1 || market.state === 2) {
    throw new Error("This launch is graduating; try again shortly");
  }
  if (market.state === 4) {
    throw new Error("Trading is unavailable for this launch");
  }
  throw new Error(`Unknown Clank curve state: ${market.state}`);
};

Create client

createClankClient verifies the launch through the factory and reads the exact curve state. It returns the same client interface for both trading venues.

import { createClankClient } from "./clank-client";

const clank = await createClankClient({
  publicClient,
  launchToken,
});

console.log(clank.venue); // "bonding-curve" or "uniswap-v4"

States 1 and 2 throw a temporary graduation error. State 4 throws because there is no automatic trading venue.

Quote buy

quoteBuy accepts the amount of pairToken offered and the final launchToken recipient. It applies 1% slippage by default without requesting a wallet signature.

const buyQuote = await clank.quoteBuy({
  pairTokenIn: 100000000000000000n,
  recipient: trader,
  slippageBps: 100,
});

Display amountOut, minimumAmountOut, fee, and refund. The bonding curve returns its fee separately; graduated V4 quotes return fee: null because the pool fee is already reflected in amountOut.

Build buy

buildBuy returns zero or more approval instructions followed by the buy. It does not submit them.

const buyInstructions = await clank.buildBuy({
  quote: buyQuote,
  trader,
});

Native-ETH buys need no approval. ERC-20 curve buys may add one approval; graduated buys may add the required ERC-20 and Permit2 approvals.

Quote sell

quoteSell accepts an amount of launchToken. Passing trader gives the V4 Quoter the same simulation account that will execute the swap.

const sellQuote = await clank.quoteSell({
  launchTokensIn: 1000000000000000000000n,
  trader,
  slippageBps: 100,
});

Display the quote before asking for wallet access. A new quote is required when the input, slippage, or trading venue changes.

Build sell

buildSell returns the launch-token approval path and the sell instruction in submission order.

const sellInstructions = await clank.buildSell({
  quote: sellQuote,
  trader,
});

Simulate and submit each instruction sequentially, waiting for its successful receipt before continuing. See the execution sections in Trade on the bonding curve and Trade after graduation for the wallet loop.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close