Skip to main content
ShorScanQRL Testnet v3 (Private)
Hands-on tutorials

Deploy a smart contract and verify it on ShorScan

Updated 8 min read

QRL 2.0 runs an EVM compatible execution layer, so deploying a smart contract feels familiar if you have shipped one on any Ethereum style chain. The toolchain differs in two places: contracts are compiled with Hyperion (hypc), the QRL toolchain's contract compiler, and transactions are signed with post-quantum ML-DSA-87 keys held by Q-prefixed accounts.

This tutorial walks the full loop: write a small Hyperion contract, compile it with hypc, deploy it to the QRL 2.0 testnet with a short @theqrl/web3 script, and then verify the source on ShorScan so anyone can read it, call it, and audit it. If the chain itself is new to you, start with What is QRL 2.0?.

You need Node.js, a funded testnet account, and about twenty minutes. If your goal is a standard token without writing contract code, the wallet has a one-click path: see Launch a QRC20 token.

Hyperion and hypc

Hyperion is a statically typed, contract-oriented language derived from Solidity, andhypc is its compiler. Source files use the .hyp extension and open with a pragma hyperion line. The compiler is developed in the open in the theQRL GitHub organization and is published to npm as @theqrl/hypc, so a plain Node.js project can compile contracts without any extra toolchain.

The compiled bytecode is regular EVM bytecode targeting the chain's virtual machine, which is why the deploy flow below looks like any web3 deploy. In the compiler's standard JSON output the artifacts sit under a zvm key, so the bytecode path is contracts[file][name].zvm.bytecode.object.

Write a simple contract

Save the following as Counter.hyp. It stores one number, exposes a public getter, and emits an event on every increment, enough surface to see the verified Code, Read, and Write tabs in action later.

hyperion
// SPDX-License-Identifier: MIT
pragma hyperion ^0.0.2;

contract Counter {
    uint256 public count;

    event Incremented(address indexed by, uint256 newCount);

    function increment() external {
        count += 1;
        emit Incremented(msg.sender, count);
    }
}

If you know Solidity you can read this without a manual. Existing contracts generally port by renaming the file to .hyp and switching the pragma; standard ERC20 style sources have been ported this way on this network.

Compile with hypc

hypc accepts the standard JSON input format: a language field set to Hyperion, a sources map keyed by filename, and a settings block for the optimizer and output selection. This script compiles the contract and writes the ABI and bytecode to a JSON artifact:

javascript
const fs = require('fs');
const hypc = require('@theqrl/hypc');

const input = {
  language: 'Hyperion',
  sources: {
    'Counter.hyp': { content: fs.readFileSync('./Counter.hyp', 'utf8') },
  },
  settings: { outputSelection: { '*': { '*': ['*'] } } },
};

const output = JSON.parse(hypc.compile(JSON.stringify(input)));
const artifact = output.contracts['Counter.hyp']['Counter'];

fs.writeFileSync('Counter.json', JSON.stringify({
  abi: artifact.abi,
  bytecode: artifact.zvm.bytecode.object,
}));

Record the exact @theqrl/hypc version and optimizer settings you compile with. Verification recompiles your source and byte-matches the result, so those settings must match at verification time.

Deploy to the testnet

The deploy script signs with an ML-DSA-87 account derived from a 34-word mnemonic and talks to the public testnet RPC proxy. The QRL 2.0 testnet uses chain ID 3151909, and the script aborts on a mismatch before signing anything.

Gas on QRL 2.0 is priced by the EIP-1559 fee market, so the script sends a type 2 transaction. @theqrl/web3 fills maxFeePerGas and maxPriorityFeePerGas from the latest block; pass both fields yourself to set your own fee cap. Version 2 of @theqrl/web3 refuses a legacy gasPrice field.

javascript
const { Web3 } = require('@theqrl/web3');
const { MLDSA87 } = require('@theqrl/wallet.js');
const { abi, bytecode } = require('./Counter.json');

const web3 = new Web3(new Web3.providers.HttpProvider(
  'https://qrlwallet.com/api/qrl-rpc/testnet',
));

// A 34-word test mnemonic. Never hardcode one; never reuse a real wallet.
const mnemonic = process.env.MNEMONIC;
const hexseed = MLDSA87.newWalletFromMnemonic(mnemonic).getHexExtendedSeed();
const acc = web3.qrl.accounts.seedToAccount(hexseed);
web3.qrl.wallet.add(hexseed);

async function main() {
  const chainId = BigInt(await web3.qrl.getChainId());
  if (chainId !== 3151909n) throw new Error('unexpected chain: ' + chainId);

  const deploy = new web3.qrl.Contract(abi)
    .deploy({ data: bytecode, arguments: [] });
  const estimated = await deploy.estimateGas({ from: acc.address });

  // Type 2 (EIP-1559) transaction: web3 fills maxFeePerGas and
  // maxPriorityFeePerGas from the latest block.
  const receipt = await web3.qrl.sendTransaction({
    type: '0x2',
    from: acc.address,
    data: deploy.encodeABI(),
    gas: (estimated * 12n) / 10n, // 20% headroom
  });
  console.log('Contract address:', receipt.contractAddress);
}

