Hartii developer docs

HartiiSwap

Integrate

js
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

js
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

js
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

js
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)

js
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

js
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

js
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

RevertMeaning / fix
HartiiSwapRouter: INSUFFICIENT_OUTPUT_AMOUNTPrice moved past your slippage — re-quote.
HartiiSwapRouter: EXPIREDDeadline passed before inclusion.
TransferHelper: TRANSFER_FROM_FAILEDMissing/insufficient approval to the router, or not enough balance.
HartiiSwapLibrary: PAIR_NOT_FOUNDA hop in path has no pool.
HartiiSwapRouter: INSUFFICIENT_A_AMOUNT / _B_AMOUNTDeposit ratio moved past your mins.
could not coalesce error (RPC)An address wasn't checksummed.
Quai Network mainnet · chain 9 · Cyprus-1. Figures marked "read on" a date were read from the chain that day; re-read before relying on them.