Cosmic Bull

books

gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books on onyx-1 testnet

realmfactory-applicationaccountinglive on onyx-1

Keep a general ledger on chain, one chart of accounts per owner.

Gives any address a set of books it alone can post to, with every entry balanced before it is accepted.

In detail curated

A general ledger, one book per address, and the FIRST CONSUMER of p/journal on any chain. It holds no coins -- no coinio import, no payable path, no withdrawal, no admin -- so a book is a set of CLAIMS BY ITS OWNER and nothing here ties 'Cash 5000' to 5000 real ugnot anywhere; the realm deliberately exposes no cross-owner aggregate, because such a total would be the sum of every stranger's self-reported assertion. What it adds over the primitive is the two decisions journal refuses to make for a consumer: the CLOCK is runtime.ChainHeight() and never an argument, so Close() has real period discipline; and the KIND IS DERIVED FROM THE ACCOUNT CODE under a mandatory 1xxx-5xxx convention (1 Asset, 2 Liability, 3 Equity, 4 Revenue, 5 Expense) rather than chosen by the caller. That derivation MOVES journal's unauditable-kind hazard rather than removing it -- after it the kind cannot disagree with the code, so 'is this kind right?' becomes the visible 'is this code right?', checkable by any reader of the chart against the published convention -- and nothing validates that the right transaction went into the right account. Authorization is per-owner and total: every mutating path resolves its book from cur.Previous().Address() and there is no cross-owner write, so one owner can never post into another's book. Posting is Post-only with a reversing entry as the sole correction, inherited wholesale from journal; Equation's third result `offsets` is a VACUOUS TRUE on every book this realm can produce and is pinned as such by the suite rather than asserted true and read green.

Use it

A realm holds state. Reading it is a free query; changing it is a transaction you sign yourself.

Read it — no key, no account, no GNOT

gnokey query vm/qeval \
  --data 'gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books.Books()' \
  --remote https://rpc.onyx.testnets.gno.land:443

Books is read-only, so this is a query rather than a transaction: it costs nothing, signs nothing and needs no funded account. Every exported function is listed under the API below; swap the name and the arguments to read any of the others.

Change its state

Every state change is a transaction you sign with your own key — this site never asks for one and holds none. You do not have to hand-write the transaction: gnoweb generates the exact gnokey maketx call command for any of this realm's 7 crossing functions, with the argument names filled in.

Read the known limitations further down this page first. They record what was not verified on chain, which is usually the part that matters.

Import it from another realm

import "gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books"

The path is the address: there is no package manager, nothing to add to a manifest and no version to pin, so the package you compile against is the one the chain runs. It differs per chain because the namespace in it is the deploying account's.

Called from real code

Not an example written for this page. These lines are quoted from cashflow, a deployed package in this portfolio that imports this one as books, out of the committed file whose sha256 onyx-1 testnet attested. They compile because they are compiled.

n := books.Accounts(owner) // panics if the owner has no book
onyx/r/cashflow/cashflow.gno line 369, onyx-1 testnet
codes := books.Codes(owner, "", books.MaxAccountsPerBook)
onyx/r/cashflow/cashflow.gno line 370, onyx-1 testnet
name, kind, debits, credits, balance := books.AccountInfo(owner, code)
onyx/r/cashflow/cashflow.gno line 379, onyx-1 testnet

Deployments

onyx-1 testnet onyx-1

RenderedSourceCall builderState

chain-attested
Import pathgno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height328,091
Deploy transaction17b30107c640d95418020af6012e4349526f387b999c7bdc2a97110cda3b8933 look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used54,632,443
Storagenot attested per package on this chain — see the limitations below
Files on chainbooks.gno gnomod.toml
Deployed bytesbooks.gno — 40,799 bytes
sha25608e3abf635d26a26e7ae2b61f6a4a9c08bb5ee874d825ddb4a5c6be22b99a235
Realm addressg1wn7qumhu55va6449qkxp5tjw6akx3jdcnny559 derived, never confirmed against the realm

Do not take the hash above on trust. $download returns the bytes onyx-1 is actually running; this command fetches them and prints their digest, which should equal the one in the table:

curl -sS 'https://onyx.testnets.gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books$download&file=books.gno' | shasum -a 256

Expected: 08e3abf635d26a26e7ae2b61f6a4a9c08bb5ee874d825ddb4a5c6be22b99a235 — 40,799 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 28 exported functions.

Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:

