runar-lang 0.2.0

Types and mock functions for Rúnar smart contract development in Rust
Documentation

runar-rs

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

The Rust 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

Add to your Cargo.toml:

[dependencies]
runar = { package = "runar-lang", version = "0.1.0" }

Contract Lifecycle

A Runar 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

use runar::sdk::*;

// 1. Load the artifact (compiled contract JSON)
let artifact: RunarArtifact = serde_json::from_str(&json)?;

// 2. Create the contract with constructor arguments
//    Panics if the number of args does not match the artifact's constructor params.
let mut contract = RunarContract::new(artifact, vec![
    SdkValue::Bytes(pub_key_hash),
]);

// 3. Set up provider and signer
//    Use ExternalSigner to wrap a real signing library
let signer = ExternalSigner::new(
    || Ok(pub_key_hex.clone()),
    || Ok(address.clone()),
    |tx_hex, input_index, subscript, satoshis, sig_hash_type| {
        // Delegate to your signing library here
        sign_with_rust_sv(tx_hex, input_index, subscript, satoshis, sig_hash_type)
    },
);
let mut provider = MockProvider::testnet();

// 4. Deploy
let (txid, tx) = contract.deploy(&mut provider, &signer, &DeployOptions {
    satoshis: 10_000,
    change_address: None,
})?;

// 5. Call a public method
let (txid2, tx2) = contract.call("unlock", &[
    SdkValue::Bytes(sig),
    SdkValue::Bytes(pub_key),
], &mut provider, &signer, None)?;

Connected API

Instead of passing the provider and signer to every deploy() and call() invocation, you can store them on the contract with connect() and then use deploy_connected() / call_connected():

use runar::sdk::*;

let mut contract = RunarContract::new(artifact, vec![SdkValue::Int(0)]);

// Store provider and signer on the contract
contract.connect(
    Box::new(MockProvider::testnet()),
    Box::new(MockSigner::new()),
);

// Deploy without passing provider/signer
let (txid, tx) = contract.deploy_connected(&DeployOptions {
    satoshis: 10_000,
    change_address: None,
})?;

// Call without passing provider/signer
let (txid2, tx2) = contract.call_connected("increment", &[], None)?;

If connect() has not been called, deploy_connected() and call_connected() return an error.

Stateful Contract Example

use std::collections::HashMap;

// Create with initial state
let mut contract = RunarContract::new(counter_artifact, vec![SdkValue::Int(0)]);

// Deploy
let (txid, _) = contract.deploy(&mut provider, &signer, &DeployOptions {
    satoshis: 10_000,
    change_address: None,
})?;

// Read current state
println!("Count: {:?}", contract.state().get("count")); // Some(Int(0))

// Call increment with updated state
let mut new_state = HashMap::new();
new_state.insert("count".to_string(), SdkValue::Int(1));
let (txid2, _) = contract.call("increment", &[], &mut provider, &signer, Some(&CallOptions {
    satoshis: Some(9_500),
    change_address: None,
    new_state: Some(new_state),
}))?;
println!("Count: {:?}", contract.state().get("count")); // Some(Int(1))

// Update state directly (without a call)
let mut override_state = HashMap::new();
override_state.insert("count".to_string(), SdkValue::Int(99));
contract.set_state(override_state);

Reconnecting to a Deployed Contract

// Reconnect to an existing on-chain contract by txid
let contract = RunarContract::from_txid(artifact, &txid, 0, &provider)?;
println!("Current state: {:?}", contract.state());

Script Access

// Get the full locking script hex (code + OP_RETURN + state for stateful contracts)
let locking_script = contract.get_locking_script();

// Build an unlocking script for a method call
let unlock = contract.build_unlocking_script("transfer", &[
    SdkValue::Bytes(sig_hex),
    SdkValue::Bytes(pubkey_hex),
])?;

Providers

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

MockProvider

For unit testing without network access:

// Create with a specific network name
let mut provider = MockProvider::new("mainnet");
// Or use the testnet shorthand
let mut provider = MockProvider::testnet();

// Pre-register UTXOs
provider.add_utxo("myAddress", Utxo {
    txid: "abc123...".to_string(),
    output_index: 0,
    satoshis: 10_000,
    script: "76a914...88ac".to_string(),
});

// Pre-register transactions
provider.add_transaction(Transaction { /* ... */ });

// Pre-register contract UTXOs for stateful lookup
provider.add_contract_utxo("scripthash...", Utxo { /* ... */ });

// Inspect broadcasts after deploying/calling
let broadcasted: &[String] = provider.get_broadcasted_txs();

// Override the fee rate (default 1 sat/byte)
provider.set_fee_rate(2);

Custom Provider

Implement the Provider trait for other network APIs:

pub trait Provider {
    fn get_transaction(&self, txid: &str) -> Result<Transaction, String>;
    fn get_raw_transaction(&self, txid: &str) -> Result<String, String>;
    fn broadcast(&mut self, raw_tx: &str) -> Result<String, String>;
    fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>, String>;
    fn get_contract_utxo(&self, script_hash: &str) -> Result<Option<Utxo>, String>;
    fn get_network(&self) -> &str;
    fn get_fee_rate(&self) -> Result<i64, String>;
}

Production providers are not included in this crate -- implement the trait using your preferred HTTP client.


Signers

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

MockSigner

For unit testing without real crypto. Returns deterministic dummy values (a fixed 66-char hex public key, a fixed 40-char hex address, and a fixed mock DER signature):

