Skip to content

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.

Machine idbalances
Maturitystable
Commands[0, account, amount] MINT; [1, account, amount] TRANSFER; amount is a positive integer
Settingmachines.balances.minter: a member public key as 64 hex characters, or empty
Stateb/<account> → the amount as BigInteger.toByteArray(); a zero balance deletes the key
Proof subjectaccount-balance-v1: coordinate account; claims exact, minimum, maximum
Result codesBALANCE_NOT_MINTER, BALANCE_INSUFFICIENT; BALANCE_EVENT_RANGE in composites
Eventsbalances.minted.v1, balances.transferred.v1, in composites only
Mint and transferIllustrative data

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.

  1. 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 is 00c8: a sign byte comes first.
    • passes: No minter is configured, or the sender is the minter
    balances state
    KeyAmountStored bytes
    b/<B key hex>20000c8
  2. Transfer to alice Member B transfers 50 to alice. A transfer always debits the sender’s own account; the command names only the recipient. alice is 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
    KeyAmountStored bytes
    b/<B key hex>1500096
    b/alice5032
  3. 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
    KeyAmountStored bytes
    b/alice20000c8

What if: B tries to mint

With a minter configured, only that member’s MINT commands apply.

  1. 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 is 00c8: a sign byte comes first.
    • passes: No minter is configured, or the sender is the minter
    balances state
    KeyAmountStored bytes
    b/<B key hex>20000c8
  2. B mints Member B mints 1000 to itself. B is not the configured minter, so the decision is BALANCE_NOT_MINTER and nothing is written.
    • fails: No minter is configured, or the sender is the minter BALANCE_NOT_MINTER
    balances state (unchanged)
    KeyAmountStored bytes
    b/<B key hex>20000c8

What if: B overspends

Balances never go negative.

  1. 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 is 00c8: a sign byte comes first.
    • passes: No minter is configured, or the sender is the minter
    balances state
    KeyAmountStored bytes
    b/<B key hex>20000c8
  2. B sends 500 Member B transfers 500 to alice but holds 200. The decision is BALANCE_INSUFFICIENT; neither account changes.
    • fails: The sender’s own account holds at least the amount BALANCE_INSUFFICIENT
    balances state (unchanged)
    KeyAmountStored bytes
    b/<B key hex>20000c8

What if: Spend alice’s units

A transfer has no “from” field. Units in a non-member account cannot move again.

  1. 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 is 00c8: a sign byte comes first.
    • passes: No minter is configured, or the sender is the minter
    balances state
    KeyAmountStored bytes
    b/<B key hex>20000c8
  2. Transfer to alice Member B transfers 50 to alice. A transfer always debits the sender’s own account; the command names only the recipient. alice is 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
    KeyAmountStored bytes
    b/<B key hex>1500096
    b/alice5032
  3. 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
    KeyAmountStored bytes
    b/alice20000c8
  4. 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 is BALANCE_INSUFFICIENT. No member can sign as alice.
    • fails: The sender’s own account holds at least the amount BALANCE_INSUFFICIENT
    balances state (unchanged)
    KeyAmountStored bytes
    b/alice20000c8

What if: Mint to an uppercase key

Account names are compared as exact text. A sender’s account is always lowercase hex.

  1. 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
    KeyAmountStored bytes
    b/<B KEY HEX>10064
  2. B tries to spend Member B transfers 10 to alice. The machine debits b/<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)
    KeyAmountStored bytes
    b/<B KEY HEX>10064
  • 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 with BALANCE_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 with BALANCE_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 as 00c8 and 50 as 32. 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.

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.

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.

The helper encodes commands. This mints 10 units to alice; 830065616c6963650a is CBOR [0, "alice", 10]:

Terminal window
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
MINT_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:

Terminal window
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.

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.

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.

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: balanceAfter for a mint; fromBalanceAfter and toBalanceAfter for a transfer. examples/bindings/balances-holding-cap.yaml caps a recipient’s balance with facts.toBalanceAfter <= params.cap.

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.