AccountInfoAccountsBalanceBooksCapacityCloseThroughClosedThroughCodesEntryCountEntryInfoEntryLegsEquationHasAccountHasBookHeightIsBalancedIsReversedKindOfOpenAccountOpenBookOpenedAtPostMultiPostSimpleRawBalanceRederiveRenderReverseTotals

Overview

Package books is a per-owner general ledger: an address opens a chart of accounts and posts balanced double-entry entries against it.

The one idea

Every other ledger-like realm in this portfolio is SINGLE-PURPOSE and its state shape IS its question. invoices tracks receivables. payroll tracks accrued wages. budgets tracks allocations. fee_split tracks shares. Each is a correct answer to one question and structurally cannot answer a second, because there is nowhere to put a transaction that is not already one of the modelled kinds.

This realm has no opinion about what the accounts mean. The owner defines the chart; the realm enforces that every entry balances, that it cannot be altered after the fact, and that only the owner may write it.

NO CUSTODY — and refusing coins is not the same as not holding them

This realm holds nothing. There is no coinio import, no payable path, no fee, no withdrawal, no admin.

So every mutating entrypoint REFUSES a transaction that arrives carrying coins. A realm with no withdrawal path that silently accepts ugnot strands it permanently, and "we don't hold funds" would then be true of the design and false of the balance. Having no custody is not the same as being safe to send coins to, and the difference is one guard.

A BOOK IS A SET OF CLAIMS BY ITS OWNER

Read this before trusting any number this realm returns.

With no custody there is no solvency invariant tying "Cash 5000" to 5000 real ugnot anywhere. A book can be internally perfect and describe nothing. IsBalanced proves arithmetic, never truth.

Consequently this realm exposes NO CROSS-OWNER AGGREGATE. Summing every book's debits would be summing every stranger's self-reported assertion, and the getter's existence would invite reading it as a signal. What it exposes instead is owner-scoped, which is the only question a consumer can act on — and a consumer acting on its OWN book is trusting only itself.

The account code DETERMINES the account kind

An account's code's leading digit fixes its kind, by the standard chart-of-accounts convention:

1xxx -> Asset 2xxx -> Liability 3xxx -> Equity 4xxx -> Revenue 5xxx -> Expense

There is no kind parameter. This is deliberate and it is the realm's answer to p/journal consumer obligation 7, which exists because of that package's audit finding Y1: NO CHECK IN p/journal VALIDATES AN ACCOUNT'S KIND. An account opened Asset that should have been Liability reports a sign-flipped Balance() forever while IsBalanced, RederiveFromJournal and AccountingEquation all three stay healthy — every one of them is built on Debits and Credits, which the kind does not touch.

Deriving the kind does not eliminate that hazard. It MOVES it: after the derivation the kind cannot disagree with the code, so the unauditable question "is this kind right?" becomes the visible question "is this code right?", which any reader of the chart can check against the convention above. That is the whole claim and it is deliberately not stated as more.

The cost is a mandatory numbering convention: a code whose first character is not 1-5 is refused. For a realm whose purpose is auditable books, that is the feature.

Entries are append-only and are corrected by REVERSAL

There is no edit and no delete. A wrong entry is corrected by posting its mirror image, which leaves both the error and the correction readable. Each entry may be reversed at most once; a reversal is itself an entry and is itself reversible once.

Periods

An owner may seal everything up to a height with CloseThrough, after which no entry may be dated at or before it. The seal only moves forward. This is the owner's own discipline, not a protocol rule — nothing forces an owner to close anything.

The clock is the chain's

Entries are dated with runtime.ChainHeight() and never with a caller- supplied value, satisfying p/journal obligations 2 and 3. Heights throughout, never seconds. A realm that let a caller choose the date would have no period discipline at all.

Caps, and why they are far below the engine's

p/journal permits 10 000 accounts and 1 000 000 entries. Its AUDIT.md names the gas ceiling on RederiveFromJournal at full cap — a worst case of 64M posting visits in one call — as UNMEASURED and does not claim it. So this realm sets its own caps far below, and exposes Rederive as an explicit owner-callable rather than running it inside a getter where another party would pay for it.

The caps are per-owner, never global. A global cap on books would be a griefing vector: one address could fill it and lock everyone else out. Per-owner, an owner can exhaust their own capacity and nobody else's, and the cost of doing so is gas plus a storage deposit per record.

