Joseon.funDocs
AppExplorerOpen app

Build

Contract calls

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

Setup#

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

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: '0x33fFaC709Ec1D3Ec8bC8d0655914Aa7DCd7b463F',
  router: '0xcD4B6582C6964Fae3dF4254cAED959789cFD5d3D',
  factory: '0x9AB150A7e487FbdC1094077C8A18898297118784',
  weth: '0x323bbaAfff0518Ca107A73BA9870A426fA550B11',
  registry: '0xC225aE54E77C254DEb4a928aE439Afe30E4637aA',
  locker: '0x2141630cf59bC2e04bd4A0864B86b84C21023fE6',
};

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

// The public RPC rate-limits bursts, so batch reads rather than firing them in
// parallel. Multicall3 is at its canonical address on this chain.

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. pairFor works right away too, since the launch's pool is reserved in this same transaction:

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 fill for the life of the deadline. Quote it from the factory's published starting reserves instead — the app does exactly that.

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 into its pool and refunds ethRefunded. Budget gas accordingly; 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, which is the loud failure you want rather than a silent misroute.

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 address argument
await walletClient.writeContract({
  account, address: pair,
  abi: [{ type: 'function', name: 'claimCreatorFeesFor', stateMutability: 'nonpayable',
          inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }, { type: 'uint256' }] }],
  functionName: 'claimCreatorFeesFor', args: [account],
});

Both claims revert when there is nothing to claim, so read the balance first rather than sending speculatively. Use creatorFeesOwed(recipient) for that: it is per-wallet, unlike creatorFees0/creatorFees1, which are pair-wide totals across every recipient the pool has had.

Prefer claimCreatorFeesFor(wallet) over the no-argument claimCreatorFees(). The latter is shorthand for the current recipient, so after a CTO reassignment it stops working for the wallet that earned the earlier fees. The explicit form keeps paying whichever address the ledger owes, current or former. Either way the payout goes to the address argument, not to the caller, so anyone can settle someone else's balance.

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 tail, use the indexer API. It has already backfilled everything, and it will not time out the way a wide eth_getLogs range does on the public RPC.

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. The curve and the router are not interchangeable.
  • Verify the curve you are about to trade against. Ask the factory for curveFor(token) and check the curve agrees which token it serves, rather than trusting an address from a feed. Cheap read, and it is the difference between a failed lookup and a signed transaction to the wrong contract.
  • One stack only. A token launched here has no pool on an older router, and that failure is quiet.