npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

runar-sdk

v0.4.6

Published

Rúnar SDK: deploy, call, and interact with compiled smart contracts

Readme

runar-sdk

Deploy, call, and interact with compiled Runar smart contracts on BSV.

The SDK provides the runtime layer between compiled contract artifacts and the BSV blockchain. It handles transaction construction, signing, broadcasting, state management for stateful contracts, and UTXO tracking.


Installation

pnpm add runar-sdk

Contract Lifecycle

A Rúnar contract goes through four stages:

  [1. Instantiate]     Load the compiled artifact and set constructor parameters.
         |
         v
  [2. Deploy]          Build a transaction with the locking script, sign, and broadcast.
         |
         v
  [3. Call]            Build an unlocking transaction to invoke a public method.
         |
         v
  [4. Read State]      (Stateful only) Read state from the contract's current UTXO.

Full Example

import { RunarContract, WhatsOnChainProvider, LocalSigner } from 'runar-sdk';
import P2PKHArtifact from './artifacts/P2PKH.json';

// 1. Instantiate
const provider = new WhatsOnChainProvider('testnet');
const signer = new LocalSigner('a1b2c3...');  // 32-byte hex private key or WIF

const contract = new RunarContract(P2PKHArtifact, [
  '89abcdef0123456789abcdef0123456789abcdef',  // pubKeyHash constructor arg
]);

// 2. Connect provider and signer (optional — avoids passing them on every call)
contract.connect(provider, signer);

// 3. Deploy (uses connected provider/signer)
const { txid } = await contract.deploy({ satoshis: 10000 });
console.log('Deployed:', txid);

// 4. Call a public method
// For P2PKH, the unlock method takes a public key as the contract method argument.
// The SDK handles transaction signing internally via the connected signer.
const pubKey = await signer.getPublicKey();
const result = await contract.call('unlock', [pubKey]);
console.log('Spent:', result.txid);

// You can also pass provider/signer explicitly (overrides connected ones):
// await contract.deploy(provider, signer, { satoshis: 10000 });
// await contract.call('unlock', [pubKey], provider, signer);

Stateful Contract Example

import { RunarContract, WhatsOnChainProvider, LocalSigner } from 'runar-sdk';
import CounterArtifact from './artifacts/Counter.json';

const provider = new WhatsOnChainProvider('testnet');
const signer = new LocalSigner('KwDiBf89QgGbjEhKnhX...');  // WIF key also accepted

const counter = new RunarContract(CounterArtifact, [0n]);  // initial count
counter.connect(provider, signer);

// Deploy with initial state
const { txid } = await counter.deploy({ satoshis: 10000 });

// Read current state (synchronous)
console.log('Count:', counter.state.count);  // 0n

// Call increment (uses connected provider/signer)
await counter.call('increment', [], {
  satoshis: 9500,
  newState: { count: 1n },
});
console.log('Count after increment:', counter.state.count);  // 1n

// Call again
await counter.call('increment', [], {
  satoshis: 9000,
  newState: { count: 2n },
});
console.log('Count:', counter.state.count);  // 2n

Reconnecting to a Deployed Contract

// Reconnect to an existing on-chain contract by txid
const contract = await RunarContract.fromTxId(
  CounterArtifact,
  'abc123...',    // txid
  0,              // output index
  provider,
);

console.log('Current state:', contract.state);

Providers

Providers handle communication with the BSV network: fetching UTXOs, broadcasting transactions, and querying transaction data.

WhatsOnChainProvider

Connects to the WhatsOnChain API for mainnet or testnet:

import { WhatsOnChainProvider } from 'runar-sdk';

const mainnet = new WhatsOnChainProvider('mainnet');
const testnet = new WhatsOnChainProvider('testnet');

// Fetch UTXOs for an address
const utxos = await testnet.getUtxos('1A1zP1...');

// Broadcast a raw transaction
const txid = await testnet.broadcast('0100000001...');  // raw tx hex

// Fetch transaction details
const tx = await testnet.getTransaction(txid);

// Get the network name
const network = testnet.getNetwork();  // 'testnet'

// Get the current fee rate (sat/byte)
const feeRate = await testnet.getFeeRate();  // 1 (BSV standard)

MockProvider

For unit testing without network access:

import { MockProvider } from 'runar-sdk';

const mock = new MockProvider();