Realms may keep books too, for free

The owner is cur.Previous().Address(), so a realm that calls in keeps books under its own address with no extra authorization code. That is the realm-to-realm integration surface, and it arrived as a property of the owner-only rule rather than as a feature.

What this realm does NOT do

Imports

Constants and variables

MaxCodeLen and MaxNameLen bound an account's identifiers. Both are at or below journal's own limits, so the engine's validation is a backstop here rather than the only check.

const (
	MaxCodeLen = 16
	MaxNameLen = 128
)

MaxAccountsPerBook caps one owner's chart. Far below journal's MaxAccountsLimit of 10000: IsBalanced sums the whole chart, and this bound is what keeps that read cheap for everyone.

const MaxAccountsPerBook = 128

MaxEntriesPerBook caps one owner's journal. Far below journal's MaxEntriesLimit of 1000000, because that limit's interaction with RederiveFromJournal is unmeasured — see the package doc.

A book that fills up refuses further entries rather than dropping history. The remedy is a fresh book with opening balances carried forward; there is deliberately no Archive, because it would destroy history and needs more design than it has had.

const MaxEntriesPerBook = 4096

MaxLegsPerEntry caps one entry's postings. journal permits 64; this is lower because an entry is parsed from a single string argument and rendered in full, and both of those costs are linear in the leg count.

const MaxLegsPerEntry = 16

MaxMemoLen bounds an entry's free text. Below journal's MaxMemoLen of 1024 because every memo is a candidate for Render output.

const MaxMemoLen = 256

RealmPath is this realm's own path, used to build Render links.

const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/books"

RenderEntryLimit caps how many entries one Render call emits. A book may hold MaxEntriesPerBook of them and rendering all of them would make the page cost grow without bound.

const RenderEntryLimit = 25

Functions

AccountInfo

func AccountInfo(owner address, code string) (name, kind string, debits, credits, balance int64)

AccountInfo returns an account's full state. journal returns Account BY VALUE, so this hands out a copy and no pointer into stored state.

Accounts

func Accounts(owner address) int

Accounts is the number of accounts in an owner's chart.

Balance

func Balance(owner address, code string) (int64, bool)

Balance returns an account's balance in its NORMAL direction: positive means a debit balance for an asset or expense, and a credit balance for a liability, equity or revenue account.

The second result distinguishes "no such account" from "zero".

Books

func Books() int

Books is the number of books that exist. It is a count and nothing more: there is deliberately no way to enumerate them or aggregate across them.

Capacity

func Capacity(owner address) (accountsLeft, entriesLeft int)

Capacity returns an owner's remaining headroom, so a consumer can see a full book coming rather than discovering it on a refused entry.

CloseThrough

func CloseThrough(cur realm, through int64)

Crossing function. Callable from a transaction via MsgCall, and from another realm as CloseThrough(cross(cur), ...).

CloseThrough seals the caller's book up to and including a height: no entry may afterwards be dated at or before it. The seal only ever moves forward.

Passing a height at or beyond the current one seals the present and makes the book immediately unpostable until the chain advances past it. That is permitted — it is the owner's own book — and it is called out because it is the one way to brick a book without filling it.

ClosedThrough

func ClosedThrough(owner address) (int64, bool)

ClosedThrough returns the height an owner's book is sealed to, and whether it has ever been closed. The second result matters: a book closed through height 0 is not the same as a book that has never been closed.

Codes

func Codes(owner address, from string, limit int) []string

Codes lists an owner's account codes in ascending code order, beginning at from INCLUSIVE and returning at most limit of them. A limit <= 0 returns nothing; a limit above the chart cap is clamped to it. Page by passing the last code received back as from, which re-reads that one code.

Why this exists, and why a counter was not enough

Accounts above returns HOW MANY accounts a chart holds, and AccountInfo answers for a code the caller already knows. Neither lets a caller DISCOVER a code. Without this, a trial balance, a balance sheet and an income statement are all unbuildable from outside this realm however many times the chart is interrogated — a consumer could only report on accounts it had independently been told about, which for a chart the OWNER defines is no guarantee at all.

Why not simply export journal's iterator

