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 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: '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 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. 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; // percentAlways 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. UseparseEtherandformatEther; neverNumberon 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.