What a site, a game server or a tool must do so that building on QNet Blockchain does not put users, their funds or the server at risk. The rules follow from how the wallets, the SDK and the nodes behave (dApp integration, Transactions, Smart contracts). To report a vulnerability in QNet Blockchain itself, follow SECURITY.md.
- Never ask a user for a recovery phrase, a private key or a wallet password, in a form, a chat, a support ticket or a file upload. A page needs none of them: the wallet signs, and the wallet shows the user what it signs. A site that asks for a phrase is indistinguishable from a theft.
- A page asks the wallet; it does not build or sign transactions itself, and no provider method signs
arbitrary bytes as a transaction.
qnet_signMessagesigns text only, bound to the page's origin and under a context the node never accepts for a transaction. - A server's own key lives in a
qnetkey file (CLI): encrypted with AES-256-GCM under an Argon2id key of its password,0600where the system has file modes. Keep the password out of the command line, shell history, environment dumps and logs; the command reads it without echo or from standard input. Wipe a key after use (secretKey.fill(0), as@aiqnet/sdkdoes) and never commit a key file. - A key made with
qnet keys newhas no phrase anywhere: back up the file and its password, or create the key in a wallet and import it.
- Verify on the server. The address a page reports proves nothing; a verified sign-in does
(Sign-in).
verifySignInchecks that the text names your site and the network, the time window, that the public key is the named account's, the signature for your origin, and that the nonce is used for the first time. - Pass your origin from configuration, exactly as a browser writes it (
https://games.aiqnet.io), never from the request. The wallet signs for the origin it reads itself, so a signature collected on another site does not verify for yours. - Issue each nonce on the server, to the session that asked for it, and accept it once, from that
session only (
issue(sessionId),consume(nonce, sessionId)). Otherwise a sign-in someone made with his own wallet for your site can be posted from another person's browser (a cross-site form), which is then signed in to his account and pays or deposits into it. Keep sign-in texts short-lived (the default is 10 minutes). Several servers need one shared nonce store. - Accept the verify request only as
application/json, with anOriginheader equal to your origin; a cross-site form can post other types without asking the browser first (Sign-in). - After a sign-in, run your own session (a cookie your server sets). Do not keep the signed text as a bearer credential.
- Identify a transaction by (
from,nonce), not by its hash. The hash is one node's name for its copy; the same signed transaction can land under another hash. - "Submitted" and "in a block" are not "succeeded". The network records no outcome. Before you
credit a user, check the effect: for a transfer the recipient's balance and the sender's nonce, for a
token transfer the token-transfer feed or the token balance, for a contract call the contract's
event or storage. A wallet's
qnet_getTransactionStatusanswersin_blockfor a call that reverted. - Wait for finality for anything valuable.
GET /api/v1/transaction/{hash}reportsfinality_indicators.level;FullyFinalizedmeans the block is at or below the checkpoint the committee certified. - One transaction per account at a time. A second send before the first is in a block is refused by the node or waits in the wallet; design flows (for example, several purchases in a row) around it.
- Amounts are exact decimal text. Convert with integers (
BigInt,parseUnits), never with floating point. JSON numbers above 2^53 lose precision in JavaScript: the SDK reads balances exactly and writes request bodies from digits. - Check addresses before building. An address is 45 lowercase characters with a SHA3-256 checksum
(
isValidAddress); a contract address has the same form. A built-in token transfer to0000000000000000000eon00000000000000036877022destroys the tokens; the wallets flag it in their approval, andqnetrefuses it without--burn. No contract can send QNC or a built-in token on, so a node's submit routes refuse a transfer or a built-in token transfer to a contract address (recipient_is_contract). That check is not a block rule: a block that carries such a transfer applies it, and what it sends stays in the contract account for good. Read the recipient first and refuse one whoseisContractis true (NodeClient.getAccount); treat an account answer too large to read as a contract.qnet transferandqnet token transferrefuse a contract recipient.
- Most reads are one node's word. Balance, nonce, token details, token balances, events and
transactions come back as the answering node reports them. Proofs exist for an account's balance and
nonce (
/api/v1/account/{address}/balance/proof?mb=latest), a holder's token balance (/api/v1/token/{contract}/{holder}/balance/proof?mb=latest) and one event (/api/v1/logs/proof). Ask for the certified form (?mb=), verify the quorum certificate of the macroblock it names (macroblock_index) yourself, and fold the proof to thestate_rootof that verified checkpoint, never to the root the node served (certified state proofs). The form withoutmbproves against the answering node's live root, which folds to a certified root only while no account changed since the macroblock, so on a busy chain it rarely verifies (the SDK'sNodeClient.getVerifiedAccountstill reads that form);verifyLogchecks the event against its checkpoint'slogs_root. An answer without a proof that verifies (a legacy body, an error, a rate limit, a timeout) is no answer: ask another node, and never show it as a balance or a zero. For anything else, ask several nodes and require agreement, as the wallets do. /api/v1/validators/proofis a hash over a list the node builds itself, not a proof; treat it as a plain read.- Any super node's public RPC (
https://<IPv4>, the wallet plane) serves only reads you verify yourself, so it can withhold, stall or serve an older certified view, never a wrong one. Before you use one, decide who it is from data you verified, never from its own say-so: take node ids from the eligible lists of macroblocks you verified (bound by theirepoch_commitment) and not in the newest verifiedbannedset; a node id's address from/api/v1/nodes/rpcis a hint. Admit the address only whenGET /api/v1/node/serve-proofwith your own fresh random challenge returns a ServeCert that decodes strictly, names that node id, the address you connected to and a current lifetime of at most 7 days, whose wallet is the EON of itswallet_pkand whose node id is that wallet's pseudonym, with the wallet's signature overq1337|node_serve_cert:{hex cert_hash}and the serving key's signature overq1337|node_serve_proof:{challenge}:{hex cert_hash}:{ip}both verifying. Refuse any address that is not public unicast IPv4. The public RPC never serves a read that names an exact address in a way you did not choose: send exact-address reads (nonce, token balances, history) to the genesis names unless you mean to show that address to the node you ask. - Token names and symbols are the deployer's text. Any token can call itself QNC or copy another token's name. Identify a token by its contract address, and show the address.
- Fix the owner at build time. No constructor runs at deploy, and the contract address is known
before the deploy lands, so an
initentry that stores its first caller can be called first by someone else. Thegame-itemstemplate compiles its owner in. - A call carries no QNC.
get_valueis 0 in the entry call and only informational in a nested call; never treat it as a payment. get_calleris the transaction's sender in the entry call and the calling contract in a nested one. Authorize by comparing it with an address the contract trusts.- Emit an event from every entry that changes state. Events are how clients learn that a call took effect; a reverted call keeps none.
- Check the module before deploying.
qnet check(orcargo check-contract) catches what a node does not check at deploy: wrong imports, a missingmemoryexport, entries of the wrong type. A module deployed with such a fault can never be called, and code cannot be replaced. - Keep contracts deterministic and small. Floating point is refused; standard-library collections quickly pass the 24,949-byte deploy limit.
- The extension gives its provider only to the top frame of
aiqnet.ioandgames.aiqnet.io, and the app only to the top frame of the page it shows. Do not load your game in a frame of another site and expect a wallet there. - Serve pages over HTTPS with a strict content security policy: a script injected into your page can call the provider too, and while the wallet shows the user every request, a trusted page is what makes a user approve.
- Handle a rejection (4001) as final. Repeating a request after the user said no makes the wallet pause the site (dApp integration).
- Never ask users to switch off a wallet warning, and never describe a contract call as something it is not: the wallet shows the method and input, and users compare.
- Respect the node limits. Each IP may make about 300 reads and 100 submits a minute on a node by
default, and 5 deploys an hour; a throttled request answers
{"success":false,"error":"Rate limit exceeded","retry_after_seconds":N}with HTTP 200. Back off forretry_after_seconds, cache what changes slowly, and spread reads over the public nodes (NodeClientdoes). - Bound what you read.
NodeClientcaps every answer (8 MiB by default); a raw client should too.GET /api/v1/account/{contract}returns a contract's whole storage and code. - Talk to nodes over HTTPS (
https://node1.aiqnet.io…node5), or over plain HTTP only on the same machine.
- Sign-in, Transactions, Smart contracts
- SDK and CLI
- RPC API: rate limits and error shapes