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:
- The contract output — Locked by your compiled Bitcoin Script with constructor arguments embedded.
- 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:
- The SDK reads the
constructorSlotsarray from the artifact. - For each slot, it takes the corresponding constructor argument from the array you provided (matched by position).
- It serializes the argument to the expected byte length (e.g., a
Ripemd160is 20 bytes). - 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
| Provider | Use 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
| Signer | Use 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
| Option | Type | Default | Description |
|---|---|---|---|
satoshis | number | 1 | Amount of satoshis to lock in the contract output |
changeAddress | string | signer’s address | Address for the change output |
What Happens During deploy()
- Fetch funding UTXOs. The SDK calls
provider.getUtxos(signer.address)to find unspent outputs controlled by the signer. - Select UTXOs. The SDK’s
selectUtxos()function picks enough UTXOs to cover the contract amount plus the estimated fee. - 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.
- Sign the inputs. Each funding input is signed by the signer.
- Broadcast. The signed transaction hex is sent to the network via
provider.broadcast(). - Update contract state. The contract instance records the deployment
txidandoutputIndexso 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:
| Error | Cause | Resolution |
|---|---|---|
Insufficient funds | Signer’s UTXOs do not cover amount + fee | Fund the address or reduce satoshis |
Missing constructor argument | A required constructor arg was not provided | Check the artifact’s ABI and provide all params |
Broadcast failed: txn-mempool-conflict | A UTXO was already spent | Refresh UTXOs and retry |
Broadcast failed: dust | Output amount is below dust threshold (546 sat) | Increase satoshis to at least 546 |
Invalid script | Constructor argument has wrong format/length | Verify argument types match the ABI |
What’s Next
- Calling a Contract — How to spend the deployed contract UTXO
- Stateful Contracts — Deploying and interacting with stateful contracts
- Fee and Change Handling — Understanding fee estimation and UTXO selection