Calling a Contract
Calling a Runar contract means constructing a transaction that spends a contract UTXO by satisfying its locking script conditions. The SDK abstracts this into a familiar method-call interface.
How Contract Calls Work on BSV
On BSV, “calling” a contract means spending its UTXO. The contract’s locking script defines the conditions that must be satisfied, and the caller provides an unlocking script that satisfies those conditions.
A contract call transaction has:
- Input: The contract UTXO being spent, with an unlocking script that pushes the method arguments.
- Outputs: Zero or more outputs depending on the contract. A stateless contract like P2PKH sends funds to a new address. A stateful contract creates a continuation UTXO with updated state.
The SDK builds this transaction automatically when you call contract.call().
Basic Call: Stateless Contract
For a stateless contract like P2PKH, calling unlock spends the UTXO and sends the funds to a specified address:
import { RunarContract, WhatsOnChainProvider, LocalSigner } from 'runar-sdk';
import artifact from './artifacts/P2PKH.json';
// Reconnect to a deployed contract
const provider = new WhatsOnChainProvider('testnet');
const signer = new LocalSigner('cN3L4FfPtE7WjknM...');
const contract = await RunarContract.fromTxId(
artifact,
'a1b2c3d4e5f6...', // deployment txid
0,
provider,
);
// Call the unlock method
// Pass null for Sig parameters — the SDK auto-signs with the provided signer
const result = await contract.call(
'unlock',
[null, signer.publicKey],
provider,
signer,
);
console.log(result);
// {
// txid: 'f6e5d4c3b2a1...',
// tx: <Transaction>,
// }
The call() Method Signature
contract.call(
methodName: string,
args: unknown[],
provider: Provider,
signer: Signer,
options?: CallOptions,
): Promise<CallResult>
Parameters:
| Parameter | Type | Description |
|---|---|---|
methodName | string | The name of the public method to call, as defined in the contract |
args | unknown[] | Method arguments as an array. Pass null for Sig parameters to auto-sign with the provided signer. |
provider | Provider | Network provider for fetching UTXOs and broadcasting |
signer | Signer | Signs transaction inputs |
options | CallOptions | Optional configuration for the transaction |
CallOptions:
| Option | Type | Default | Description |
|---|---|---|---|
satoshis | number | auto | Satoshis for the continuation output (stateful contracts) |
changeAddress | string | signer’s address | Address for change output |
newState | Record<string, unknown> | auto-computed | Override next state (stateful contracts). If omitted, the SDK computes the new state by interpreting the contract’s ANF IR. |
CallResult:
| Field | Type | Description |
|---|---|---|
txid | string | Transaction ID of the spending transaction |
tx | Transaction | The full transaction object |
Passing Arguments
Method arguments are passed as an array. The SDK validates argument types against the contract’s ABI.
Signature Arguments
For parameters of type Sig, pass null and the SDK auto-signs with the connected signer:
const result = await contract.call('unlock', [
null, // Sig param — auto-signed by the provided signer
signer.publicKey, // Hex-encoded public key
], provider, signer);
This is the recommended approach because the sighash depends on the full transaction, which is not known until the transaction is built. The SDK builds the transaction, computes the signature for each input, and inserts it automatically.
Preimage Arguments
For stateful contracts that use SigHashPreimage, the SDK computes the preimage automatically:
const result = await contract.call('increment', [], provider, signer);
The preimage is auto-injected by the SDK. You only pass the non-preimage arguments. The SDK:
- Builds the transaction with all outputs.
- Computes the sighash preimage for the contract input.
- Inserts the preimage into the unlocking script.
Other Argument Types
| Type | How to Pass | Example |
|---|---|---|
PubKey | Hex string | "02a1b2c3..." |
Ripemd160 | Hex string (20 bytes) | "89abcdef..." |
Sha256 | Hex string (32 bytes) | "2cf24dba..." |
ByteString | Hex string | "deadbeef" |
bigint | BigInt or number | 42n or 42 |
boolean | Boolean | true |
Multi-Method Contracts
Contracts with multiple public methods use a dispatch table in the compiled script. The SDK handles the dispatch automatically — you just call the method by name.
// A contract with two methods: transfer and burn
const result = await contract.call('transfer', [
null, // sig (auto-sign)
signer.publicKey,
'1234abcd...', // newOwnerPubKeyHash
], provider, signer);
// Or call the other method
const burnResult = await contract.call('burn', [
null, // sig (auto-sign)
signer.publicKey,
], provider, signer);
Under the hood, the SDK pushes the method index onto the stack before the method arguments. The dispatch table in the locking script reads this index and branches to the correct method body.
The method index is determined by the order of methods in the artifact’s abi.methods array. The SDK looks up the method name, finds its index, and pushes it as part of the unlocking script.
Building Unlocking Scripts Manually
For advanced use cases, you can build the unlocking script without broadcasting:
const unlockingScript = contract.buildUnlockingScript('unlock', [
rawSignatureHex,
publicKeyHex,
]);
console.log(unlockingScript);
// Hex-encoded unlocking script: "<sig> <pubKey>"
This is useful when you are constructing transactions manually or integrating with external transaction builders.
Transaction Construction with buildCallTransaction
For full control over the spending transaction:
import { buildCallTransaction } from 'runar-sdk';
const unlockingScript = contract.buildUnlockingScript('unlock', [
rawSignatureHex,
publicKeyHex,
]);
const currentUtxo = {
txid: contract.txid,
outputIndex: contract.outputIndex,
satoshis: contract.satoshis,
script: contract.getLockingScript(),
};
const tx = buildCallTransaction(
currentUtxo,
unlockingScript,
null, // newLockingScript (none for stateless)
null, // newSatoshis
signer.address, // changeAddress
signer.changeScript,
);
// Inspect before signing
console.log('Inputs:', tx.inputs.length);
console.log('Outputs:', tx.outputs.length);
// Sign each input and broadcast
for (let i = 0; i < tx.inputs.length; i++) {
const sig = await signer.sign(tx.toHex(), i, currentUtxo.script, currentUtxo.satoshis);
tx.inputs[i].setScript(sig);
}
const txid = await provider.broadcast(tx.toHex());
Calling Stateful Contracts
Stateful contracts require special handling because the spending transaction must create a continuation output. See Stateful Contracts for the full guide. Here is a brief example:
// Increment a Counter contract
const result = await contract.call('increment', [], provider, signer);
// The SDK auto-computes the new state by interpreting the contract's ANF IR
console.log(contract.state);
// { count: 1n }
For stateful contracts, the SDK:
- Interprets the contract’s ANF IR to compute the new state.
- Builds the continuation output with the updated state serialized into the locking script.
- Computes the sighash preimage that covers these outputs.
- Inserts the preimage into the unlocking script.
- After broadcast, updates the contract instance to point to the new UTXO.
Error Handling
Script Execution Failure
If the unlocking script does not satisfy the locking script conditions, the BSV node rejects the transaction:
try {
const result = await contract.call('unlock', [
null,
wrongSigner.publicKey,
], provider, wrongSigner);
} catch (error) {
console.log(error.message);
// "Script execution failed: OP_EQUALVERIFY failed at position 5"
}
Argument Validation
The SDK validates arguments against the ABI before building the transaction:
try {
const result = await contract.call('unlock', [
null,
// Missing pubKey argument
], provider, signer);
} catch (error) {
console.log(error.message);
// "Missing required argument 'pubKey' for method 'unlock'"
}
Method Not Found
try {
const result = await contract.call('nonexistent', [], provider, signer);
} catch (error) {
console.log(error.message);
// "Method 'nonexistent' not found in contract 'P2PKH'. Available methods: unlock"
}
Reading Contract State After a Call
For stateless contracts, a successful call simply spends the UTXO. The contract is “consumed” and there is nothing further to read.
For stateful contracts, after a successful call, the contract instance is updated to point to the new continuation UTXO:
// Before call
console.log(contract.state); // { count: 0n }
await contract.call('increment', [], provider, signer);
// After call -- state is auto-computed from ANF IR
console.log(contract.state); // { count: 1n }
What’s Next
- Stateful Contracts — In-depth guide to stateful contract interactions
- Token Contracts — Calling token transfer and merge methods
- Fee and Change Handling — Understanding fees in call transactions