Docs · 03 / 8
Program format
The JSON language reference: storage, meta, read, write, init and expressions.
Top level
A program is one JSON object with these keys: v, type, storage, meta, read, write and an optional init.
storage
Maps a slot name to a type.
| Type | Meaning |
|---|---|
uint | Unsigned integer |
string | String |
boolean | Boolean |
address | Address |
map:<type> | Map from key to value of <type> |
map:map:<type> | Nested map. Maximum depth is 2 |
A missing value reads as the type's zero.
meta
Constants: name, symbol, price, fee receiver address, limits. The type comes from how the value is written:
| Written as | Type |
|---|---|
| A number or digit string | uint |
A bc1…, tb1… or bcrt1… string | address |
true / false | boolean |
| Anything else | string |
read methods
{ "args": ["name:type"], "return": "<expr>" }args is optional. Read methods run without a transaction, so $tx is not available in them.
write methods
{
"args": ["name:type"],
"require": [["<cond>", "<message>"]],
"set": [ /* steps */ ]
}requireconditions are checked in order. The first failure rejects the action with its message.setis an ordered list of steps. Each step sees earlier writes, so a self-transfer is safe.
A step is one of:
["$storage.slot", "<expr>"]
["$storage.map[key]", "<expr>"]
{ "if": "<cond>", "then": [ /* steps */ ], "else": [ /* steps */ ] }else is optional; chain with another if inside else. Conditions and assignments are type-checked in every branch when the program is compiled.
init
The same steps as set, run once at deploy. There is no transaction and no arguments; it can read $program, $meta and $storage.
Argument declarations
Declared as "name:type", where type is uint, address, string or boolean.
Expressions
| Kind | Operators |
|---|---|
| Arithmetic (integers) | + - * / %. / is floor division; division by zero rejects. |
| Comparison | < <= > >= == != |
| Logic | && || ! |
| Unary | - |
| Ternary | a ? b : c |
Variables
| Variable | Content |
|---|---|
$storage | Storage slots |
$args | Declared method arguments |
$meta | Constants |
$program | deployer, id |
$tx | The action transaction (write methods only) |
$tx
height,txid,inputs.length,outputs.lengthinputs[i].{txid, vout, outpoint, address}outputs[i].{index, outpoint, address, value}
- An index out of range rejects the action.
outputs[i].addressis an error for the OP_RETURN output.valueis in sats.inputs[i].addressis the signer's identity address (see Actions).
Limits
| Limit | Value |
|---|---|
| Methods | 32 at most |
| Storage slots | 32 |
| Assignments per method | 16 |
| Steps per method | 32 |
| Branch depth | 3 |
| require per method | 8 |
| Expression nodes in total | 256 |
| Messages | up to 128 chars |
| Strings | up to 128 chars |
| Values | up to 2^256 - 1 |
| Names | [A-Za-z_][A-Za-z0-9_]{0,31} |
Every value is bounded, so state can never blow up.
Determinism
The VM is pure. Executing a write method never mutates the input storage; it returns the list of writes to apply.
Example: a fungible token
A token with these methods:
mint: paysprice_satsto afee_receiverat output 2, with a per-call limit and a supply cap.transfer: debits$tx.inputs[0].address, credits$tx.outputs[2].address.burn,approve.- Reads:
balanceOf,totalSupply,name,symbol.
{
"v": 1,
"type": "token",
"storage": {
"balances": "map:uint",
"allowances": "map:map:uint",
"minted": "uint",
"burned": "uint"
},
"meta": {
"name": "Example",
"symbol": "EXM",
"decimals": 8,
"totalSupply": "2100000000000000",
"lim": "100000000",
"price_sats": 1000,
"fee_receiver": "bc1p<fee-receiver-taproot-address>"
},
"read": {
"balanceOf": {
"args": ["owner:address"],
"return": "$storage.balances[$args.owner]"
},
"totalSupply": { "return": "$storage.minted - $storage.burned" },
"name": { "return": "$meta.name" },
"symbol": { "return": "$meta.symbol" }
},
"write": {
"mint": {
"args": ["amt:uint"],
"require": [
["$args.amt > 0", "amount must be positive"],
["$args.amt <= $meta.lim", "above per-call limit"],
["$storage.minted + $args.amt <= $meta.totalSupply", "supply cap reached"],
["$tx.outputs[2].address == $meta.fee_receiver", "pay the fee receiver at output 2"],
["$tx.outputs[2].value >= $meta.price_sats", "payment below price"]
],
"set": [
["$storage.balances[$tx.inputs[0].address]",
"$storage.balances[$tx.inputs[0].address] + $args.amt"],
["$storage.minted", "$storage.minted + $args.amt"]
]
},
"transfer": {
"args": ["amt:uint"],
"require": [
["$storage.balances[$tx.inputs[0].address] >= $args.amt", "insufficient balance"]
],
"set": [
["$storage.balances[$tx.inputs[0].address]",
"$storage.balances[$tx.inputs[0].address] - $args.amt"],
["$storage.balances[$tx.outputs[2].address]",
"$storage.balances[$tx.outputs[2].address] + $args.amt"]
]
},
"burn": {
"args": ["amt:uint"],
"require": [
["$storage.balances[$tx.inputs[0].address] >= $args.amt", "insufficient balance"]
],
"set": [
["$storage.balances[$tx.inputs[0].address]",
"$storage.balances[$tx.inputs[0].address] - $args.amt"],
["$storage.burned", "$storage.burned + $args.amt"]
]
},
"approve": {
"args": ["spender:address", "amt:uint"],
"require": [],
"set": [
["$storage.allowances[$tx.inputs[0].address][$args.spender]", "$args.amt"]
]
}
}
}Annotations
storagedeclares balances as a map keyed by identity address, allowances as a nested map (depth 2), and two counters.meta.fee_receiveris typedaddressbecause it starts withbc1;price_satsis auint. Replace the placeholder with a real address.- In
mint, therequirelist checks the limit, the supply cap, and that output 2 pays at least the price to the fee receiver. Only then do thesetsteps credit the sender. - In
transfer, the debit runs before the credit, and each step sees the previous write, so sending to yourself is safe. - The receiver of a transfer is whatever address output 2 decodes to. See Receiver address types.
approveonly records an allowance; this example defines no method that consumes it.