Skip to main content

Commit Methods

Modality contracts are append-only logs of commits. Each commit contains one or more actions, and each action has a method that determines what it does.

GENESIS​

Opens a contract. modal contract create writes it as the first commit. Validators refuse GENESIS in any later commit: it would change state without taking a step in the model.

POST​

The most common method — writes data to a path in this contract's state.

{
"method": "post",
"path": "/users/alice.id",
"value": "12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz..."
}

After the commit is accepted, formulas on this contract can name that path (signed_by(/users/alice.id), text_eq, state_exists, …).

Path Types​

Paths must end with a known extension:

ExtensionTypeExample Value
.boolBooleantrue or false
.textText string"Hello world"
.numNumber100 or 0.5
.dateDate"2024-01-15"
.datetimeDate and time"2024-01-15T10:30:00Z"
.jsonJSON object{"key": "value"}
.mdMarkdown"# Title\n\nContent..."
.idModality spelling: lowercase, mirrored, type at the end"imqi74td...aiajaazfab"
.wasmWebAssemblyBase64-encoded WASM
.modalityRules/formulasModality syntax

MODEL​

Sets or replaces the contract's state machine model. Unlike rules, models can be replaced.

{
"method": "model",
"value": "model escrow { initial idle; idle -> funded [DEPOSIT]; funded -> released [RELEASE] }"
}

CLI Usage​

modal contract commit --method model \
--value 'model members_only { initial active; active -> active [] }' \
--sign alice

Note: Models define structure but don't enforce security on their own. Use RULE methods to add protection that persists even if the model is replaced.

RULE​

Adds a temporal logic constraint to the contract. Rules are accumulated over time and all must be satisfied.

{
"method": "rule",
"path": "/rules/auth.modality",
"value": "always([<+signed_by(/users/alice.id)>] true)"
}

Rules must have paths ending in .modality.

REPOST​

Copies a snapshot of another contract's value into this contract so later rules can name it like any other path. It is not a live pointer: later updates on the source do not change this contract until you REPOST again.

{
"method": "repost",
"path": "/reposts/abc123def456/announcements/latest.text",
"value": "Hello from the other contract!",
"source_contract": "abc123def456",
"source_path": "/announcements/latest.text",
"source_commit": "a1b2c3..."
}
  • path — dest path in this contract (a normal /... path). Default dest is /reposts/<source_id><source_path>. Choose a path like /parties/alice.id when the imported value should be that identity.
  • value — bytes copied from the source.
  • source_contract, source_path, source_commit — provenance. The hub checks that the source contract at source_commit has value at source_path.

After accept, dest is ordinary accepted state. Example: REPOST Alice's key to /parties/alice.id, then signed_by(/parties/alice.id) works. Method label is +REPOST (not +POST). modifies(dest) matches. post_to_path stays POST-only. Models that only allow +POST must also allow +REPOST (or an unlabeled write loop) before import commits will pass.

Working tree​

Default dest files live under reposts/<source_id>/.... Custom dests live under state/ like POST. modal commit --all emits method: "repost" for paths staged by modal repost.

CLI Usage​

# Default dest /reposts/<source_id>/announcements/latest.text
modal repost abc123def456 /announcements/latest.text

# Custom dest so formulas can say signed_by(/parties/alice.id)
modal repost abc123def456 /parties/alice.id /parties/alice.id

# Source is a local contract directory (no hub)
modal repost abc123def456 /notes/hello.text --from-dir ../source-contract

modal commit --all

CREATE​

Creates a new asset in the contract. The asset_id may not contain :, which separates a creator from an asset id in a held asset's name.

{
"method": "create",
"value": {
"asset_id": "token1",
"quantity": 2100000000000000,
"divisibility": 1,
"decimals": 8
}
}

quantity and every amount are in the asset's smallest unit. Every amount sent must be a multiple of divisibility. decimals is optional and only for display: with 8, a wallet shows 150000000 as 1.5.

SEND​

Sends assets to another contract.

{
"method": "send",
"value": {
"asset_id": "token1",
"to_contract": "target_contract_id",
"amount": 1000
}
}

A contract sends an asset it created, or one it received. Name a received asset by its creator in asset_contract; omit it for the contract's own asset. Naming the contract itself is refused.

An optional memo carries JSON the receiver reads, for example what the sender wants done with the asset. It is recorded with the SEND.

{
"method": "send",
"value": {
"asset_contract": "creator_contract_id",
"asset_id": "token1",
"to_contract": "pool_contract_id",
"amount": 100,
"memo": {"op": "swap", "min_out": 300}
}
}

RECV​

Receives assets from a SEND in another contract. The SEND commit must already be sequenced. When the network requires validator certs, dest apply also needs a prefix-cert supermajority through that SEND commit.

{
"method": "recv",
"value": {
"send_commit_id": "abc123...",
"send_index": 1
}
}

send_index picks which SEND of that commit, counting from 0 (the default). SENDs a program emitted count too, in the order the invoke emitted them. Each SEND is received once.

A RECV may also state what it receives: from_contract, asset_contract and asset_id, amount, and memo. Apply refuses a RECV whose statement differs from the SEND, so rules can read the statement as the fact. A statement of asset_id without asset_contract claims the receiver's own asset.

{
"method": "recv",
"value": {
"send_commit_id": "abc123...",
"from_contract": "trader_contract_id",
"asset_contract": "creator_contract_id",
"asset_id": "token1",
"amount": 100,
"memo": {"op": "swap", "min_out": 300}
}
}

A commit applies whole or not at all: if any action fails, none of its writes are kept.

INVOKE​

Executes a WASM program stored in the contract.

{
"method": "invoke",
"path": "/__programs__/calculator.wasm",
"value": {
"args": {
"operation": "add",
"a": 5,
"b": 3
}
}
}