Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

journal package

Overview

Package journal is a pure double-entry accounting primitive for realms that need books which provably balance. It is to *bookkeeping* what feeledger is to *money* and duebook is to *time*: it holds no coins, performs no effects, imports no chain APIs, and owns no package-level state. The importing realm owns a *Book, supplies the clock, and performs its own effects.

The one idea

Every financial primitive in the searched Gno ecosystem is ONE-SIDED. A treasury records what moved. A pull-payment ledger records who is owed what. A GRC20 ledger records who holds what. Each stores a balance and mutates it.

This package stores no balance. It stores ENTRIES, and derives balances from them:

Example
1book.MustOpen("1000", "Cash", Asset)
2book.MustOpen("4000", "Revenue", Revenue)
3book.MustPost(now, "invoice 17 paid", []Posting{
4	{Account: "1000", Side: Debit, Amount: 5000},
5	{Account: "4000", Side: Credit, Amount: 5000},
6})

An entry that does not balance is refused, and the refusal says which side is short. So `sum(debits) == sum(credits)` is not a property the consumer must remember to maintain — it is the precondition for the state changing at all. A Book cannot be put into an unbalanced state by any sequence of calls to this package.

No amend, no void

Post is the only way in, and there is no Edit and no Delete. A mistake is corrected the way real books correct one: with a REVERSING ENTRY that mirrors every posting of the original, leaving both visible.

Example
1book.MustReverse(entryID, now, "wrong account")

This is why history is trustworthy here and would not be if entries were mutable. An auditor reading the journal sees the error AND the correction, which is the entire point of keeping a journal rather than a balance. Reverse refuses a second reversal of the same entry, so a correction cannot be looped.

Periods

Close(through) seals everything dated at or before `through`. It only ever moves forward, and after it Post refuses any entry dated into the sealed range — the backdating that makes a closed period meaningless. Reverse is refused there too: correcting a sealed period means opening a new entry in the current one, which is the consumer's call to make explicitly.

What "balanced" is actually worth

Read this before quoting IsBalanced as a guarantee.

IsBalanced compares TWO INDEPENDENTLY MAINTAINED ACCUMULATORS: the per-account debit/credit 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 IsBalanced finds it. What it does NOT do is re-read the journal, so a defect that corrupted both accumulators identically would survive it.

RederiveFromJournal is the strong check: it replays every posting of every entry and compares the result against both accumulators. It costs O(all postings ever) and exists to be run deliberately, not on every read.

AccountingEquation is WEAKER THAN IT LOOKS and is documented as such rather than left for a reader to assume otherwise: under this package's sign conventions it is implied by IsBalanced, and its boolean proves EXACTLY ONE of IsBalanced's three conjuncts. It is NOT an independent accounting fact. Three checks that are really two, presented as three, would be the defect this package exists to avoid.

This doc said "it is a cross-check of this package's own normal-balance handling" until 2026-10-07, and that sentence was FALSE — the one place the careful disclaimer around it still overclaimed. The boolean is the sum of every account's RawDelta, bucketed by Kind and then added back together, so the bucketing cannot change it: offsets is INVARIANT under every possible Kind misclassification, which is the one thing "normal-balance handling" means. See obligation 7 below, and AccountingEquation's own doc for what it does prove.

And the ceiling on all three: a Book can be internally perfect and describe nothing. See obligation 1 below.

State growth

Entries are never removed, because an append-only journal whose entries could vanish is not a journal. Storage therefore grows with every Post and is bounded only by maxEntries, set at construction. A Book that reaches that cap refuses further entries rather than silently dropping history — a long-lived consumer must plan for this, and the plan is a new Book with opening balances carried forward, not a pruned one.

Consumer contract (the parts the package cannot enforce)

  1. THE BOOKS ARE CLAIMS BY WHOEVER POSTED THEM. This package proves arithmetic, never truth. With no custody there is no solvency invariant tying "Cash 5000" to 5000 real ugnot anywhere. A realm that presents a journal balance as a holding is asserting something this package did not check. This is the single most important obligation and the one most commonly got wrong.
  2. SUPPLY A REAL CLOCK. journal cannot verify that `at` came from runtime.ChainHeight() or time.Now().Unix(). A realm that lets a caller choose `at` has no period discipline at all: the caller dates entries wherever they are convenient. Pass the chain's clock, never a transaction parameter.
  3. USE ONE CLOCK CONSISTENTLY. Heights and seconds must not be mixed within a Book; `at` and `through` are both in the caller's unit.
  4. DO NOT EXPORT THE BOOK. A *Book is a mutable handle. Returning one across a realm boundary hands out the right to open accounts, post and reverse. Expose your own crossing functions instead; this package returns Account and Entry VALUES, never pointers into its state.
  5. AUTHORIZE THE POSTER. journal authenticates nobody. Who may open an account, who may post, and who may close a period are the realm's policy — derive the caller from cur.Previous().Address(), not from an argument.
  6. CHOOSE THE UNIT ONCE. Amounts are int64 in whatever minor unit the consumer picks. journal does not know about denominations and will happily balance an entry that mixes two of them.
  7. GET THE KIND RIGHT AT OPEN TIME. NO CHECK IN THIS PACKAGE VALIDATES AN ACCOUNT'S KIND. A Kind is accepted if it is one of the five and is never revisited, so an account opened Asset that should have been Liability reports a sign-flipped Balance() forever — and IsBalanced, RederiveFromJournal and AccountingEquation all three stay healthy, because every one of them is built on Debits and Credits, which the Kind does not touch. This is the invariant tiers' blind spot, it is structural rather than an omission to be fixed later, and a realm that lets a caller choose the Kind has delegated a decision no later check can audit.

All failures are returned as errors and leave the Book COMPLETELY UNCHANGED. Must* wrappers are the only functions here that panic.

The Book is address-agnostic: account codes are non-empty strings. Realms normally use a chart-of-accounts code, not an address.

Functions

MustNew

func MustNew(maxAccounts, maxEntries int) *Book
Ask AI Claude ChatGPT

MustNew is New but panics on error.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal.MustNew(,)"

Result

New

func New(maxAccounts, maxEntries int) (*Book, error)
Ask AI Claude ChatGPT

New returns an empty Book.

maxAccounts caps the chart and must be in [1, MaxAccountsLimit]. maxEntries caps the journal and must be in [1, MaxEntriesLimit]; because entries are never removed, it is a lifetime cap rather than a concurrent one, and a Book that reaches it is finished.

Params

Command

gnokey query vm/qeval -remote "https://rpc.onyx.testnets.gno.land" -data "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal.New(,)"

Result