// Pre-register UTXOs (keyed by address)
mock.addUtxo('1A1zP1...', {
  txid: 'abc123...',
  outputIndex: 0,
  satoshis: 10000,
  script: '76a914...88ac',
});

// Pre-register transactions (for getTransaction() lookups)
mock.addTransaction({
  txid: 'abc123...',
  version: 1,
  inputs: [],
  outputs: [{ satoshis: 10000, script: '76a914...88ac' }],
  locktime: 0,
});

// broadcast() returns a deterministic fake txid but does NOT register the
// transaction in the mock store. Calling getTransaction() with the returned
// txid will throw unless you pre-register it with addTransaction().
const txid = await mock.broadcast(rawTx);
// mock.getTransaction(txid) would throw -- the broadcast is recorded but
// the transaction is not stored. Use addTransaction() to pre-populate.

// Inspect what was broadcast (raw tx hex strings)
const broadcastedTxs = mock.getBroadcastedTxs();

// Override the fee rate (default is 1 sat/byte)
mock.setFeeRate(2);

RPCProvider

Connects directly to a Bitcoin node via JSON-RPC. Suitable for regtest and testnet integration testing:

import { RPCProvider } from 'runar-sdk';

const provider = new RPCProvider(
  'http://localhost:18332',  // node RPC URL
  'bitcoin',                 // RPC username
  'bitcoin',                 // RPC password
  {
    network: 'testnet',      // 'mainnet' | 'testnet' (default: 'testnet')
    autoMine: true,          // auto-mine 1 block after broadcast (default: false)
    mineAddress: '',         // mining address for generatetoaddress (optional)
  },
);

Note: getContractUtxo() always returns null on RPCProvider — use address-based UTXO tracking instead.

Custom Provider

Implement the Provider interface for other network APIs:

import { Provider, UTXO, Transaction } from 'runar-sdk';

class MyProvider implements Provider {
  async getUtxos(address: string): Promise<UTXO[]> {
    // Your implementation
  }

  async broadcast(rawTx: string): Promise<string> {
    // Your implementation -- returns txid
  }

  async getTransaction(txid: string): Promise<Transaction> {
    // Your implementation
  }

  async getRawTransaction(txid: string): Promise<string> {
    // Return raw tx hex for the given txid
  }

  async getContractUtxo(scriptHash: string): Promise<UTXO | null> {
    // Find UTXO by script hash (for stateful contract lookup)
  }

  getNetwork(): 'mainnet' | 'testnet' {
    // Return the network
  }

  async getFeeRate(): Promise<number> {
    return 1;  // BSV standard: 1 sat/byte
  }
}

Signers

Signers handle private key operations: signing transactions and deriving public keys.

LocalSigner

Holds a private key in memory. Uses @bsv/sdk for secp256k1 key derivation and ECDSA signing with BIP-143 sighash preimage computation. Accepts either a 64-char hex string or a WIF-encoded key:

import { LocalSigner } from 'runar-sdk';

// From hex
const signer = new LocalSigner('a1b2c3...');  // 32-byte hex private key

// From WIF (Base58Check, starts with 5/K/L)
const signerWif = new LocalSigner('KwDiBf89QgGbjEhKnhX...');

const pubKey = await signer.getPublicKey();    // compressed public key hex
const address = await signer.getAddress();     // P2PKH address

// Sign a transaction input
const signature = await signer.sign(
  txHex,          // raw transaction hex
  inputIndex,     // which input to sign
  subscript,      // locking script of the UTXO being spent
  satoshis,       // value of the UTXO being spent
  sigHashType,    // optional, defaults to SIGHASH_ALL | SIGHASH_FORKID (0x41)
);

ExternalSigner

Delegates signing to a caller-provided callback. Useful for hardware wallets and browser extensions:

import { ExternalSigner, SignCallback } from 'runar-sdk';

const signFn: SignCallback = async (txHex, inputIndex, subscript, satoshis, sigHashType?) => {
  // Request signature from hardware wallet / browser extension
  return derSignatureHex;
};

const signer = new ExternalSigner(
  pubKeyHex,    // 33-byte compressed public key (66 hex chars)
  addressStr,   // Base58Check BSV address
  signFn,
);

WalletSigner

Delegates signing to a BRC-100 compatible wallet via @bsv/sdk's WalletClient. Computes BIP-143 sighash locally, then sends the pre-hashed digest to the wallet for ECDSA signing:

