Rúnar

Hello World Contract

This tutorial walks you through creating, compiling, testing, and deploying a minimal “Hello World” contract on BSV using Runar. You will build a Pay-to-Public-Key-Hash (P2PKH) contract — the most fundamental Bitcoin spending condition — and take it from source code to a live transaction on the BSV testnet.

By the end of this tutorial, you will understand:

  • The structure of a SmartContract in Runar
  • How constructor arguments become embedded locking script parameters
  • How public methods define spending conditions
  • The full workflow: write, compile, test, deploy, verify

Prerequisites

Before starting, make sure you have:

  • Runar CLI installed (see Installation)
  • Node.js >= 20 and pnpm 9.15+
  • A BSV testnet wallet with some test coins (needed for the deployment step at the end)

Setting Up the Project

Scaffold a new Runar project using the CLI:

runar init hello-world
cd hello-world
pnpm install

This creates the standard Runar project layout:

hello-world/
  contracts/       # Your .runar.ts contract source files
  tests/           # Test files (*.test.ts)
  artifacts/       # Compiled JSON artifacts (generated)
  package.json
  tsconfig.json

The src/contracts/ directory is where your contract source lives. The tests/ directory is pre-configured with vitest. The artifacts/ directory will be populated when you compile.

Writing the Hello World Contract

Create a new file at src/contracts/P2PKH.runar.ts. This implements Pay-to-Public-Key-Hash, the standard Bitcoin pattern where funds can only be spent by proving ownership of the private key that corresponds to a given public key hash.

import {
  SmartContract,
  assert,
  PubKey,
  Sig,
  Ripemd160,
  hash160,
  checkSig,
} from 'runar-lang';

class P2PKH extends SmartContract {
  readonly pubKeyHash: Ripemd160;

  constructor(pubKeyHash: Ripemd160) {
    super(pubKeyHash);
    this.pubKeyHash = pubKeyHash;
  }

  public unlock(sig: Sig, pubKey: PubKey) {
    assert(hash160(pubKey) === this.pubKeyHash);
    assert(checkSig(sig, pubKey));
  }
}

export { P2PKH };

Let’s break down every part of this contract:

The Import Statement

import { SmartContract, assert, PubKey, Sig, Ripemd160, hash160, checkSig } from 'runar-lang';

All Runar contracts import from runar-lang. This is the only allowed import in contract files. The import provides the base class (SmartContract), assertion function (assert), cryptographic types (PubKey, Sig, Ripemd160), and built-in functions (hash160, checkSig).

The Class Declaration

class P2PKH extends SmartContract {

The contract extends SmartContract, which means it is stateless. All properties are readonly and baked into the locking script at deployment. Once the UTXO is spent, the contract instance is consumed.

The Property

readonly pubKeyHash: Ripemd160;

Ripemd160 is a 20-byte type representing a RIPEMD-160 hash digest. The readonly keyword is required for SmartContract properties — stateless contracts cannot have mutable state. This value is embedded directly in the compiled Bitcoin Script.

The Constructor

constructor(pubKeyHash: Ripemd160) {
  super(pubKeyHash);
  this.pubKeyHash = pubKeyHash;
}

The constructor takes the public key hash as a deployment-time parameter. The super() call registers it as a constructor argument so the compiler knows to embed it in the script. Every Runar constructor must call super() — omitting it is a compile-time error.

The Public Method

public unlock(sig: Sig, pubKey: PubKey) {
  assert(hash160(pubKey) === this.pubKeyHash);
  assert(checkSig(sig, pubKey));
}

Public methods define spending conditions. When someone wants to spend this UTXO, they must provide arguments that satisfy all assertions in a public method:

  1. hash160(pubKey) === this.pubKeyHash — The provided public key must hash to the stored hash. This binds the contract to a specific key pair without revealing the full public key until spending time.
  2. checkSig(sig, pubKey) — The signature must be valid for the provided public key. This proves the spender controls the corresponding private key.

If both assertions pass, the UTXO is spent. If either fails, the transaction is rejected by the network.

Compiling the Contract

Compile the contract source into a deployable artifact:

runar compile contracts/P2PKH.runar.ts --output ./artifacts --asm

This produces artifacts/P2PKH.json. The --asm flag includes human-readable assembly in the output, which is useful for learning and debugging.

Understanding the Artifact

The compiled artifact is a JSON file containing everything needed to deploy and interact with the contract:

{
  "version": "runar-v0.1.0",
  "compilerVersion": "0.1.0",
  "contractName": "P2PKH",
  "abi": {
    "constructor": {
      "params": [
        { "name": "pubKeyHash", "type": "Ripemd160" }
      ]
    },
    "methods": [
      {
        "name": "unlock",
        "params": [
          { "name": "sig", "type": "Sig" },
          { "name": "pubKey", "type": "PubKey" }
        ]
      }
    ]
  },
  "script": "76a914<pubKeyHash>88ac",
  "asm": "OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG"
}

Key fields:

