Rúnar

Deploying a Contract

Deploying a Runar contract means broadcasting a transaction that locks funds into a UTXO governed by your compiled Bitcoin Script. This page walks through the deployment process step by step.

Deployment Conceptually

A deployment transaction is a standard BSV transaction with one special output: the locking script is your compiled contract. The transaction spends one or more funding UTXOs (regular P2PKH outputs that you control) and creates:

  1. The contract output — Locked by your compiled Bitcoin Script with constructor arguments embedded.
  2. A change output — Returns excess funds to your address.

After the transaction is broadcast and confirmed, the contract UTXO exists on-chain and can be spent by anyone who can satisfy its conditions.

Step 1: Compile the Contract

Start by compiling your contract to produce an artifact:

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

This produces artifacts/P2PKH.json. See Output Artifacts for a detailed explanation of the artifact format.

Step 2: Create a RunarContract Instance

Load the artifact and provide constructor arguments:

import { RunarContract } from 'runar-sdk';
import artifact from './artifacts/P2PKH.json';

const contract = new RunarContract(artifact, [
  '89abcdef01234567890abcdef01234567890abcd',
]);

How Constructor Arguments Work

The compiled script in the artifact contains OP_0 placeholders at the positions where constructor parameters belong. The artifact’s constructorSlots field tells the SDK exactly where each placeholder is and how many bytes it expects.

When you create a RunarContract instance:

  1. The SDK reads the constructorSlots array from the artifact.
  2. For each slot, it takes the corresponding constructor argument from the array you provided (matched by position).
  3. It serializes the argument to the expected byte length (e.g., a Ripemd160 is 20 bytes).
  4. It splices the serialized bytes into the script at the recorded byte offset.

The result is a fully resolved locking script with no placeholders.

Contracts Without Constructor Arguments

Some contracts have no constructor parameters (e.g., a stateful Counter that starts at zero). Pass an empty array:

const counter = new RunarContract(counterArtifact, []);

Step 3: Set Up Provider and Signer

The provider connects to the BSV network and the signer produces signatures for your funding inputs. These are passed to deploy() and call() as arguments.

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

const provider = new WhatsOnChainProvider('testnet');
const signer = new LocalSigner('cN3L4FfPtE7WjknM...'); // WIF private key

The connect() method stores the provider and signer on the contract instance so you do not need to pass them to every subsequent method call.

Provider Options

ProviderUse Case
WhatsOnChainProvider('mainnet')Production deployment
WhatsOnChainProvider('testnet')Testing with real transactions
new RPCProvider(url, user, pass, options?)Local regtest node
MockProvider()Unit tests (no real broadcast)

Signer Options

SignerUse Case
LocalSigner(wif)Server-side, testing
new ExternalSigner(pubKeyHex, addressStr, signFn)HSM, hardware wallet
new WalletSigner({ protocolID, keyID, wallet? })Browser wallet extension

Step 4: Deploy

Call deploy() to build, sign, and broadcast the deployment transaction:

const deployResult = await contract.deploy(provider, signer, { satoshis: 10000 });

console.log(deployResult);
// {
//   txid: 'a1b2c3d4e5f6...',
//   tx: <Transaction>,
// }

Deploy Options

OptionTypeDefaultDescription
satoshisnumber1Amount of satoshis to lock in the contract output
changeAddressstringsigner’s addressAddress for the change output

What Happens During deploy()

  1. Fetch funding UTXOs. The SDK calls provider.getUtxos(signer.address) to find unspent outputs controlled by the signer.
  2. Select UTXOs. The SDK’s selectUtxos() function picks enough UTXOs to cover the contract amount plus the estimated fee.
  3. Build the transaction. A new transaction is constructed with:
    • Inputs: The selected funding UTXOs.
    • Output 0: The contract locking script with the specified satoshis.
    • Output 1: A P2PKH change output returning excess funds to the change address.
  4. Sign the inputs. Each funding input is signed by the signer.
  5. Broadcast. The signed transaction hex is sent to the network via provider.broadcast().
  6. Update contract state. The contract instance records the deployment txid and outputIndex so it knows where the UTXO lives on-chain.

Verifying Deployment

After deployment, verify the transaction exists on-chain:

const tx = await provider.getTransaction(deployResult.txid);
console.log(tx.confirmations); // 0 initially, then 1+ after a block

For testnet deployments, you can view the transaction on a block explorer:

https://test.whatsonchain.com/tx/a1b2c3d4e5f6...

Deploying Stateful Contracts

Stateful contracts (those extending StatefulSmartContract) are deployed the same way, but the locking script includes serialized initial state at the end.

import counterArtifact from './artifacts/Counter.json';

const counter = new RunarContract(counterArtifact, [0n]);

const result = await counter.deploy(provider, signer, { satoshis: 10000 });

The setState() call sets the initial state. During deployment, the SDK serializes this state and appends it to the locking script. The resulting script looks like:

[contract logic] [OP_RETURN] [serialized state]

The state is placed after OP_RETURN so it does not affect script execution — it is just data carried in the output.

Reconnecting After Deployment

If your application restarts after deploying a contract, you need to reconnect to the existing UTXO rather than deploying again. Use RunarContract.fromTxId():

const contract = await RunarContract.fromTxId(
  artifact,
  'a1b2c3d4e5f6...', // the deployment txid
  0,                   // output index
  provider,
);

This fetches the transaction from the network, reads the locking script from the specified output, and reconstructs the contract instance. For stateful contracts, it also deserializes the current state from the script.

Deployment with Pre-Built Transactions

For advanced use cases where you need full control over the transaction, use the buildDeployTransaction() utility:

import { buildDeployTransaction, selectUtxos } from 'runar-sdk';

const utxos = await provider.getUtxos(signer.address);
const lockingScript = contract.getLockingScript();
const selected = selectUtxos(utxos, 10000 + 300, lockingScript.length / 2); // amount + estimated fee

const tx = buildDeployTransaction(
  lockingScript,
  selected,
  10000,
  signer.address,
  signer.changeScript,
);

// Inspect the transaction before signing
console.log(tx.inputs.length);  // number of funding inputs
console.log(tx.outputs.length); // 2 (contract + change)

// Sign each input and broadcast manually
for (let i = 0; i < tx.inputs.length; i++) {
  const sig = await signer.sign(tx.toHex(), i, selected[i].script, selected[i].satoshis);
  tx.inputs[i].setScript(sig);
}
const txid = await provider.broadcast(tx.toHex());

This gives you the ability to inspect, modify, or audit the transaction before it is broadcast.

Handling Deployment Errors

Common deployment errors and how to resolve them:

ErrorCauseResolution
Insufficient fundsSigner’s UTXOs do not cover amount + feeFund the address or reduce satoshis
Missing constructor argumentA required constructor arg was not providedCheck the artifact’s ABI and provide all params
Broadcast failed: txn-mempool-conflictA UTXO was already spentRefresh UTXOs and retry
Broadcast failed: dustOutput amount is below dust threshold (546 sat)Increase satoshis to at least 546
Invalid scriptConstructor argument has wrong format/lengthVerify argument types match the ABI

What’s Next