Integrate
import { quais } from 'quais';
const provider = new quais.JsonRpcProvider('https://rpc.quai.network/cyprus1', undefined, { usePathing: false });
const addr = (a) => quais.getAddress(a.toLowerCase()); // the Quai RPC rejects non-checksummed addresses
const FACTORY = addr('0x002E75653C6189087Eb9E512cE7319eC21140956');
const ROUTER = addr('0x006B3450cca780066A3A8aA8b508103d25315Ea4');
const LENS = addr('0x00295dFCA930646abAC327C9CcB2f4e1E206C5Ec');
const WQUAI = addr('0x006C3e2AaAE5DB1bCd11A1a097cE572312EADdBB');
const ROUTER_ABI = [
'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])',
'function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[])',
'function swapExactTokensForETH(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[])',
'function addLiquidityETH(address token, uint256 amountTokenDesired, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) payable returns (uint256, uint256, uint256)',
'function removeLiquidityETH(address token, uint256 liquidity, uint256 amountTokenMin, uint256 amountETHMin, address to, uint256 deadline) returns (uint256, uint256)',
];
const ERC20_ABI = ['function approve(address, uint256) returns (bool)', 'function allowance(address, address) view returns (uint256)', 'function balanceOf(address) view returns (uint256)'];
const PAIR_ABI = [...ERC20_ABI, 'function token0() view returns (address)', 'function getReserves() view returns (uint112, uint112, uint32)', 'function totalSupply() view returns (uint256)'];
const deadline = () => BigInt(Math.floor(Date.now() / 1000) + 20 * 60);
const withSlippage = (x, bps = 50n) => (x * (10000n - bps)) / 10000n; // 0.5%Find a pool and its price
const factory = new quais.Contract(FACTORY, ['function getPair(address, address) view returns (address)'], provider);
export async function poolFor(token) {
const pair = await factory.getPair(addr(token), WQUAI); // never compute it — pairs are CREATE-deployed
if (pair === quais.ZeroAddress) return null;
const p = new quais.Contract(pair, PAIR_ABI, provider);
const [r0, r1] = await p.getReserves();
const tokenIs0 = (await p.token0()).toLowerCase() === addr(token).toLowerCase();
const [tokenReserve, quaiReserve] = tokenIs0 ? [r0, r1] : [r1, r0];
return { pair, tokenReserve, quaiReserve, priceWei: (quaiReserve * 10n ** 18n) / tokenReserve };
}Swap QUAI → token
export async function buy(wallet, token, quaiIn) {
const router = new quais.Contract(ROUTER, ROUTER_ABI, wallet);
const path = [WQUAI, addr(token)];
const [, out] = await router.getAmountsOut(quaiIn, path); // equals the real swap to the wei
const tx = await router.swapExactETHForTokens(withSlippage(out), path, wallet.address, deadline(),
{ value: quaiIn, from: wallet.address });
return tx.wait();
}Swap token → QUAI
export async function sell(wallet, token, amountIn) {
const router = new quais.Contract(ROUTER, ROUTER_ABI, wallet);
const t = new quais.Contract(addr(token), ERC20_ABI, wallet);
if ((await t.allowance(wallet.address, ROUTER)) < amountIn) {
await (await t.approve(ROUTER, amountIn, { from: wallet.address })).wait(); // approve the ROUTER, not the pair
}
const path = [addr(token), WQUAI];
const [, out] = await router.getAmountsOut(amountIn, path);
const tx = await router.swapExactTokensForETH(amountIn, withSlippage(out), path, wallet.address, deadline(),
{ from: wallet.address });
return tx.wait();
}Two-hop routes (token A → WQUAI → token B) use path = [A, WQUAI, B] with
swapExactTokensForTokens; every hop must have a pool.
Add liquidity (token + QUAI)
export async function addLiquidity(wallet, token, tokenAmount) {
const router = new quais.Contract(ROUTER, ROUTER_ABI, wallet);
const pool = await poolFor(token);
// Existing pool: pair QUAI at the pool ratio. Empty pool: YOU set the price — see Liquidity › first deposit.
if (!pool) throw new Error('No pool yet: seed at the curve price (docs: swap/liquidity)');
const quaiAmount = (tokenAmount * pool.quaiReserve) / pool.tokenReserve + 1n;
const t = new quais.Contract(addr(token), ERC20_ABI, wallet);
await (await t.approve(ROUTER, tokenAmount, { from: wallet.address })).wait();
const tx = await router.addLiquidityETH(addr(token), tokenAmount, withSlippage(tokenAmount), withSlippage(quaiAmount),
wallet.address, deadline(), { value: quaiAmount, from: wallet.address });
return tx.wait(); // excess QUAI is refunded by the router
}Remove liquidity
export async function removeAll(wallet, token) {
const { pair, tokenReserve, quaiReserve } = await poolFor(token);
const lp = new quais.Contract(pair, PAIR_ABI, wallet);
const liquidity = await lp.balanceOf(wallet.address);
const supply = await lp.totalSupply();
const minToken = withSlippage((tokenReserve * liquidity) / supply);
const minQuai = withSlippage((quaiReserve * liquidity) / supply);
await (await lp.approve(ROUTER, liquidity, { from: wallet.address })).wait();
const router = new quais.Contract(ROUTER, ROUTER_ABI, wallet);
return (await router.removeLiquidityETH(addr(token), liquidity, minToken, minQuai, wallet.address, deadline(),
{ from: wallet.address })).wait();
}List every pool and a user's positions in one call
const lens = new quais.Contract(LENS, [
'function pools(address factory, uint256 start, uint256 count) view returns (tuple(address pair, tuple(address token, string symbol, uint8 decimals) token0, tuple(address token, string symbol, uint8 decimals) token1, uint112 reserve0, uint112 reserve1, uint256 totalSupply)[])',
'function positions(address factory, address user, uint256 start, uint256 count) view returns (tuple(address pair, uint256 lpBalance, uint256 totalSupply, uint256 amount0, uint256 amount1)[])',
], provider);
const pools = await lens.pools(FACTORY, 0, 50);
const mine = (await lens.positions(FACTORY, userAddress, 0, 50)).filter((p) => p.lpBalance > 0n);Listening for activity
Pairs emit Swap, Mint, Burn and Sync; the factory emits PairCreated. Use getLogs in
windows of at most ~10,000 blocks (the Quai RPC cap).
Errors you will see
| Revert | Meaning / fix |
|---|---|
HartiiSwapRouter: INSUFFICIENT_OUTPUT_AMOUNT | Price moved past your slippage — re-quote. |
HartiiSwapRouter: EXPIRED | Deadline passed before inclusion. |
TransferHelper: TRANSFER_FROM_FAILED | Missing/insufficient approval to the router, or not enough balance. |
HartiiSwapLibrary: PAIR_NOT_FOUND | A hop in path has no pool. |
HartiiSwapRouter: INSUFFICIENT_A_AMOUNT / _B_AMOUNT | Deposit ratio moved past your mins. |
could not coalesce error (RPC) | An address wasn't checksummed. |