  • abi — The contract’s interface. The SDK and codegen tools use this to construct transactions.
  • script — Hex-encoded Bitcoin Script with <pubKeyHash> as a placeholder, filled in at deployment.
  • asm — Human-readable assembly. This P2PKH compiles to the classic Bitcoin opcodes: OP_DUP OP_HASH160 <hash> OP_EQUALVERIFY OP_CHECKSIG.

Notice that the Runar contract compiles to the exact same Bitcoin Script as a standard P2PKH output. The Runar language is a zero-overhead abstraction over Bitcoin Script.

Writing and Running a Test

Create a test file at tests/P2PKH.test.ts:

import { describe, it, expect } from 'vitest';
import { TestContract, ALICE, BOB } from 'runar-testing';
import { readFileSync } from 'fs';

describe('P2PKH', () => {
  const source = readFileSync('./src/contracts/P2PKH.runar.ts', 'utf-8');

  it('should unlock with correct key pair', () => {
    const contract = TestContract.fromSource(source, {
      pubKeyHash: ALICE.pubKeyHash,
    });

    // Unlock should succeed with matching key pair
    const result = contract.call('unlock', {
      sig: ALICE.privKey,
      pubKey: ALICE.pubKey,
    });

    expect(result.success).toBe(true);
  });

  it('should fail with wrong key pair', () => {
    const contract = TestContract.fromSource(source, {
      pubKeyHash: ALICE.pubKeyHash,
    });

    // Unlock should fail when signed with wrong key
    const result = contract.call('unlock', {
      sig: BOB.privKey,
      pubKey: BOB.pubKey,
    });

    expect(result.success).toBe(false);
  });

  it('should fail when pubKey does not match hash', () => {
    // Lock to ALICE's hash
    const contract = TestContract.fromSource(source, {
      pubKeyHash: ALICE.pubKeyHash,
    });

    // Sign correctly with BOB, but provide BOB's pubKey (wrong hash)
    const result = contract.call('unlock', {
      sig: BOB.privKey,
      pubKey: BOB.pubKey,
    });

    // Signature is valid for BOB, but hash160(BOB.pubKey) !== pubKeyHash
    expect(result.success).toBe(false);
  });
});

Run the tests:

runar test

Expected output:

 ✓ tests/P2PKH.test.ts (3 tests) 62ms
   ✓ P2PKH > should unlock with correct key pair
   ✓ P2PKH > should fail with wrong key pair
   ✓ P2PKH > should fail when pubKey does not match hash

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  345ms

Tests run entirely locally. No BSV node or network connection is required. The TestContract utility compiles the contract on the fly and executes the Bitcoin Script in a local interpreter.

Deploying to Testnet

Once your tests pass, deploy the contract to the BSV testnet. You need a WIF-format private key with testnet coins.

runar deploy ./artifacts/P2PKH.json \
  --network testnet \
  --key <your-WIF-private-key> \
  --satoshis 10000

The --satoshis flag specifies how many satoshis to lock in the contract output. On success:

Contract deployed successfully.
  Network:  testnet
  TxID:     a1b2c3d4e5f6...
  Output:   0
  Satoshis: 10000
  Script:   76a914...88ac

The deploy command constructs a transaction with one output whose locking script is the compiled P2PKH script (with your public key hash substituted in). It broadcasts the transaction to the BSV testnet.

Verifying the Deployment

Confirm the on-chain script matches your compiled artifact:

runar verify a1b2c3d4e5f6... \
  --artifact ./artifacts/P2PKH.json \
  --network testnet

This fetches the transaction from the network and compares the on-chain locking script against the artifact:

Verification passed.
  Contract:  P2PKH
  TxID:      a1b2c3d4e5f6...
  Script match: ✓

Key Concepts Recap

ConceptWhat You Learned
SmartContractStateless contracts with readonly properties embedded in the locking script
constructor + super()How deployment-time parameters are registered with the compiler
Public methodsSpending conditions that callers must satisfy with valid arguments
assert()The assertion primitive — every public method must contain at least one
hash160 / checkSigBuilt-in cryptographic functions that map to Bitcoin Script opcodes
TestContractLocal contract testing without network dependencies
runar compileSource-to-artifact compilation with optional ASM output
runar deployBroadcasting a contract to the BSV network

What’s Next

You have completed the full Runar workflow: write, compile, test, deploy, and verify. Continue with:

  • Fungible Token Tutorial — Build a stateful token contract with transfer, send, and merge operations
  • Contract Basics — Deep dive into the type system, built-in functions, and compiler constraints
  • Project Structure — Understand the full project layout and artifact format