Hartii developer docs

QuaiRelay

Integrate

QuaiRelay has no HTTP API — every integration talks straight to the contract and to the public Quai RPC. All examples use quais, checksum every address before it reaches the RPC, and treat an unset gas price/balance as unknown rather than zero (the same rules the relay's own front ends follow).

Send QUAI through the relay

js
import { quais } from 'quais';

const RPC_URL = 'https://rpc.quai.network/cyprus1';
const RELAY_ADDRESS = '0x003D2EC4C1Ff5e98898fEa6C6D1AAaA48FaDc11A';
const RELAY_ABI = ['function sendTo(address payable to) external payable'];

// quais.getAddress() checksums — Quai's RPC rejects a lowercase address outright.
async function sendThroughRelay(signer, destination, valueWei) {
  const to = quais.getAddress(String(destination).toLowerCase());
  if (!to.startsWith('0x00')) {
    throw new Error('destination is not a Cyprus-1 address — QUAI sent there is unreachable');
  }
  if (parseInt(to.slice(4, 6), 16) >= 0x80) {
    throw new Error('destination is a Qi-ledger address — QUAI cannot be sent to it');
  }

  const relay = new quais.Contract(RELAY_ADDRESS, RELAY_ABI, signer);
  const tx = await relay.sendTo(to, { value: valueWei });
  await tx.wait();
  return tx.hash;
}

Read balance + gas price to offer "send everything"

js
import { quais } from 'quais';

const provider = new quais.JsonRpcProvider('https://rpc.quai.network/cyprus1', undefined, {
  usePathing: false, // RPC_URL already ends in /cyprus1 — do not let the SDK append it again
});

async function maxSendable(accountAddress) {
  const address = quais.getAddress(String(accountAddress).toLowerCase());
  const [balance, feeData] = await Promise.all([
    provider.getBalance(address),
    provider.getFeeData(quais.Zone.Cyprus1), // getFeeData needs the zone on Quai
  ]);

  // A sendTo to a never-used recipient estimates ~98,000 gas (2026-11-02); 60,000 is too low and
  // reverts. Use a live provider.estimateGas(...) + 20% where you can.
  const gasLimit = 130_000n;
  const gasPrice = BigInt(feeData.gasPrice ?? feeData.maxFeePerGas ?? 0n);
  const gasCost = (gasLimit * gasPrice * 125n) / 100n; // 25% headroom

  if (gasCost >= balance) return 0n; // balance can't cover its own fee
  return balance - gasCost;
}

Raw JSON-RPC (no wallet SDK)

Useful for a script that just wants to read the relay's bytecode or watch its events — no signing involved.

http
POST https://rpc.quai.network/cyprus1
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"quai_getCode","params":["0x003D2EC4C1Ff5e98898fEa6C6D1AAaA48FaDc11A","latest"]}

Returns the deployed runtime bytecode (non-0x response = the contract exists at that address).

http
POST https://rpc.quai.network/cyprus1
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"quai_getBalance","params":["0x<your-checksummed-address>","latest"]}

Calldata by hand

If you need to build the transaction yourself instead of using quais.Contract:

js
// selector for sendTo(address), keccak-256("sendTo(address)")[0..4]
const SELECTOR = '0xe6d25245';

function encodeSendTo(toChecksummed) {
  const body = toChecksummed.slice(2).toLowerCase().padStart(64, '0');
  return SELECTOR + body;
}

const tx = {
  from: accountAddress,
  to: '0x003D2EC4C1Ff5e98898fEa6C6D1AAaA48FaDc11A',
  value: '0x' + valueWei.toString(16),
  data: encodeSendTo(destinationChecksummed),
  // A never-used recipient needs ~98,000 gas; use a live quai_estimateGas + 20%, or 130,000.
  gas: '0x' + (130_000n).toString(16),
};
// wallet.request({ method: 'quai_sendTransaction', params: [tx] })
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.