import { WalletSigner } from 'runar-sdk';

const signer = new WalletSigner({
  protocolID: [2, 'my app'],  // BRC-100 protocol ID
  keyID: '1',                 // Key derivation ID
  // wallet: existingClient,  // Optional pre-existing WalletClient
});

Custom Signer

Implement the Signer interface:

import { Signer } from 'runar-sdk';

class MySigner implements Signer {
  async getPublicKey(): Promise<string> {
    // Return compressed public key hex (66 chars)
  }

  async getAddress(): Promise<string> {
    // Return Base58Check P2PKH address
  }

  async sign(
    txHex: string,
    inputIndex: number,
    subscript: string,
    satoshis: number,
    sigHashType?: number,
  ): Promise<string> {
    // Return DER-encoded signature + sighash byte, hex-encoded
  }
}

Script Access

Methods on RunarContract for direct script and state manipulation:

// Get the full locking script hex (code + OP_RETURN + state for stateful contracts)
const lockingScript = contract.getLockingScript();

// Build an unlocking script for a method call
const unlock = contract.buildUnlockingScript('transfer', [sigHex, pubKeyHex]);

// Update state directly (useful for testing)
contract.setState({ count: 5n });

Signatures

| Method | Signature | |---|---| | getLockingScript | getLockingScript(): string | | buildUnlockingScript | buildUnlockingScript(methodName: string, args: unknown[]): string | | setState | setState(newState: Record<string, unknown>): void |


Stateful Contract Support

State Chaining

Stateful contracts maintain state across transactions using the OP_PUSH_TX pattern. The SDK manages this automatically:

  1. Deploy: The initial state is serialized and appended after an OP_RETURN separator in the locking script.
  2. Call: The SDK reads the current state from the existing UTXO, builds the unlocking script, and creates a new output with the updated locking script containing the new state.
  3. Read: The SDK deserializes state from the UTXO's locking script.

State Serialization Format

The SDK knows the contract's state schema from the artifact's stateFields array. State is stored as a suffix of the locking script:

<code_part> OP_RETURN <field_0_bytes> <field_1_bytes> ... <field_n_bytes>

Each field is encoded as Bitcoin Script push data, ordered by the field's index property. Type-specific encoding:

  • int/bigint: minimally-encoded Script integers (with sign byte)
  • bool: OP_0 for false, OP_1 for true
  • bytes/ByteString/PubKey/Ripemd160/Addr/Sha256: direct pushdata

Deserialization reverses this: the SDK finds the last OP_RETURN at an opcode boundary (skipping push data), extracts the suffix, and decodes each field.

UTXO Management

For stateful contracts, the SDK tracks the "current" UTXO internally. After each call, the SDK updates its pointer to the new UTXO created by the transaction.

// The SDK tracks the current UTXO automatically (uses connected provider/signer)
const tx1 = await counter.call('increment', [], {
  satoshis: 9500,
  newState: { count: 1n },
});
// counter now points to the new UTXO created by tx1

const tx2 = await counter.call('increment', [], {
  satoshis: 9000,
  newState: { count: 2n },
});
// counter now points to the new UTXO created by tx2

Token Support

The SDK provides a TokenWallet utility for managing fungible token contracts:

import { TokenWallet, WhatsOnChainProvider, LocalSigner } from 'runar-sdk';

const provider = new WhatsOnChainProvider('testnet');
const signer = new LocalSigner('a1b2c3...');

const wallet = new TokenWallet(FungibleTokenArtifact, provider, signer);

// Get total balance across all token UTXOs
const balance = await wallet.getBalance();
console.log('Balance:', balance);

// Transfer tokens to a recipient
const txid = await wallet.transfer(recipientAddress, 500n);

// Merge two UTXOs into one (calls the contract's merge() method)
const mergeTxid = await wallet.merge();

// List all token UTXOs
const utxos = await wallet.getUtxos();

Transaction Building Utilities

The SDK exports lower-level functions for custom transaction construction:

import {
  buildDeployTransaction,
  buildCallTransaction,
  selectUtxos,
  estimateDeployFee,
  serializeState,
  deserializeState,
  extractStateFromScript,
} from 'runar-sdk';

// Select UTXOs (largest-first strategy)
const selected = selectUtxos(utxos, targetSatoshis, lockingScriptByteLen, feeRate);