journal.Book has IterateAccounts(fn func(Account) bool), and exporting a realm entrypoint that takes fn from the caller would be the shortest code here and a security defect. A caller-supplied callback is the "callback params" class, and journal's own doc records the sharp edge: fn runs under the CALLING REALM'S STORAGE AUTHORITY, because the receiver is stamped with this realm's PkgID and a top-level fn has no declaring realm to anchor it elsewhere. A hostile fn would execute holding r/books' own authority.

So the iterator stays internal and this returns VALUES. The closure below is written by this realm, appends to a local slice, and re-enters no mutator — which is exactly the condition journal's doc attaches to it.

Codes is a free read and NOT a crossing function, so an owner does not pay for a consumer's paging. The cost ceiling is the chart cap: the underlying iterator has no seek, so `from` is honoured by skipping, and one call walks the whole chart however small limit is. A caller paging a 128-account chart 8 codes at a time therefore does 16 full walks, not one. That is a deliberate trade for not touching p/journal, whose Codes-with-seek would have re-opened Gate A and the sabotage sweeps for all three of its importers.

EntryCount

func EntryCount(owner address) int

EntryCount is the number of entries in an owner's journal.

EntryInfo

func EntryInfo(owner address, id uint64) (at int64, memo string, legs int, reversalOf uint64, total int64)

EntryInfo returns one entry's header. Postings are returned separately by EntryLegs, because an entry's leg count is variable and a crossing-function return cannot carry a slice usefully.

EntryLegs

func EntryLegs(owner address, id uint64) string

EntryLegs returns one entry's postings in the "code:side:amount,..." form PostMulti accepts, so an entry can be read and re-posted without a separate encoding.

Equation

func Equation(owner address) (debitNormal, creditNormal int64, offsets bool)

Equation partitions an owner's chart BY ACCOUNT KIND: the summed raw delta of every debit-normal account (Asset, Expense), the same for every credit-normal one (Liability, Equity, Revenue), and whether the two offset.

The two FIGURES are why this exists. They are the balance-sheet and income-statement partition of the chart, and no other accessor on this realm exposes it — Totals sums debits and credits without regard to kind, and AccountInfo answers one code at a time.

`offsets` IS A VACUOUS TRUE ON THIS REALM — do not quote it as a check

Read this before using the third result for anything.

The kind partition is TOTAL: journal's Kind is 1..5, IsDebitNormal is `k == Asset || k == Expense`, so every account lands in exactly one of the two sums and neither can omit one. Therefore

debitNormal + creditNormal == sum of RawDelta over ALL accounts == totalDebits - totalCredits

and this realm refuses an entry that does not balance, so that difference is always zero. `offsets` is consequently TRUE for every book this realm can produce, and it is true for a chart whose accounts are classified nonsensically just as readily as for a correct one.

This is the `HasValidSignature` shape §7b already names: a predicate that is a constant under current conditions, and so is evidence of nothing. The suite pins the vacuity deliberately rather than asserting true and reading green — a test that cannot fail is the thing that makes this dangerous.

What `offsets` DOES still distinguish is arithmetic overflow, where journal returns (0, 0, false). So the honest reading is **"the two sums are trustworthy"**, never "the classification is right". A consumer wanting a genuine classification check must compare the kinds against its own expectation, which is the statement consumer's job and not this realm's.

Both figures are RAW deltas (debits - credits), not normalised balances, so a debit-normal total is normally positive and a credit-normal total normally negative. A credit-normal account carrying a positive raw delta is a contra account, which is legitimate, and is why neither figure is forced non-negative.

HasAccount

func HasAccount(owner address, code string) bool

HasAccount reports whether an owner's chart contains a code.

HasBook

func HasBook(owner address) bool

HasBook reports whether an address has opened a book.

Height

func Height() int64

Height is the chain height this realm dates entries with. Exposed so a caller can predict what CloseThrough will seal.

IsBalanced

func IsBalanced(owner address) bool

IsBalanced is the CHEAP invariant and this realm's live-verification probe.

It compares two independently-maintained accumulators: the per-account totals summed across the chart, against the book's own running totals. They are written by different code paths, so a drift between them is a real defect and this finds it.

What it does NOT do is re-read the journal, so a defect that corrupted both accumulators identically would survive it. That is Rederive's job, and Rederive is a crossing function because the owner should pay for it.

And the ceiling on both: this proves the book is self-consistent, never that its entries describe real events. See the package doc.

IsReversed

func IsReversed(owner address, id uint64) bool

IsReversed reports whether an entry has already been reversed. An entry may be reversed at most once, so this is also "may this still be reversed".

