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:
[]
= { = "runar-lang", = "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 *;
// 1. Load the artifact (compiled contract JSON)
let artifact: RunarArtifact = from_str?;
// 2. Create the contract with constructor arguments
// Panics if the number of args does not match the artifact's constructor params.
let mut contract = new;
// 3. Set up provider and signer
// Use ExternalSigner to wrap a real signing library
let signer = new;
let mut provider = testnet;
// 4. Deploy
let = contract.deploy?;
// 5. Call a public method
let = contract.call?;
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 *;
let mut contract = new;
// Store provider and signer on the contract
contract.connect;
// Deploy without passing provider/signer
let = contract.deploy_connected?;
// Call without passing provider/signer
let = contract.call_connected?;
If connect() has not been called, deploy_connected() and call_connected() return an error.
Stateful Contract Example
use HashMap;
// Create with initial state
let mut contract = new;
// Deploy
let = contract.deploy?;
// Read current state
println!; // Some(Int(0))
// Call increment with updated state
let mut new_state = new;
new_state.insert;
let = contract.call?;
println!; // Some(Int(1))
// Update state directly (without a call)
let mut override_state = new;
override_state.insert;
contract.set_state;
Reconnecting to a Deployed Contract
// Reconnect to an existing on-chain contract by txid
let contract = from_txid?;
println!;
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?;
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 = new;
// Or use the testnet shorthand
let mut provider = testnet;
// Pre-register UTXOs
provider.add_utxo;
// Pre-register transactions
provider.add_transaction;
// Pre-register contract UTXOs for stateful lookup
provider.add_contract_utxo;
// Inspect broadcasts after deploying/calling
let broadcasted: & = provider.get_broadcasted_txs;
// Override the fee rate (default 1 sat/byte)
provider.set_fee_rate;
Custom Provider
Implement the Provider trait for other network APIs:
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 = 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?;
// Fields are public for customization in tests
let custom = MockSigner ;
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 = new?;
// From WIF
let signer = new?;
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?; // 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 = new;
Custom Signer
Implement the Signer trait:
Stateful Contract Support
State Chaining
Stateful contracts maintain state across transactions using the OP_PUSH_TX pattern. The SDK manages this automatically:
- Deploy: The initial state is serialized and appended after an OP_RETURN separator in the locking script.
- 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.
- 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 truebytes/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:
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 *;
use *;
use *;
// Select UTXOs (largest-first strategy)
let selected = select_utxos;
// Estimate the fee for a deploy transaction
let fee = estimate_deploy_fee;
// Build an unsigned deploy transaction
// Panics if utxos is empty or if total funds are insufficient.
let = build_deploy_transaction;
// Build a method call transaction
let = build_call_transaction;
// State serialization
let state_hex = serialize_state;
let state: = deserialize_state;
let state: = extract_state_from_script;
// Opcode-aware OP_RETURN finder (returns hex-char offset or None)
let pos: = find_last_op_return;
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
Providertrait with your stack. - Built-in LocalSigner:
LocalSigner::new()provides real secp256k1 signing via thek256crate with manual BIP-143 sighash computation. For custom signing flows, useExternalSignerwith closure callbacks. - Synchronous API: All methods are synchronous (
fn, notasync fn). This makes the SDK usable with any async runtime without imposingSend/Syncconstraints. SdkValueenum: Unlike Go'sinterface{}, Rust uses a typed enum for state values, providing exhaustive matching and type safety.