Joseon docs
AppExplorerLaunch app

Build

Contract calls

viem snippets for create, buy, sell, swap, and claim.

Setup#

There is no SDK package. The contracts are plain enough that viem plus a handful of ABI fragments is the whole integration, and it means nothing to keep in sync when a new stack ships.

npm i viem
import { createPublicClient, createWalletClient, custom, defineChain, http } from 'viem';

export const giwaSepolia = defineChain({
  id: 91342,
  name: 'GIWA Sepolia',
  nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
  rpcUrls: { default: { http: ['https://sepolia-rpc.giwa.io'] } },
  blockExplorers: { default: { name: 'GIWA Explorer', url: 'https://sepolia-explorer.giwa.io' } },
  testnet: true,
});

export const CONTRACTS = {
  launchFactory: '0x87A02E73778979b86487E30304f57eaaA044BCd0',
  router: '0xF347ADa4e1C0b565FC1399FB2Cb70e56B47F65C2',
  factory: '0x3c892A39A0c8287592E74815E21581d6aDCcf2B5',
  weth: '0xFD20F4555a526230479583449D1ce6F6C6980D8F',
  registry: '0x4368d52D68b035482ba740693250281535f3C457',
};

export const publicClient = createPublicClient({ chain: giwaSepolia, transport: http() });

export const walletClient = createWalletClient({
  chain: giwaSepolia,
  transport: custom(window.ethereum),
});

A deadline helper, since every state-changing call takes one:

const deadline = () => BigInt(Math.floor(Date.now() / 1000) + 600); // 10 minutes

Create a token#

const launchFactoryAbi = [{
  type: 'function', name: 'createToken', stateMutability: 'payable',
  inputs: [
    { type: 'string', name: 'name' },
    { type: 'string', name: 'symbol' },
    { type: 'uint256', name: 'initialBuyMinOut' },
    { type: 'uint256', name: 'deadline' },
  ],
  outputs: [{ type: 'address', name: 'token' }, { type: 'address', name: 'curve' }],
}];

const [account] = await walletClient.getAddresses();

const hash = await walletClient.writeContract({
  account,
  address: CONTRACTS.launchFactory,
  abi: launchFactoryAbi,
  functionName: 'createToken',
  args: ['My Token', 'MINE', 0n, deadline()],
  value: parseEther('0.01'), // optional first buy, in the same transaction
});

const receipt = await publicClient.waitForTransactionReceipt({ hash });

Read the deployed addresses out of the TokenCreated event, or ask the factory:

const curve = await publicClient.readContract({
  address: CONTRACTS.launchFactory,
  abi: [{ type: 'function', name: 'curveFor', stateMutability: 'view',
          inputs: [{ type: 'address' }], outputs: [{ type: 'address' }] }],
  functionName: 'curveFor',
  args: [token],
});

initialBuyMinOut is your slippage guard on the bundled first buy. Passing 0n accepts any amount out, which is fine when you are the first buyer on a curve nobody else can front-run yet, but quote it properly for anything else.

Quote and buy on the curve#

quoteBuy returns everything you need to show an exact preview, including whether this buy ends the curve.

const curveAbi = [
  { type: 'function', name: 'quoteBuy', stateMutability: 'view',
    inputs: [{ type: 'uint256', name: 'grossEthIn' }],
    outputs: [
      { type: 'uint256', name: 'ethUsed' }, { type: 'uint256', name: 'ethRefunded' },
      { type: 'uint256', name: 'fee' }, { type: 'uint256', name: 'netEthIn' },
      { type: 'uint256', name: 'tokenOut' }, { type: 'uint256', name: 'nextVirtualEthReserve' },
      { type: 'uint256', name: 'nextVirtualTokenReserve' }, { type: 'bool', name: 'willGraduate' },
    ] },
  { type: 'function', name: 'buy', stateMutability: 'payable',
    inputs: [{ type: 'uint256', name: 'minTokensOut' }, { type: 'uint256', name: 'deadline' }],
    outputs: [{ type: 'uint256' }] },
];

const ethIn = parseEther('0.05');
const [ethUsed, refunded, fee, netEthIn, tokenOut, , , willGraduate] =
  await publicClient.readContract({
    address: curve, abi: curveAbi, functionName: 'quoteBuy', args: [ethIn],
  });

// 1% slippage tolerance
const minOut = (tokenOut * 99n) / 100n;

await walletClient.writeContract({
  account, address: curve, abi: curveAbi, functionName: 'buy',
  args: [minOut, deadline()], value: ethIn,
});

If willGraduate is true, this transaction also migrates the launch to JoseonSwap and refunds ethRefunded. Budget gas for it — it is a much heavier transaction than a normal buy.

Sell on the curve#

Selling needs an approval first, since the curve pulls tokens with transferFrom.

await walletClient.writeContract({
  account, address: token, abi: erc20Abi,
  functionName: 'approve', args: [curve, tokenIn],
});