// Estimate deployment fee (default 1 sat/byte)
const fee = estimateDeployFee(numInputs, lockingScriptByteLen, feeRate);

// Build an unsigned deploy transaction
const { txHex, inputCount } = buildDeployTransaction(
  lockingScript, utxos, satoshis, changeAddress, changeScript, feeRate,
);

// Build a method call transaction
const { txHex: callTxHex, inputCount: callInputCount } = buildCallTransaction(
  currentUtxo, unlockingScript, newLockingScript, newSatoshis,
  changeAddress, changeScript, additionalUtxos, feeRate,
);

// State serialization
const stateHex = serializeState(stateFields, { count: 5n });
const stateObj = deserializeState(stateFields, stateHex);
const extracted = extractStateFromScript(artifact, fullLockingScriptHex);

Types

interface Transaction {
  txid: string;
  version: number;
  inputs: TxInput[];
  outputs: TxOutput[];
  locktime: number;
  raw?: string;
}

interface TxInput {
  txid: string;
  outputIndex: number;
  script: string;     // hex
  sequence: number;
}

interface TxOutput {
  satoshis: number;
  script: string;     // hex
}

interface UTXO {
  txid: string;
  outputIndex: number;
  satoshis: number;
  script: string;     // hex
}

interface DeployOptions {
  satoshis?: number;       // defaults to 1
  changeAddress?: string;
}

interface CallOptions {
  satoshis?: number;
  changeAddress?: string;
  changePubKey?: string;
  newState?: Record<string, unknown>;
  outputs?: Array<{ satoshis: number; state: Record<string, unknown> }>;
  additionalContractInputs?: UTXO[];
  additionalContractInputArgs?: unknown[][];
  terminalOutputs?: Array<{ scriptHex: string; satoshis: number }>;
}

Code Generation

Generate typed wrapper classes from compiled artifacts instead of using stringly-typed contract.call():

# CLI
runar codegen artifacts/*.json -o src/generated/
// Programmatic
import { generateTypescript } from 'runar-sdk';
const code = generateTypescript(artifact);

The generated wrapper provides typed methods, hides auto-computed params (Sig, SigHashPreimage), and distinguishes terminal from state-mutating methods:

import { AuctionContract } from './generated/AuctionContract.js';

const auction = new AuctionContract(artifact, {
  auctioneer: myPubKey,
  highestBidder: myPubKey,
  highestBid: 0n,
  deadline: 1000n,
});
auction.connect(provider, signer);
await auction.deploy({ satoshis: 10000 });

// State-mutating — Sig auto-computed, options optional
await auction.bid(bidderPubKey, 5000n, { satoshis: 10000 });

// Terminal — outputs use address (converted to P2PKH) or raw scriptHex
await auction.close([{ address: winnerAddr, satoshis: 9000 }]);

The underlying RunarContract is accessible via .contract for advanced use cases.

See the API Reference for full details on parameter handling and generated class structure.


OP_PUSH_TX Helper

For contracts that use checkPreimage(), the SDK provides computeOpPushTx to compute the BIP-143 sighash preimage and OP_PUSH_TX signature:

import { computeOpPushTx } from 'runar-sdk';

const { sigHex, preimageHex } = computeOpPushTx(
  txHex,       // raw transaction hex (with placeholder unlocking scripts)
  inputIndex,  // the contract input index (usually 0)
  subscript,   // locking script of the UTXO being spent (hex)
  satoshis,    // satoshi value of the UTXO being spent
);

This is called internally by RunarContract.call() for stateful contracts, but is exposed for manual transaction building workflows. The function uses the OP_PUSH_TX technique with private key k=1 (public key = generator point G).


Design Decision: Provider/Signer Abstraction

The provider and signer are separate abstractions because they serve different trust boundaries:

  • Provider handles read operations (fetching UTXOs, querying transactions) and write operations (broadcasting). It does NOT hold private keys. A provider can be swapped between mainnet, testnet, and mocks without changing any contract logic.

  • Signer handles private key operations only. It never touches the network directly. This separation means you can use a LocalSigner for development and swap in an ExternalSigner for production without changing your provider configuration.

This pattern enables:

  • Testing with MockProvider + LocalSigner (no network, fast).
  • Staging with WhatsOnChainProvider('testnet') + LocalSigner (real network, test keys).
  • Production with WhatsOnChainProvider('mainnet') + ExternalSigner (real network, hardware wallet).