balances
balances is a simple, non-negative account ledger. A configured minter
creates units, and each member can spend only from its own account. Every
balance can be proved against the state root.
These are application credits, not Cardano ada or native assets. The machine creates no Cardano transactions, holds no custody, and charges no fees.
At a glance
Section titled “At a glance”| Machine id | balances |
| Maturity | stable |
| Commands | [0, account, amount] MINT; [1, account, amount] TRANSFER; amount is a positive integer |
| Setting | machines.balances.minter: a member public key as 64 hex characters, or empty |
| State | b/<account> → the amount as BigInteger.toByteArray(); a zero balance deletes the key |
| Proof subject | account-balance-v1: coordinate account; claims exact, minimum, maximum |
| Result codes | BALANCE_NOT_MINTER, BALANCE_INSUFFICIENT; BALANCE_EVENT_RANGE in composites |
| Events | balances.minted.v1, balances.transferred.v1, in composites only |
How it works
Section titled “How it works”What you're seeing: a minter, one member, and an application account called alice. Try a “What if” to meet each rule that turns a final command into a no-op.
- Mint to B Member M is the configured minter. It mints 200 to B’s account, which is named by B’s member public key in lowercase hex. The amount is stored as
BigInteger.toByteArray(), so 200 is00c8: a sign byte comes first.- passes: No minter is configured, or the sender is the minter
balances state Key Amount Stored bytes b/<B key hex>200 00c8 - Transfer to alice Member B transfers 50 to
alice. A transfer always debits the sender’s own account; the command names only the recipient.aliceis not a member key, so nobody can ever spend from it: it is receive-only.- passes: The sender’s own account holds at least the amount
balances state Key Amount Stored bytes b/<B key hex>150 0096b/alice50 32 - Drain to zero Member B transfers its remaining 150 to
alice. B’s balance reaches zero, so its key is deleted: an absent account means a zero balance.- passes: The sender’s own account holds at least the amount
balances state Key Amount Stored bytes b/alice200 00c8
What if: B tries to mint
With a minter configured, only that member’s MINT commands apply.
- Mint to B Member M is the configured minter. It mints 200 to B’s account, which is named by B’s member public key in lowercase hex. The amount is stored as
BigInteger.toByteArray(), so 200 is00c8: a sign byte comes first.- passes: No minter is configured, or the sender is the minter
balances state Key Amount Stored bytes b/<B key hex>200 00c8 - B mints Member B mints 1000 to itself. B is not the configured minter, so the decision is
BALANCE_NOT_MINTERand nothing is written.- fails: No minter is configured, or the sender is the minter
BALANCE_NOT_MINTER
balances state (unchanged) Key Amount Stored bytes b/<B key hex>200 00c8 - fails: No minter is configured, or the sender is the minter
What if: B overspends
Balances never go negative.
- Mint to B Member M is the configured minter. It mints 200 to B’s account, which is named by B’s member public key in lowercase hex. The amount is stored as
BigInteger.toByteArray(), so 200 is00c8: a sign byte comes first.- passes: No minter is configured, or the sender is the minter
balances state Key Amount Stored bytes b/<B key hex>200 00c8 - B sends 500 Member B transfers 500 to
alicebut holds 200. The decision isBALANCE_INSUFFICIENT; neither account changes.- fails: The sender’s own account holds at least the amount
BALANCE_INSUFFICIENT
balances state (unchanged) Key Amount Stored bytes b/<B key hex>200 00c8 - fails: The sender’s own account holds at least the amount
What if: Spend alice’s units
A transfer has no “from” field. Units in a non-member account cannot move again.
- Mint to B Member M is the configured minter. It mints 200 to B’s account, which is named by B’s member public key in lowercase hex. The amount is stored as
BigInteger.toByteArray(), so 200 is00c8: a sign byte comes first.- passes: No minter is configured, or the sender is the minter
balances state Key Amount Stored bytes b/<B key hex>200 00c8 - Transfer to alice Member B transfers 50 to
alice. A transfer always debits the sender’s own account; the command names only the recipient.aliceis not a member key, so nobody can ever spend from it: it is receive-only.- passes: The sender’s own account holds at least the amount
balances state Key Amount Stored bytes b/<B key hex>150 0096b/alice50 32 - Drain to zero Member B transfers its remaining 150 to
alice. B’s balance reaches zero, so its key is deleted: an absent account means a zero balance.- passes: The sender’s own account holds at least the amount
balances state Key Amount Stored bytes b/alice200 00c8 - B tries to pay for alice Member B transfers 10 to
carol, hoping to use alice’s units. The machine debits B’s own account, which is empty, so the decision isBALANCE_INSUFFICIENT. No member can sign asalice.- fails: The sender’s own account holds at least the amount
BALANCE_INSUFFICIENT
balances state (unchanged) Key Amount Stored bytes b/alice200 00c8 - fails: The sender’s own account holds at least the amount
What if: Mint to an uppercase key
Account names are compared as exact text. A sender’s account is always lowercase hex.
- Mint to uppercase hex Member M mints 100 to B’s key written in uppercase hex. That is a different account from B’s own, lowercase account.
- passes: No minter is configured, or the sender is the minter
balances state Key Amount Stored bytes b/<B KEY HEX>100 64 - B tries to spend Member B transfers 10 to
alice. The machine debitsb/<B key hex>in lowercase, which is empty:BALANCE_INSUFFICIENT. The 100 units are stranded.- fails: The sender’s own account holds at least the amount
BALANCE_INSUFFICIENT
balances state (unchanged) Key Amount Stored bytes b/<B KEY HEX>100 64 - fails: The sender’s own account holds at least the amount
- member node
- deterministic state machine
- state changed
- no effect
The rules:
- MINT credits
account. If a minter is configured, a MINT from any other member is rejected withBALANCE_NOT_MINTER. With no minter, any member can mint. - TRANSFER debits the sender’s own account and credits
account. The sender’s account is its 32-byte member public key in lowercase hex. A transfer larger than that balance is rejected withBALANCE_INSUFFICIENT. Balances never go negative. - Accounts that are not member keys are receive-only. No member can sign as
alice, so units sent there cannot move again. - Account names are exact text. A key written in uppercase hex is a different account from the member’s own, so units minted there are stranded.
- Storage. The value is
BigInteger.toByteArray(): minimal two’s complement, so 200 is stored as00c8and 50 as32. Readers decode it as an unsigned number. A balance of zero deletes the key.
On a standalone chain a rejected command is a final no-op. Inside a declarative composite it rejects the whole cascade.
When to use it
Section titled “When to use it”Use balances for simple member-owned units: netting or settlement inputs,
loyalty or service credits, prepaid usage units, and demos that need a
deterministic mint and transfer.
Write a custom state machine when accounts belong to identities other than member nodes, minting needs several roles or approvals, you need more than one denomination, transfers need holds or swaps, or settlement on Cardano is part of the transition.
Configure
Section titled “Configure”There is no balances recipe and no balances chain in the stock cluster.
Add a chain entry, with the three
state-identity settings and a fresh
genesis id:
yano: app-chain: chains[3]: chain-id: "credits-chain" state-machine: balances state: commitment-profile: mpf-blake2b256-v1 format-fingerprint: 91ee14091200f1e24659112d640e877e9177779dcc81dd06117f013e9190082b genesis-id: <64 lowercase hex characters, unique to this chain> membership: mode: governed machines: balances: minter: <64-hex member public key>An empty minter lets every member mint; use that only when it is the
intended governance. The minter is a consensus setting.
Submit through REST
Section titled “Submit through REST”The helper encodes commands. This mints 10 units to alice; 830065616c6963650a
is CBOR [0, "alice", 10]:
TOOL=docs/appchain/tutorials/tools/stdlib_command.pyMINT_HEX=$(python3 "$TOOL" balances mint alice 10) # 830065616c6963650a
curl -sS -X POST \ http://127.0.0.1:7070/api/v1/app-chain/chains/credits-chain/messages \ -H 'Content-Type: application/json' \ -d "{\"topic\":\"balances.command.v1\",\"bodyHex\":\"$MINT_HEX\"}" | jq .Send it through the minter’s node. Add -H "X-API-Key: ..." when API
authentication is enabled.
Prove the balance of alice; the state key b/alice is 622f616c696365 in hex:
curl -sS \ http://127.0.0.1:7070/api/v1/app-chain/chains/credits-chain/state/proof/622f616c696365 \ | jq '{committedHeight, stateRoot, presence, valueHex}'For this mint, valueHex is 0a.
Submit from Java
Section titled “Submit from Java”A member transfers from its own account, so it needs a balance first. This example mints to the submitting node’s account, then transfers from it:
AppChainClient raw = AppChainClient.builder("http://127.0.0.1:7070/api/v1") .chainId("credits-chain") .build();StdlibAppChainClient balances = new StdlibAppChainClient(raw);
// The member public key of the node at port 7070, in lowercase hex. It is the// `sender` of any message finalized through that node.String nodeAccount = "<64-hex member public key>";
balances.mint(nodeAccount, BigInteger.valueOf(100)); // needs this node to be the minter// ...wait until the mint is final...balances.transfer("customer-42", BigInteger.TEN); // debits nodeAccount
balances.balance("customer-42").ifPresent(state -> System.out.println(state.value()));GET /api/v1/app-chain/chains/credits-chain/messages/{messageId} returns a
finalized message with its sender. A message id proves only that the command
was accepted; read the balance after the command is final. With the Spring
starter, StdlibAppChainTemplate offers mint, transfer and balance with
the same semantics.
Advanced
Section titled “Advanced”Composites
Section titled “Composites”The composite kernel reports amounts as signed 64-bit integers in its events.
A command whose amount or resulting balances do not fit is rejected with
BALANCE_EVENT_RANGE; it is never rounded. Standalone balances keeps
arbitrary-precision balances.
Admission-rule views and facts
Section titled “Admission-rule views and facts”In a declarative composite, admission rules can read this machine’s state and post-state (ADR-031.4, see admission rules):
- Value view (namespace
"", key: the account id text; a sender’s own account is{fn: hex, args: [{context: sender}]}):balance. A zero balance deletes the account’s key, so an absent read means a zero balance. - Post-state facts, after an approved command and equal to its event:
balanceAfterfor a mint;fromBalanceAfterandtoBalanceAfterfor a transfer.examples/bindings/balances-holding-cap.yamlcaps a recipient’s balance withfacts.toBalanceAfter <= params.cap.
Customization boundary
Section titled “Customization boundary”Account naming, authorization, arithmetic and the deletion of zero balances are consensus rules. Configuration selects only the minter. Write a versioned plugin for more asset types, role-aware ownership, fees, locks or settlement.