main().catch((e) => { console.error(e); process.exitCode = 1; });

Run it with MNEMONIC set in your environment. On success it prints the new Q-prefixed contract address. Paste that address into the ShorScan search bar to see the creation transaction, or open the deployment hash directly; How to read a transaction explains every field, and the gas page shows current prices. Your contract also appears in the contracts list.

Verify the source on ShorScan

Right after deployment the explorer only knows your contract's bytecode. Verification publishes the source: you submit it, the backend recompiles it with the pinned Hyperion build you select, and byte-matches the output against the on-chain runtime code. On success the source, ABI, and compiler settings appear on the contract's page.

  1. Open the verify form

    Go to /verify-contract. The form loads the list of supported compiler builds from the backend and preselects the default.

  2. Pick the compiler build

    Choose the exact Hyperion build your contract was compiled with. The byte-match only succeeds against that build, so a version mismatch is the most common cause of a failed verification.

  3. Fill in address, name, and settings

    Enter the Q-prefixed contract address and the contract name, which must match a contract declared in the source (Counter here). Set the optimizer checkbox and runs to whatever you compiled with, tick Via IR if you compiled with viaIR, pick a license, and optionally set an EVM version.

  4. Paste the source

    Paste the full .hyp source into the source field. Multi-file projects put the main file there and supply the rest through the Imports field as a JSON object mapping each file's path to its source contents, for example {"Context.hyp": "...source..."}. Constructor arguments are an optional hex field; the byte-match runs against the deployed runtime code either way.

    If the project keeps its sources in directories, give the files the paths you compiled them under. Set Primary source path to the main file's path, for example contracts/core/Counter.hyp, and key each import by its own path, such as contracts/lib/Math.hyp. A ../lib/Math.hyp import then resolves exactly as it did at deploy time. The quickest route is to load the standard JSON input file your deploy script passed to hypc --standard-json: the form takes every path and the optimizer and viaIR settings from it, and you pick the primary file.

  5. Submit and watch the job

    Press Verify & Publish. Verification runs as an async job: the page shows it moving from queued to compiling and polls until it lands on verified or failed. Failures come back with the compiler error text so you can fix the input and resubmit. If someone already verified the address, the form says so and links straight to the contract page.

What verification unlocks

A verified contract gets a verified badge on its address page, and its Contract section grows three tabs:

  • Code: the published source with license header, the compiler settings used for the match, the full ABI, and the deployed bytecode.
  • Read: every view and pure function becomes callable directly from the explorer through a read-only call proxy, without connecting a wallet.
  • Write: state-changing functions can be dispatched by pairing a wallet over QRL Connect.

Verified contracts also unlock an AI explanation card on the Code tab, which produces a plain-language summary of what the contract does. The explainer only works with verified source, one more reason to publish it.

Verify programmatically

Everything the form does is available over the public API at https://shorscan.com/api, which suits CI pipelines that verify every deployment automatically:

  • GET /api/contract/compiler-info lists the supported Hyperion builds and marks the default.
  • POST /api/contract/verify enqueues a job. The JSON body requires address, sourceCode, and contractName; optional fields include compilerVersion, optimizerEnabled, optimizerRuns, viaIR, evmVersion, primarySourcePath, imports, and license. It returns a jobId. An unknown compilerVersion returns 400 with the supported build list. The endpoint is rate-limited per IP and caps the request body at 1 MiB.
  • GET /api/contract/verify/{jobId} polls the job until it reports success or failed.

The API explorer documents these endpoints alongside the rest of the public API, with example requests you can run in the browser.

FAQ

Can I reuse my existing Solidity contracts?

Largely yes. Hyperion is derived from Solidity, so typical contracts port by renaming files to .hyp and switching the pragma to pragma hyperion. Test the compile with @theqrl/hypc and fix anything the compiler flags.

Verification fails even though my source is correct. Why?

The byte-match requires the exact compiler build and the exact optimizer and via-IR settings used at deploy time. Check the build list on the form or at /api/contract/compiler-info, and make sure the optimizer checkbox, runs value, and Via IR checkbox match your compile. The failed job includes the compiler output to help you diagnose the difference.

Do I need to supply constructor arguments to verify?

No. The field is optional and advisory. The match runs against the deployed runtime code, which does not include constructor arguments.

Does verification cost anything?

Verification is free. Deployment costs gas in testnet Quanta, which you can get for free from the faucet and which carries no monetary value.

My contract imports several files. How do I verify it?

Put the main file in the source field and the imported files in the Imports field as a JSON object mapping each file's path to its full source. When the files live in directories, use the repo-relative paths you compiled with and set the primary source path to the main file's path, so relative imports such as ../lib/Math.hyp resolve as they did at deploy time. Loading your standard JSON input file fills all of this in. The API accepts the same structure in the primarySourcePath and imports body fields.