const [grossEthOut, fee, netEthOut] = await publicClient.readContract({
  address: curve,
  abi: [{ type: 'function', name: 'quoteSell', stateMutability: 'view',
          inputs: [{ type: 'uint256', name: 'tokenIn' }],
          outputs: [
            { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' },
            { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' },
          ] }],
  functionName: 'quoteSell', args: [tokenIn],
});

await walletClient.writeContract({
  account, address: curve,
  abi: [{ type: 'function', name: 'sell', stateMutability: 'nonpayable',
          inputs: [{ type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }],
          outputs: [{ type: 'uint256' }] }],
  functionName: 'sell', args: [tokenIn, (netEthOut * 99n) / 100n, deadline()],
});

Check curve state#

const [price, progress, state] = await Promise.all([
  publicClient.readContract({ address: curve, abi: curveAbi2, functionName: 'getCurrentPrice' }),
  publicClient.readContract({ address: curve, abi: curveAbi2, functionName: 'getProgress' }),
  publicClient.readContract({ address: curve, abi: curveAbi2, functionName: 'launchState' }),
]);

// price and progress are WAD-scaled; state is 0 CURVE_ACTIVE, 1 GRADUATING, 2 DEX_ACTIVE
const pct = Number(progress) / 1e16; // percent

Always branch on launchState before choosing a venue. Calling buy on a graduated curve reverts with NOT_ACTIVE.

Swap after graduation#

const routerAbi = [
  { type: 'function', name: 'getAmountsOut', stateMutability: 'view',
    inputs: [{ type: 'uint256', name: 'amountIn' }, { type: 'address[]', name: 'path' }],
    outputs: [{ type: 'uint256[]' }] },
  { type: 'function', name: 'swapExactETHForTokens', stateMutability: 'payable',
    inputs: [
      { type: 'uint256', name: 'amountOutMin' }, { type: 'address[]', name: 'path' },
      { type: 'address', name: 'to' }, { type: 'uint256', name: 'deadline' },
    ],
    outputs: [{ type: 'uint256[]' }] },
  { type: 'function', name: 'swapExactTokensForETH', stateMutability: 'nonpayable',
    inputs: [
      { type: 'uint256', name: 'amountIn' }, { type: 'uint256', name: 'amountOutMin' },
      { type: 'address[]', name: 'path' }, { type: 'address', name: 'to' },
      { type: 'uint256', name: 'deadline' },
    ],
    outputs: [{ type: 'uint256[]' }] },
];

const path = [CONTRACTS.weth, token];
const amounts = await publicClient.readContract({
  address: CONTRACTS.router, abi: routerAbi, functionName: 'getAmountsOut',
  args: [ethIn, path],
});
const minOut = (amounts[amounts.length - 1] * 99n) / 100n;

await walletClient.writeContract({
  account, address: CONTRACTS.router, abi: routerAbi,
  functionName: 'swapExactETHForTokens',
  args: [minOut, path, account, deadline()],
  value: ethIn,
});

Selling to ETH is swapExactTokensForETH after approving the router. Paths are capped at two hops, so route token-to-token through WETH: [tokenA, WETH, tokenB].

Claim fees#

// curve creator fees
const claimable = await publicClient.readContract({
  address: curve,
  abi: [{ type: 'function', name: 'claimableCreatorFees', stateMutability: 'view',
          inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }],
  functionName: 'claimableCreatorFees', args: [account],
});

if (claimable > 0n) {
  await walletClient.writeContract({
    account, address: curve,
    abi: [{ type: 'function', name: 'claimCreatorFees', stateMutability: 'nonpayable',
            inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }] }],
    functionName: 'claimCreatorFees', args: [account],
  });
}

// swap fees on a graduated pair — permissionless, pays the registered recipient
await walletClient.writeContract({
  account, address: pair,
  abi: [{ type: 'function', name: 'claimCreatorFees', stateMutability: 'nonpayable',
          inputs: [], outputs: [{ type: 'uint256' }, { type: 'uint256' }] }],
  functionName: 'claimCreatorFees',
});

Both claims revert when there is nothing to claim, so check the balance first rather than sending speculatively.

Watching for launches#

publicClient.watchContractEvent({
  address: CONTRACTS.launchFactory,
  abi: [{ type: 'event', name: 'TokenCreated', inputs: [
    { indexed: true, name: 'token', type: 'address' },
    { indexed: true, name: 'creator', type: 'address' },
    { indexed: true, name: 'bondingCurve', type: 'address' },
    { indexed: false, name: 'initialBuy', type: 'uint256' },
  ] }],
  eventName: 'TokenCreated',
  onLogs: (logs) => console.log(logs.map((l) => l.args)),
});

For history rather than a live feed, use the indexer API — it has already backfilled everything and will not rate-limit you the way eth_getLogs over a wide range will.

Gotchas#

  • Every write takes a deadline. Pass a real timestamp; expired ones revert with EXPIRED.
  • Amounts are bigint. Use parseEther and formatEther; never Number on a wei value.
  • Approve before selling or swapping tokens. ETH-in paths need no approval.
  • Branch on launchState. Curve and router are not interchangeable.
  • One stack only. A V3 token has no pair on an older router, and the failure is silent.