Unlocking Scripts¶
To spend a UTXO locked behind a compiled contract, you call one of the contract's entrypoints with arguments, and the compiler emits the bytes that satisfy the lock.
# Genesis with min spend threshold of 100
contract = silverscript.compile(SOURCE, [100])
# Spend 150
sig_script = contract.build_sig_script("check", [150])
build_sig_script
takes the entrypoint name and a list of args, and returns the
unlocking script bytes. Those bytes go into a transaction input's
signature_script. For a covenant entrypoint, use
build_sig_script_for_covenant_decl
instead — see Covenants.
Calling an entrypoint¶
Only functions declared with entry are callable from a spend. Pass the
entrypoint name and a list of positional arguments, in the order the
entrypoint declares them; the compiler emits the right unlocking script —
including the four-byte dispatch tag that selects the entrypoint. You don't
construct or read these bytes yourself —
you put them on the input (see
Spending a locked UTXO).
# check(int amount) — args are a positional list matching the parameters.
sig_script = contract.build_sig_script("check", [150])
An entrypoint that takes no arguments is called with no args (or an empty list):
Argument types¶
args are native Python values, mapped to the entrypoint's declared
SilverScript types (the type_names you can read off the
ABI):
| SilverScript type | Python value |
|---|---|
int |
int (must fit in a signed 64-bit integer) |
bool |
bool (a real bool — not 0/1) |
temporal |
int (a time value in milliseconds) |
byte |
int in 0..=255 (a one-byte bytes also works) |
byte[] |
bytes / bytearray (not a list) |
byte[N] |
bytes / bytearray of length N |
string |
str (encoded as UTF-8) |
pubkey |
bytes (a 32-byte x-only public key) |
sig |
bytes (a 65-byte signature — 64 plus the sighash type byte) |
datasig |
bytes (a 64-byte signature over a message, for checkMsgSig) |
T[] |
list or tuple of T |
struct / State |
dict |
A few rules worth knowing:
boolis distinct fromint.Trueis not1here — pass the type the entrypoint declares.- Which Python value means what is decided by the declared type, not the
value.
1is abytefor abyteparameter and anintfor anintone; a smallintis never silently treated as abyte. listandtupleare interchangeable for array arguments.- Out-of-range and mistyped values raise
SilverScriptError, not a PythonOverflowErrororTypeError. Anintoutside the signed 64-bit range, abyte[4]given five bytes, or a deeply nested argument all fail cleanly instead of producing a bad script.
Spending a locked UTXO¶
The unlocking script is one piece of a P2SH spend. The input's
signature_script must reveal the redeem script and satisfy it, so
you concatenate the contract call with the pushed redeem script:
from kaspa import ScriptBuilder
contract = silverscript.compile(SOURCE, [100])
call = contract.build_sig_script("check", [150])
# Push the redeem script so it rides along in the same signature_script.
redeem = bytes.fromhex(
ScriptBuilder().add_data(contract.bytecode).to_string()
)
signature_script = call + redeem
Put signature_script on the
TransactionInput that
spends the locked UTXO. The full P2SH mechanics — wrapping the lock,
building the address, the spend side — are in
Transactions → Scripts.
Compile once, build many¶
compile does all
the expensive work up front: it parses the source, compiles the contract,
and builds its portable ABI artifact
once, then keeps them on the
CompiledContract.
Each later call to
build_sig_script
works from that stored artifact — it converts your arguments to the
declared parameter types, pushes them, and appends the entrypoint's
four-byte dispatch tag. Nothing is recompiled. Building many unlocking
scripts from one contract is cheap, and it's deterministic: the same call
always yields the same bytes.
Because the artifact is all build_sig_script reads, you don't even need
the contract: load_artifact
restores one from JSON and builds the same bytes with no source and no
compiler — see
Loading an artifact back.
Next: stateful contracts that carry state from one UTXO to the next — Covenants.