Hartii developer docs

QuaiAxe

Integrate QuaiAxe

All examples use quais, Quai's SDK. Every address below is checksummed before use — the Quai RPC rejects lowercase addresses. Addresses and constants are the live mainnet ones from the overview.

js
import { quais } from 'quais';

const RPC = 'https://rpc.quai.network/cyprus1';
const provider = new quais.JsonRpcProvider(RPC, undefined, { usePathing: false });

const FACTORY = quais.getAddress('0x0000c6ae51b3698a2945d68696b59cf52dbb3a94');
const VAULT   = quais.getAddress('0x000e8fa7cfa09827f0a0786ce3e96ac5e4e9f46c');
const LENS    = quais.getAddress('0x00472f13e471b97429a45bf1890187a8ab850e8f');
const CURVE   = quais.getAddress('0x004bc407903a51506bcf0b1ab423958c5991c237');
const INIT_CODE_HASH = '0x93921cd2500d1f18799ed6c2728cfd6983ac9afde51abc838963cbfea5597dea';

1. Derive a miner's deposit address off-chain

This mirrors quaiaxe/src/lib/deposit-address.js (the site's own dependency-free implementation) using quais's primitives instead of a hand-rolled keccak256:

js
const coder = quais.AbiCoder.defaultAbiCoder();
const ZERO_ADDRESS = quais.ZeroAddress;
const MAX_GRIND = 4096; // matches factory.MAX_GRIND() and findAddress's own cap

function saltOf(payout, burnBps, nonce) {
  // payout is forced to the zero address at 100% burn — there is no payout wallet to name.
  const effectivePayout = burnBps === 10000 ? ZERO_ADDRESS : payout;
  return quais.keccak256(coder.encode(['address', 'uint256', 'uint256'], [effectivePayout, burnBps, nonce]));
}

function addressOf(payout, burnBps, nonce) {
  const salt = saltOf(payout, burnBps, nonce);
  return quais.getAddress(
    '0x' + quais.keccak256(quais.concat(['0xff', FACTORY, salt, INIT_CODE_HASH])).slice(26),
  );
}

// Cyprus-1 shard (first byte 0x00) AND the Quai ledger (top bit of byte 2 clear) — the same rule
// the factory's isMiningAddress / findAddress enforce.
function isMiningAddress(addr) {
  const hex = addr.slice(2);
  return hex.slice(0, 2) === '00' && (parseInt(hex.slice(2, 4), 16) & 0x80) === 0;
}

function deriveDepositAddress(payout, burnBps) {
  for (let nonce = 0; nonce < MAX_GRIND; nonce++) {
    const address = addressOf(payout, burnBps, nonce);
    if (isMiningAddress(address)) return { address, nonce };
  }
  throw new Error('no address found within MAX_GRIND tries');
}

// 60% burn, the rest paid out to a miner's own wallet:
const payout = quais.getAddress('0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); // example only
const { address, nonce } = deriveDepositAddress(payout, 6000);
console.log(address, nonce);

Cross-check against the chain

Never trust a derivation you can't verify. findAddress is the on-chain source of truth and grinds the identical rule:

js
const FACTORY_ABI = [
  'function addressOf(address payout, uint256 burnBps, uint256 nonce) view returns (address)',
  'function findAddress(address payout, uint256 burnBps, uint256 start) view returns (address depositAddress, uint256 nonce)',
];
const factory = new quais.Contract(FACTORY, FACTORY_ABI, provider);

const [onChainAddress, onChainNonce] = await factory.findAddress(payout, 6000, 0);
if (onChainAddress !== address || Number(onChainNonce) !== nonce) {
  throw new Error('off-chain derivation disagrees with the chain — do not show this address to a miner');
}

2. Build stratum settings

QuaiAxe deposit addresses are plain payout addresses as far as any miner or pool is concerned. The recommended path is StratumX solo (0% fee, non-custodial — the protocol pays the address directly):

js
function buildMinerConfig({ address, nickname, lock = 0, algorithm = 'sha256', region = 'eu' }) {
  if (!/^0x00[0-9a-fA-F]{38}$/.test(address)) throw new Error('address must be a Cyprus-1 address (0x00…)');
  if (!/^[A-Za-z0-9_-]{2,20}$/.test(nickname)) throw new Error('nickname must be 2-20 chars, [A-Za-z0-9_-]');
  if (![0, 1, 2, 3].includes(lock)) throw new Error('lock must be 0, 1, 2 or 3');

  const ports = { sha256: 3333, scrypt: 3334, kawpow: 3335 };
  // StratumX solo serves SHA-256 only. Scrypt/KawPoW need your own go-quai node on the port above.
  const host = algorithm === 'sha256' ? `mining-${region}.stratumx.org` : 'localhost';
  const port = ports[algorithm];

  return {
    url: `stratum+tcp://${host}:${port}`,
    // The pool splits on the FIRST dot: everything before it is the payout address.
    user: `${address}.QAXE_${nickname}`,
    password: `lock=${lock}`,
  };
}

console.log(buildMinerConfig({ address, nickname: 'rig1' }));
// { url: 'stratum+tcp://mining-eu.stratumx.org:3333',
//   user: '0x00….QAXE_rig1', password: 'lock=0' }

lock=0 is Quai's mandatory minimum lock (14 days, ~241,920 blocks) — it is not a QuaiAxe choice and cannot be made shorter. lock=1/2/3 (3/6/12 months) earn a network reward boost that decays over the first year, at the cost of a longer wait before the address's balance is real.

StratumX's SHA-256 endpoint is on port 3333; Scrypt and KawPoW are only available against your own go-quai node's built-in stratum, on ports 3334 and 3335 respectively.

3. Check a rig without waiting 14 days

A reward is locked before it's credited, so a misconfigured payout address looks identical to a correct one for two weeks unless you check workshares directly — they appear within minutes:

js
async function checkRig(address, { blocks = 30 } = {}) {
  const addr = address.toLowerCase();
  const latest = await provider.send('quai_getBlockByNumber', ['latest', false]);
  const head = Number(BigInt(latest.woHeader.number));
  let seen = false, shares = 0;

  for (let i = 0; i < blocks; i++) {
    const n = head - i;
    if (n < 0) break;
    const b = i === 0 ? latest : await provider.send('quai_getBlockByNumber', ['0x' + n.toString(16), false]);
    if (!b || !b.woHeader) continue;
    const coinbases = (b.workshares || []).map((w) => w.primaryCoinbase);
    if (b.woHeader.primaryCoinbase) coinbases.push(b.woHeader.primaryCoinbase);
    for (const c of coinbases) if (c && c.toLowerCase() === addr) { shares++; seen = true; }
  }
  return { seen, shares, scannedBlocks: Math.min(blocks, head + 1) };
}

console.log(await checkRig(address));

seen: true means the address is genuinely receiving workshares; seen: false after a few minutes of mining means something in the miner config is wrong — check the address, the port, and the worker string before waiting two weeks to find out the hard way.

4. Collect a deposit

js
const DEPOSIT_FACTORY_ABI = [
  'function collect(address payout, uint256 burnBps, uint256 nonce, address refundTo) returns (uint256 gross)',
];
const factoryWrite = new quais.Contract(FACTORY, DEPOSIT_FACTORY_ABI, wallet); // wallet = quais.Wallet

const tx = await factoryWrite.collect(payout, 6000n, BigInt(nonce), await wallet.getAddress());
const receipt = await tx.wait();
console.log('collected in', receipt.hash);

Anyone can call collect on anyone's deposit address — collection just moves the balance into the vault's accounting under that deposit address's own ledger entry, it never redirects who the principal or payout belongs to. See deposit-factory for collectMany / collectAndSweep when batching.

5. Preview and execute a sweep, with a real slippage floor

previewSweep()'s buyValue is what actually reaches the curve — not the slice, because a portion of the slice is reserved for the caller's gas refund. Quote against buyValue, not the slice, and remember curve.quoteBuy expects the net amount (after the curve's own fee), so back that out first:

js
const VAULT_ABI = [
  'function previewSweep() view returns (uint256 slice, uint256 fee, uint256 refundReserve, uint256 buyValue)',
  'function canSweep() view returns (bool)',
  'function sweep(uint256 minTokensOut, address refundTo) returns (uint256 quaiSpent, uint256 burned)',
];
const CURVE_ABI = [
  'function feeBps() view returns (uint256)',
  'function quoteBuy(uint256 quaiIn) view returns (uint256 tokensOut)',
];

const vault = new quais.Contract(VAULT, VAULT_ABI, provider);
const curve = new quais.Contract(CURVE, CURVE_ABI, provider);

if (!(await vault.canSweep())) throw new Error('not ready — cooldown or below minimum');

const { buyValue } = await vault.previewSweep();
const feeBps = await curve.feeBps();
const net = buyValue - (buyValue * feeBps) / 10000n; // the curve's OWN fee comes off first
const quoted = await curve.quoteBuy(net);
const minTokensOut = (quoted * 99n) / 100n; // 1% slippage floor — tune to your risk tolerance

const vaultWrite = vault.connect(wallet);
const tx = await vaultWrite.sweep(minTokensOut, await wallet.getAddress());
const receipt = await tx.wait();
console.log('swept in', receipt.hash);

sweep's floor is enforced against the vault's actual post-buy QAXE balance (donations included), not just what the curve reports buying — see burn-vault for the exact mechanics.

6. Read everything in one call with QaxeLens

js
const LENS_ABI = [
  'function read(address vault, address[] addrs) view returns (uint256[] balances, uint256[] received, uint256[] contributed, uint256[] kept, uint256[] pending)',
];
const lens = new quais.Contract(LENS, LENS_ABI, provider);

const addrs = [address /* , more deposit addresses */];
const [balances, received, contributed, kept, pending] = await lens.read(VAULT, addrs);

addrs.forEach((a, i) => {
  console.log(a, {
    uncollectedWei: balances[i].toString(),
    lifetimeReceivedWei: received[i].toString(),
    lifetimeContributedToBurnWei: contributed[i].toString(),
    lifetimeKeptWei: kept[i].toString(),
    pendingWithdrawWei: pending[i].toString(),
  });
});

balances[i] is the raw wallet balance sitting at the deposit address, uncollected. pending[i] is what's waiting in step 7 below.

7. withdrawPending for a miner whose push failed

The miner-leg payout is pushed with only 30,000 gas. If the payout address can't accept a plain transfer within that (a hostile or gas-hungry contract, or simply out of gas), the amount falls back to pending[payout] instead of reverting the whole collection. The affected address pulls it themselves, any time:

js
const VAULT_WRITE_ABI = ['function withdrawPending()', 'function pending(address) view returns (uint256)'];
const vaultAsPayout = new quais.Contract(VAULT, VAULT_WRITE_ABI, payoutWallet); // payoutWallet = quais.Wallet

const owed = await vaultAsPayout.pending(await payoutWallet.getAddress());
if (owed > 0n) {
  const tx = await vaultAsPayout.withdrawPending();
  await tx.wait();
}
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.