Skip to main content

Stateful Contracts

The StatefulContract class manages the full lifecycle of stateful contracts: deployment, state reading, function calls, upgrades, and destruction.

Deploying

import { StatefulContract, ElectrumNetworkProvider, SignatureTemplate } from 'solidcash';
import artifact from './Counter.artifact.js';

const provider = new ElectrumNetworkProvider('chipnet');
const signer = new SignatureTemplate(adminPrivateKey);

const { contract, tx, categoryId } = await StatefulContract.deploy({
artifact,
constructorArgs: [adminPub],
provider,
fundingUtxo: myUtxo,
funder: signer.unlockP2PKH(),
changeAddress: adminAddress,
initialState: { counter: 0n },
masterAmount: 1200n,
dataNftAmount: 1200n,
fee: 1000n,
addressType: 'p2sh32',
});

console.log('Category:', categoryId);
console.log('TX:', tx.txid);
caution

The fundingUtxo must be at vout 0 (the first output of its parent transaction) because the funding transaction's ID becomes the CashToken category ID for the contract.

Deploy Options

OptionTypeDefaultDescription
artifactArtifactrequiredCompiled contract artifact
constructorArgsany[]requiredConstructor parameter values
providerNetworkProviderrequiredNetwork connection
fundingUtxoUtxorequiredFunding UTXO (must be vout 0)
funderUnlockerrequiredUnlocker for the funding UTXO
changeAddressstringrequiredAddress for change output
initialStateRecord<string, any>{}Initial state variable values
masterAmountbigint1200nSatoshis per master NFT
dataNftAmountbigint1200nSatoshis per data NFT
feebigint1000nTransaction fee
addressTypeAddressType'p2sh32'Must be 'p2sh32'
threadCountnumber1Number of parallel threads
initialBalancebigint0nInitial satoshi container balance

Connecting to an Existing Contract

// From a Contract instance + category ID
const sc = StatefulContract.withCategory(innerContract, categoryId, {
externals: { ICounter: otherContract },
});

// From artifact directly
const sc = StatefulContract.from(
artifact,
constructorArgs,
{ provider, addressType: 'p2sh32' },
categoryId,
);

External Contract Binding

When your contract reads external contracts, bind them at connect time:

const sc = StatefulContract.withCategory(innerA, catA, {
externals: { ICounter: counterContract },
});

// Or with explicit specification
const sc = StatefulContract.withCategory(innerA, catA, {
externalContracts: {
ICounter: {
artifact: counterArtifact,
categoryId: catB,
constructorArgs: [adminPub],
},
},
});

Calling Functions

Three equivalent patterns for calling stateful functions:

// Pattern 1: Direct call (recommended)
const result = await sc.call.increment(alicePub, aliceSig).send();

// Pattern 2: Short-form (if no name collision with class methods)
const result = await sc.increment(alicePub, aliceSig).send();

// Pattern 3: Function builder (manual control)
const builder = await sc.functions.increment(alicePub, aliceSig);
const result = await builder.send();

All patterns return a TransactionDetails with txid and hex fields.

Chained Builder Methods

const result = await sc.call.transfer(to, amount)
.withEntryKey(senderPkh) // specify mapping key
.setState('counter', 5n) // override state
.transfer(recipientLbc, 1000n) // manual satoshi transfer
.emitEvent('Transfer', from, to, amount) // emit event
.send();

Reading State

// All state variables
const state = await sc.getState();
console.log(state.counter); // 5n

// Individual variable
const counter = await sc.state.counter();

// Short-form
const counter = await sc.counter();

Working with Mappings

// Set entry by key
await sc.call.setBalance(bobPkh, 1000n)
.withEntryKey(bobPkh)
.send();

// Delete entry
await sc.call.removeAccount(bobPkh)
.deleteEntry(0)
.send();

Token Container Operations

// Fungible token transfer
await sc.call.withdraw(tokenCategory, recipient, amount)
.tokenContainerTransfer(tokenCategory, recipientLbc, amount)
.send();

// NFT transfer
await sc.call.withdrawNft(nftCategory, recipient, commitment)
.tokenContainerTransferNft(nftCategory, recipientLbc, commitment, 'none')
.send();

Satoshi Transfers

// Usually auto-derived from VM trace
await sc.call.sendFunds(sig, recipient, 5000n).send();

// Manual override
await sc.call.sendFunds(sig, recipient, 5000n)
.transfer(recipientLockingBytecode, 5000n)
.send();

Upgrading

Deploy a new version while preserving the category ID and state:

import newArtifact from './CounterV2.artifact.js';

const { contract: upgraded, tx } = await StatefulContract.upgrade({
provider,
oldContract: sc,
upgradeFunctionName: 'migrate',
upgradeFunctionArgs: [adminSig],
newArtifact,
newConstructorArgs: [adminPub],
});

The new artifact can have additional state variables appended. Existing state values are preserved; new fields default to zero.

Destroying

Permanently destroy a contract, burning all NFTs and reclaiming satoshis:

const { tx } = await StatefulContract.destroy({
provider,
contract: sc,
destroyFunctionName: 'destroy',
destroyFunctionArgs: [adminSig],
recipientAddress: adminAddress,
maxFee: 10000n,
});
caution

Destruction is irreversible. All state, entries, events, and tokens are permanently lost.

Deploying Child Contracts

Factory pattern — deploy a child contract from a parent:

const { contract: child, tx } = await StatefulContract.deployFrom({
provider,
parentContract: factory,
deployFunctionName: 'createChild',
deployFunctionArgs: [adminSig],
childArtifact,
childConstructorArgs: [],
fundingUtxo,
funder: signer.unlockP2PKH(),
changeAddress: adminAddress,
});

Debugging

// Debug a function call
const debugResults = await sc.call.myFunc(args).debug();

// VM resource usage
const builder = await sc.call.myFunc(args).build();
const usage = builder.getVmResourceUsage();

Auto-Derivation

The SDK automatically derives the full transaction from your function call:

  1. Runs the contract bytecode in a local VM
  2. Inspects which state variables are read/written
  3. Discovers required entry NFTs, event outputs, and transfers
  4. Constructs the complete transaction with all covenant outputs
  5. Iterates up to 20 times until the state converges
tip

Auto-derivation handles most scenarios automatically. Use manual overrides (.setState(), .transfer(), .withEntryKey()) only when auto-derivation cannot determine the correct values — for example, with expression-based mapping keys.