KindOf

func KindOf(code string) string

KindOf reports the kind a code would be given, without opening anything. A consumer can call this before OpenAccount to check its own numbering.

OpenAccount

func OpenAccount(cur realm, code, name string)

Crossing function. Callable from a transaction via MsgCall, and from another realm as OpenAccount(cross(cur), ...).

OpenAccount adds an account to the caller's book. The KIND IS DERIVED from the code's leading digit; there is no kind parameter. See the package doc.

OpenBook

func OpenBook(cur realm)

Crossing function. Callable from a transaction via MsgCall, and from another realm as OpenBook(cross(cur), ...).

OpenBook creates the caller's book. It panics if the caller already has one.

Separating this from OpenAccount is deliberate: creating a book is the call that pays the storage deposit for the owner's record, and a caller should see that cost on its own rather than hidden inside their first account.

OpenedAt

func OpenedAt(owner address) int64

OpenedAt is the height an owner's book was created.

PostMulti

func PostMulti(cur realm, legs, memo string) uint64

Crossing function. Callable from a transaction via MsgCall, and from another realm as PostMulti(cross(cur), ...).

PostMulti posts an entry of up to MaxLegsPerEntry legs, specified as a comma-separated list of colon-separated triples:

"1000:D:500,4000:C:300,4100:C:200"

Side is D or C, case-insensitive. Amounts are positive integers in the owner's chosen unit. The string form is used because a crossing function cannot usefully take a slice over MsgCall; treasury_board encodes a list the same way.

Debits must equal credits, and journal names which side is short.

A leg may name an account another leg already named — journal folds them, and "1000:D:500,1000:C:200,4000:C:300" is a legitimate net +300 to 1000. What is refused is an entry in which EVERY leg names the same account: it balances only by netting to exactly zero, so it is the PostSimple mistake wearing the multi-leg syntax. The narrow rule is deliberate — a blanket "no repeated account" would reject the legitimate fold above.

PostSimple

func PostSimple(cur realm, debitCode, creditCode string, amount int64, memo string) uint64

Crossing function. Callable from a transaction via MsgCall, and from another realm as PostSimple(cross(cur), ...).

PostSimple posts the two-leg entry that is the overwhelmingly common case: debit one account, credit another, for the same amount.

It exists beside PostMulti because the two-leg form cannot be mis-encoded. A caller who wants one debit and one credit should never have to get a leg specification's syntax right.

RawBalance

func RawBalance(owner address, code string) (int64, bool)

RawBalance returns an account's debits minus credits, without the normal- direction sign convention. Exposed because a consumer reconciling against an external system usually wants the raw figure.

Rederive

func Rederive(cur realm) (debits, credits int64, agrees bool)

Crossing function. Callable from a transaction via MsgCall, and from another realm as Rederive(cross(cur), ...).

Rederive runs journal's STRONG invariant check over the caller's own book: it replays every posting of every entry and compares the result against both accumulators.

It is a crossing function rather than a getter on purpose. The check is O(all postings ever) and the owner is the party who should pay for it — exposing it as a free read would let anyone make an owner's book expensive to query. IsBalanced is the cheap check and is a free read.

Returns the replayed totals and whether they agree.

Render

func Render(path string) string

Render serves the index at "" and one owner's book at "<address>".

Every caller-supplied string — account names and entry memos — goes through the ecosystem sanitizer. Free text reaching Render unescaped is the one injection-shaped surface a realm with no custody still has.

Reverse

func Reverse(cur realm, id uint64, memo string) uint64

Crossing function. Callable from a transaction via MsgCall, and from another realm as Reverse(cross(cur), ...).

Reverse posts the mirror image of an existing entry in the caller's book, dated now. The original is left exactly as it was: this is a correction, not an edit.

An entry may be reversed at most once. The reversal is itself an entry and may itself be reversed once.

Totals

func Totals(owner address) (debits, credits int64)

Totals returns an owner's running debit and credit totals.


Doc text is reproduced as vm/qdoc returns it. The node markdown-escapes doc comments, so a bracket or angle bracket may carry a backslash the committed source does not have. The source itself is at source and in this repository.

Dependencies

chain-attested
Importschain/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv, strings
First-party dependenciesgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal

Known limitations

Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.

curated

Source

Source fileonyx/r/books/books.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytesyes — byte-identical