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 viemimport { 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 minutesCreate 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; // percentAlways 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. UseparseEtherandformatEther; neverNumberon 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.