let signer = MockSigner::new();
let pub_key = signer.get_public_key()?;  // "0200...00" (66-char hex)
let address = signer.get_address()?;     // "0000...00" (40-char hex)
let sig = signer.sign(tx_hex, 0, subscript, satoshis, None)?;

// Fields are public for customization in tests
let custom = MockSigner {
    public_key: "02aabb...".to_string(),
    address: "myaddr".to_string(),
};

MockSigner also implements Default (equivalent to MockSigner::new()).

LocalSigner

Holds a private key in memory and performs real secp256k1 ECDSA signing with BIP-143 sighash computation. Uses the k256 crate for ECDSA and manual BIP-143 preimage construction. Accepts hex or WIF private keys:

// From hex
let signer = LocalSigner::new("0000...0001")?;

// From WIF
let signer = LocalSigner::new("KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn")?;

let pub_key = signer.get_public_key()?;  // 66-char compressed pubkey hex
let address = signer.get_address()?;     // mainnet P2PKH address (starts with "1")
let sig = signer.sign(tx_hex, 0, subscript, satoshis, None)?;  // DER + sighash byte

Suitable for CLI tooling and testing. For production wallets, use ExternalSigner with hardware wallet callbacks.

ExternalSigner

Delegates signing to caller-provided closures. Use this to wrap real signing libraries (e.g., rust-sv, secp256k1):

let signer = ExternalSigner::new(
    || Ok("02aabb...".to_string()),           // get_public_key
    || Ok("1Address...".to_string()),          // get_address
    |tx_hex, idx, sub, sats, sht| {            // sign
        Ok(your_sign_fn(tx_hex, idx, sub, sats, sht))
    },
);

Custom Signer

Implement the Signer trait:

pub trait Signer {
    fn get_public_key(&self) -> Result<String, String>;
    fn get_address(&self) -> Result<String, String>;
    fn sign(
        &self,
        tx_hex: &str,
        input_index: usize,
        subscript: &str,
        satoshis: i64,
        sig_hash_type: Option<u32>,
    ) -> Result<String, String>;
}

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: contract.state() returns the deserialized state as &HashMap<String, SdkValue>.

State Serialization Format

State is stored as a suffix of the locking script:

<code_part> OP_RETURN <field_0> <field_1> ... <field_n>

Type-specific encoding:

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

The find_last_op_return() function uses opcode-aware walking to locate the real OP_RETURN boundary, properly skipping 0x6a bytes inside push data payloads.


Value Types

The SdkValue enum represents typed contract values:

pub enum SdkValue {
    Int(i64),
    Bool(bool),
    Bytes(String),  // hex-encoded
    Auto,           // placeholder for auto-computed Sig/PubKey parameters
}

Convenience accessors are available. They panic if called on the wrong variant:

let n: i64 = value.as_int();    // panics if not Int
let b: bool = value.as_bool();  // panics if not Bool
let s: &str = value.as_bytes(); // panics if not Bytes

Transaction Building Utilities

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

use runar::sdk::deployment::*;
use runar::sdk::calling::*;
use runar::sdk::state::*;

// Select UTXOs (largest-first strategy)
let selected = select_utxos(&utxos, target_satoshis, locking_script_byte_len, Some(fee_rate));

// Estimate the fee for a deploy transaction
let fee = estimate_deploy_fee(num_inputs, locking_script_byte_len, Some(fee_rate));

// Build an unsigned deploy transaction
// Panics if utxos is empty or if total funds are insufficient.
let (tx_hex, input_count) = build_deploy_transaction(
    &locking_script, &utxos, satoshis, change_address, &change_script, Some(fee_rate),
);

// Build a method call transaction
let (tx_hex, input_count) = build_call_transaction(
    &current_utxo, &unlocking_script, Some(new_locking_script), Some(new_satoshis),
    Some(change_address), Some(&change_script), Some(&additional_utxos), Some(fee_rate),
);

// State serialization
let state_hex = serialize_state(&state_fields, &values);
let state: HashMap<String, SdkValue> = deserialize_state(&state_fields, &state_hex);
let state: Option<HashMap<String, SdkValue>> = extract_state_from_script(&artifact, &full_script);

// Opcode-aware OP_RETURN finder (returns hex-char offset or None)
let pos: Option<usize> = find_last_op_return(&script_hex);

Panics

Several functions panic instead of returning Result for programmer errors:

Function Panic condition
RunarContract::new() Constructor arg count does not match artifact ABI
build_deploy_transaction() Empty UTXO slice, or insufficient funds
SdkValue::as_int() Called on a non-Int variant
SdkValue::as_bool() Called on a non-Bool variant
SdkValue::as_bytes() Called on a non-Bytes variant

All other error conditions return Result<T, String>.


Design Decisions

  • No built-in network provider: Rust applications typically use specific async runtimes (tokio, async-std) and HTTP clients. Implement the Provider trait with your stack.
  • Built-in LocalSigner: LocalSigner::new() provides real secp256k1 signing via the k256 crate with manual BIP-143 sighash computation. For custom signing flows, use ExternalSigner with closure callbacks.
  • Synchronous API: All methods are synchronous (fn, not async fn). This makes the SDK usable with any async runtime without imposing Send/Sync constraints.
  • SdkValue enum: Unlike Go's interface{}, Rust uses a typed enum for state values, providing exhaustive matching and type safety.