Cosmic Bull

journal

gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/journal on gno.land mainnet
gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal on onyx-1 testnet

pure packageprimitiveaccountinglive on mainnetlive on onyx-1

Keep double-entry books that always balance.

Every entry is checked before it is recorded, so the ledger cannot drift out of balance one posting at a time.

In detail curated

gno.land mainnet

Double-entry journal primitive: balanced entries enforced at Post (sum(debits)==sum(credits)), immutable once posted, reversal by compensating entry, balances derived not stored. The accounting-suite floor -- eight prepared mainnet applications import it. Byte-exact twin of the live onyx copy (zero substitutions); audit clean, 99/0 sabotage coverage run live by register gate SEC8.

onyx-1 testnet

Double-entry bookkeeping as a pure primitive. It stores no balance -- it stores ENTRIES and derives balances from them, which is what separates it from every one-sided financial primitive in the searched ecosystem (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). An entry that does not balance is REFUSED and the refusal names the short column, so sum(debits) == sum(credits) is the precondition for the state changing at all rather than a property the consumer must remember to maintain: a Book cannot be put into an unbalanced state by any sequence of calls to this package. Post is the only way in -- there is no Edit and no Delete -- and a mistake is corrected the way real books correct one, with a REVERSING ENTRY that mirrors every posting and leaves both visible; Reverse refuses a second reversal of the same entry, so a correction cannot be looped. Close(through) seals a period, only ever moves forward, and afterwards both Post and Reverse refuse anything dated into the sealed range. Amounts are always POSITIVE with direction carried by Side rather than a sign, which is what makes 'which column is short' answerable. Three consistency checks of DELIBERATELY DIFFERENT STRENGTH, each documented at its real strength: IsBalanced compares two independently-maintained accumulators (cheap, catches a drift between code paths, does not re-read the journal); RederiveFromJournal replays every posting ever and is the strong claim at O(all postings); AccountingEquation is WEAKER THAN IT LOOKS and says so -- its boolean proves exactly one of IsBalanced's three conjuncts and is INVARIANT under every possible Kind misclassification, so its value is the two subtotals, not the flag. Holds no coins, performs no effects, imports no chain APIs, owns no package-level state; the realm owns a *Book and supplies the clock.

One description per chain, because this application is deployed on more than one chain and each account was written for its own deployment. The programs differ only where the chain's namespace appears in the source — proved, not asserted, under Deployments below — so where the accounts differ they differ in emphasis and in which chain's history they describe, not in what the code does.

Use it

A pure package has no state and no transactions: you use it by importing it.

Import it

on gno.land mainnet

import "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/journal"

on onyx-1 testnet

import "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal"

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 assets, a deployed package in this portfolio that imports this one as journal, out of the committed file whose sha256 onyx-1 testnet attested. They compile because they are compiled.

b := journal.MustNew(2, MaxEntriesPerRegister)
onyx/r/assets/assets.gno line 371, onyx-1 testnet

Deployments

Deployed on 2 chains from the same source. The bytes are identical — one digest, cca6944e1dcbddd2…, on both — so the API below is the API on either. Everything that is not the bytes differs per chain and is stated per chain: the import path, the namespace, the transaction, the height, the cost, and which files the chain holds.

gno.land mainnet gnoland-1

RenderedSourceCall builder

chain-attested
Import pathgno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/journal
Namespaceg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Deployed at height1,316,628
Deploy transactionf43ed0546985bcd744af240ad3c4f196defbb7bc1138b9ec6a6f55380083fda1 look it up on the RPC
Deployerg1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Gas used104,756,192
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml journal.gno journal_test.gno
Deployed bytesjournal.gno — 35,285 bytes
sha256cca6944e1dcbddd212c18e7b2531bca6065a28f7c80b541b4281f034ff505a1a
Realm addressnone — A /p/ package is not a realm: no state, no custody, never a transaction sender.
Used byno consumer on this chain

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

curl -sS 'https://gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/journal$download&file=journal.gno' | shasum -a 256

Expected: cca6944e1dcbddd212c18e7b2531bca6065a28f7c80b541b4281f034ff505a1a — 35,285 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

onyx-1 testnet onyx-1

RenderedSourceCall builder

chain-attested
Import pathgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal
Namespaceg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Deployed at height327,179
Deploy transactione405a57357ab24aa6d56001996f167edb7075c010e2dcc2574c29ea343747f76 look it up on the RPC
Deployerg1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3
Gas used47,593,781
Storagenot attested per package on this chain — see the limitations below
Files on chaingnomod.toml journal.gno
Deployed bytesjournal.gno — 35,285 bytes
sha256cca6944e1dcbddd212c18e7b2531bca6065a28f7c80b541b4281f034ff505a1a
Realm addressnone — A /p/ package is not a realm: it holds no state, custodies no coins and is never a transaction sender. The pkgPath derivation would still produce a value; recording one would name nothing.
Used byp/cashflow, p/periods, p/statements, r/assets, r/books, r/cashflow, r/inventory, r/statements

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/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal$download&file=journal.gno' | shasum -a 256

Expected: cca6944e1dcbddd212c18e7b2531bca6065a28f7c80b541b4281f034ff505a1a — 35,285 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.

API

chain-derived 2 exported functions, 6 types, 36 methods.

Read from vm/qdoc on gnoland-1. It describes the onyx-1 testnet deployment as well: the deployed bytes are identical on every chain here — one digest, cca6944e1dcbddd2… — and this site refuses to build a shared page for a package whose bytes differ across chains, so the same API is the API on each. The API is the only thing that carries across; every provenance fact above is per-chain.

This is a /p/ package: you import it, you do not call it in a transaction. gnoweb's $help shows only exported top-level functions, so the types and methods below do not appear there at all — which is why this reference exists.

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:

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

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.

book.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.

Imports

Constants and variables

MaxAccountsLimit is the largest maxAccounts a Book may be constructed with. It bounds the cost of IsBalanced and AccountingEquation, which scan the chart of accounts.

const MaxAccountsLimit = 10000

MaxCodeLen bounds an account code.

const MaxCodeLen = 64

MaxEntriesLimit is the largest maxEntries a Book may be constructed with.

const MaxEntriesLimit = 1000000

MaxMemoLen bounds an entry memo. The memo is opaque to journal — it exists so a realm can record why an entry was made without keeping a parallel table.

const MaxMemoLen = 1024

MaxNameLen bounds an account's human-readable name.

const MaxNameLen = 256

MaxPostingsPerEntry bounds a single entry's posting count. Two is the minimum a balanced entry can have; the ceiling keeps one Post bounded.

const MaxPostingsPerEntry = 64

Errors returned by Book operations.

var (
	ErrEmptyCode       = errors.New("journal: empty account code")
	ErrCodeTooLong     = errors.New("journal: account code exceeds MaxCodeLen")
	ErrNameTooLong     = errors.New("journal: account name exceeds MaxNameLen")
	ErrMemoTooLong     = errors.New("journal: memo exceeds MaxMemoLen")
	ErrInvalidKind     = errors.New("journal: unknown account kind")
	ErrInvalidSide     = errors.New("journal: posting side must be Debit or Credit")
	ErrInvalidAmount   = errors.New("journal: posting amount must be positive")
	ErrInvalidAt       = errors.New("journal: at must be non-negative")
	ErrInvalidConfig   = errors.New("journal: invalid book configuration")
	ErrAccountExists   = errors.New("journal: account code already open")
	ErrUnknownAccount  = errors.New("journal: no such account")
	ErrChartFull       = errors.New("journal: account cap reached")
	ErrBookFull        = errors.New("journal: entry cap reached")
	ErrTooFewPostings  = errors.New("journal: an entry needs at least two postings")
	ErrTooManyPostings = errors.New("journal: entry exceeds MaxPostingsPerEntry")
	ErrNoDebit         = errors.New("journal: entry has no debit posting")
	ErrNoCredit        = errors.New("journal: entry has no credit posting")
	ErrDebitShort      = errors.New("journal: entry does not balance, debits are short")
	ErrCreditShort     = errors.New("journal: entry does not balance, credits are short")
	ErrEntryNotFound   = errors.New("journal: no such entry")
	ErrAlreadyReversed = errors.New("journal: entry has already been reversed")
	ErrPeriodClosed    = errors.New("journal: at falls in a closed period")
	ErrCloseNotForward = errors.New("journal: close must move forward")
	ErrOverflow        = errors.New("journal: int64 overflow")
	ErrIDExhausted     = errors.New("journal: identifier space exhausted")
)

The five account kinds. Asset and Expense are debit-normal; Liability, Equity and Revenue are credit-normal.

const (
	Asset     Kind = 1
	Liability Kind = 2
	Equity    Kind = 3
	Revenue   Kind = 4
	Expense   Kind = 5
)

The two posting sides.

const (
	Debit  Side = 1
	Credit Side = 2
)

Types

type Account

type Account struct {
	Code    string
	Name    string
	Kind    Kind
	Debits  int64
	Credits int64
}

Account is one line of the chart of accounts, carrying its cumulative debit and credit totals. It is returned BY VALUE: holders cannot reach into a Book through it.

Account.Balance

func (a Account) Balance() int64

Balance returns the account's balance in its NORMAL direction: for a debit-normal account Debits-Credits, for a credit-normal account Credits-Debits. A normal account therefore has a non-negative balance, and a negative result is a real signal (a contra position) rather than a sign convention.

Account.RawDelta

func (a Account) RawDelta() int64

RawDelta returns Debits-Credits regardless of kind. Summed over every account in a balanced book it is exactly zero, which is what makes it the useful quantity for a consistency check and the wrong one for a report.

type Book

type Book struct {
	maxAccounts int
	maxEntries  int

	accounts *avl.Tree // code -> Account
	entries  *avl.Tree // padded id -> Entry
	// reversed stores `true`, not a struct{} sentinel, and deliberately:
	// avl.Tree.Get cannot tell a nil-valued key from an absent one, so a
	// nil sentinel invites a future reader to switch from Has to Get and
	// get a silently wrong answer. Membership is the whole value here —
	// both readers use Has and the stored value is never read.
	reversed *avl.Tree // padded id -> true, for entries already reversed

	nextID        uint64 // never decreases; entry IDs are never reused
	closedThrough int64  // entries dated at or before this are refused
	hasClosed     bool   // distinguishes "closed through 0" from "never closed"

	totalDebits  int64 // running total, maintained independently of accounts
	totalCredits int64
}

Book is the chart of accounts and the journal of one consuming realm. The zero value is not usable; construct with New.

Book.Account

func (b *Book) Account(code string) (Account, bool)

Account returns an account by code. The second result is false if no such account is open.

Book.AccountCount

func (b *Book) AccountCount() int

AccountCount returns how many accounts are open.

Book.AccountingEquation

func (b *Book) AccountingEquation() (debitNormal, creditNormal int64, offsets bool)

AccountingEquation returns the two halves of assets+expenses versus liabilities+equity+revenue, in RAW debit-minus-credit terms, and whether they offset.

IT IS WEAKER THAN IT LOOKS, and weaker than this doc used to claim.

What offsets ACTUALLY proves: sum(RawDelta) == 0 over the whole chart, which is the same fact as IsBalanced's first conjunct (d == c) and nothing more. IsBalanced requires three. So this is implied by IsBalanced, is NOT an independent accounting fact, and citing it as a second guarantee alongside IsBalanced is counting one check twice.

What offsets DOES NOT prove, despite the Kind partition sitting right there in the code: anything whatsoever about normal-balance handling. dn and cn are a partition of the same addends, so dn+cn is the total regardless of which bucket each account landed in. Reclassify every account in the chart and BOTH subtotals change while offsets stays true. A Kind set wrongly at Open time is undetectable here and undetectable anywhere else in this package — obligation 7.

It is exported because the two SUBTOTALS are correct and a consumer presenting a balance sheet wants them anyway. They are the product; offsets is a duplicate of a check you already have.

Book.Balance

func (b *Book) Balance(code string) (int64, bool)

Balance returns an account's balance in its normal direction, and whether the account exists. A missing account returns (0, false) rather than a zero balance, because "no such account" and "balance zero" are different answers and a consumer branching on the number alone would conflate them.

Book.Close

func (b *Book) Close(through int64) error

Close seals the period ending at `through`, after which Post and Reverse refuse any entry dated at or before it.

It only moves forward. A first close may name any non-negative instant; a later one must name a strictly greater instant, so a seal can never be loosened and backdating can never be re-enabled.

Fails with ErrInvalidAt (through < 0) or ErrCloseNotForward. On error nothing is modified.

Book.ClosedThrough

func (b *Book) ClosedThrough() (int64, bool)

ClosedThrough returns the period seal: entries dated at or before it are refused. The second result is false if the Book has never been closed, in which case the first is meaningless — a Book closed through 0 and a Book never closed differ, and collapsing them would silently reject entries dated at 0.

Book.Entries

func (b *Book) Entries(from uint64, limit int) []Entry

Entries returns up to limit entries with ID >= from, in ascending ID order, each with a defensive copy of its postings. A limit <= 0 returns nothing. It is the paging read for a consumer that cannot hold the whole journal in one call.

Book.Entry

func (b *Book) Entry(id uint64) (Entry, bool)

Entry returns an entry by ID, with a defensive copy of its postings. The second result is false if no such entry exists — which, because entries are never removed, means it was never created.

Book.EntryCount

func (b *Book) EntryCount() int

EntryCount returns how many entries the journal holds. It never decreases.

Book.IsBalanced

func (b *Book) IsBalanced() bool

IsBalanced reports whether the Book's two independently-maintained accumulators agree with each other and with themselves.

It scans the chart, sums every account's debit and credit totals, and requires that the two sums are equal AND that each matches the Book's own running total. Those totals are written by different code in Post, so a disagreement is a defect in this package and IsBalanced is how a consumer detects one.

What it does not do is re-read the journal. Read the package doc's "What balanced is actually worth" before quoting it, and use RederiveFromJournal for the strong claim.

An empty Book is balanced.

Book.IsReversed

func (b *Book) IsReversed(id uint64) bool

IsReversed reports whether an entry has already been reversed. It returns false for an entry that does not exist, so pair it with Entry when the distinction matters.

Book.IterateAccounts

func (b *Book) IterateAccounts(fn func(Account) bool)

IterateAccounts calls fn for every account in ascending code order. Iteration stops early when fn returns true.

fn receives a COPY: Account is passed by value and holds only scalars and strings, so fn gets no pointer into the Book.

Two rules for fn, and the second is the one that is easy to miss.

Book.IterateEntries

func (b *Book) IterateEntries(fn func(Entry) bool)

IterateEntries calls fn for every entry in ascending ID order, which for an append-only journal is also chronological by insertion. Iteration stops early when fn returns true.

fn receives an Entry whose Postings slice is a DEFENSIVE COPY, so a callback cannot write through it into Book state. The two rules on IterateAccounts's fn apply here unchanged, including the storage-authority one.

Book.MaxAccounts

func (b *Book) MaxAccounts() int

MaxAccounts returns the Book's cap on chart size.

Book.MaxEntries

func (b *Book) MaxEntries() int

MaxEntries returns the Book's lifetime cap on entries.

Book.MustClose

func (b *Book) MustClose(through int64)

MustClose is Close but panics on error.

Book.MustOpen

func (b *Book) MustOpen(code, name string, kind Kind)

MustOpen is Open but panics on error.

Book.MustPost

func (b *Book) MustPost(at int64, memo string, postings []Posting) uint64

MustPost is Post but panics on error.

Book.MustReverse

func (b *Book) MustReverse(id uint64, at int64, memo string) uint64

MustReverse is Reverse but panics on error.

Book.NextID

func (b *Book) NextID() uint64

NextID returns the identifier the next entry will take. It only ever increases, so an entry ID identifies one entry for the life of the Book.

Book.Open

func (b *Book) Open(code, name string, kind Kind) error

Open adds an account to the chart. Codes are unique and immutable: there is no Rename and no Close, because an account's cumulative totals are part of the derivation of every balance that cites it, and removing one would make the journal unreplayable.

Fails with ErrEmptyCode, ErrCodeTooLong, ErrNameTooLong, ErrInvalidKind, ErrAccountExists, or ErrChartFull. On error nothing is modified.

Book.Post

func (b *Book) Post(at int64, memo string, postings []Posting) (uint64, error)

Post appends a balanced entry and returns its ID.

The postings must number at least two and at most MaxPostingsPerEntry, each naming an open account, a valid Side, and a positive Amount. The entry must contain at least one debit and at least one credit, and the two column totals must be equal — an inequality is reported as ErrDebitShort or ErrCreditShort, naming the column that needs more.

Fails with ErrInvalidAt, ErrMemoTooLong, ErrPeriodClosed, ErrBookFull, ErrIDExhausted, ErrTooFewPostings, ErrTooManyPostings, ErrInvalidSide, ErrInvalidAmount, ErrUnknownAccount, ErrNoDebit, ErrNoCredit, ErrDebitShort, ErrCreditShort, or ErrOverflow. On error nothing is modified — every posting is validated and every total computed before the first write.

Book.RederiveFromJournal

func (b *Book) RederiveFromJournal() (debits, credits int64, agrees bool)

RederiveFromJournal replays every posting of every entry and reports the debit and credit totals it finds, together with whether they agree with both the per-account accumulators and the Book's running totals.

This is the strong check and the expensive one: its cost is proportional to every posting ever made, so it is meant to be called deliberately — by an auditor, or by a consumer's own invariant view — and not from a hot path. `agrees` false on a Book this package built is a defect in this package.

Book.Reverse

func (b *Book) Reverse(id uint64, at int64, memo string) (uint64, error)

Reverse appends the mirror of an existing entry — every posting repeated on the opposite side, for the same amount — and returns the new entry's ID. The original is left exactly as it was.

An entry may be reversed at most once. Reversing a reversal IS permitted (it re-applies the original's effect) and is itself subject to the same once-only rule, so a correction cannot be looped.

`at` dates the correction and must be in the open period: a sealed period cannot be corrected in place, by construction. Correcting one means posting into the current period, which is the consumer's decision to make explicitly rather than this package's to make silently.

Fails with ErrInvalidAt, ErrMemoTooLong, ErrPeriodClosed, ErrEntryNotFound, ErrAlreadyReversed, ErrBookFull, ErrIDExhausted, or ErrOverflow. On error nothing is modified.

Book.TotalCredits

func (b *Book) TotalCredits() int64

TotalCredits returns the Book's running credit total across every entry.

Book.TotalDebits

func (b *Book) TotalDebits() int64

TotalDebits returns the Book's running debit total across every entry.

type Entry

type Entry struct {
	ID         uint64
	At         int64
	Memo       string
	Postings   []Posting
	ReversalOf uint64
}

Entry is one journal entry: a dated, memo'd, balanced set of postings. It is returned BY VALUE, and its Postings slice is a DEFENSIVE COPY — a holder can mutate the returned slice without touching the Book. Entry is the one type here that contains a slice, so it is the one type where returning the stored value directly would have handed out a writable window into Book state.

ReversalOf is 0 unless this entry was produced by Reverse, in which case it is the ID of the entry it mirrors.

Entry.IsReversal

func (e Entry) IsReversal() bool

IsReversal reports whether this entry was produced by Reverse.

Entry.Total

func (e Entry) Total() int64

Total returns the entry's debit total, which for any entry a Book accepted equals its credit total.

type Kind

type Kind uint8

Kind is an account's classification, which fixes its normal balance. The zero value is deliberately invalid so a forgotten field is an error rather than an Asset.

Kind.IsDebitNormal

func (k Kind) IsDebitNormal() bool

IsDebitNormal reports whether an increase in k is recorded as a debit. Asset and Expense are debit-normal; the other three are credit-normal. An invalid Kind reports false, which is why callers validate first.

Kind.String

func (k Kind) String() string

String returns the kind's name, or "invalid" for the zero value and any unrecognised code.

Kind.Valid

func (k Kind) Valid() bool

Valid reports whether k is one of the five kinds.

type Posting

type Posting struct {
	Account string
	Side    Side
	Amount  int64
}

Posting is one leg of an entry: an amount in one column of one account. Amount is always POSITIVE — direction is carried by Side, not by a sign.

That is a deliberate choice over signed amounts. With a sign, "which side is short" is unanswerable without knowing each account's normal balance, so an unbalanced entry can only be reported as a number that does not add up. With an explicit Side the refusal names the short column, which is what a bookkeeper needs in order to fix it.

type Side

type Side uint8

Side is which column a posting lands in. The zero value is deliberately invalid: a posting with a forgotten Side must be refused, not silently treated as a debit.

Side.Opposite

func (s Side) Opposite() Side

Opposite returns the other side. An invalid Side returns itself, so a caller that skipped validation gets an invalid posting rather than a plausible one.

Side.String

func (s Side) String() string

String returns "debit", "credit", or "invalid".

Side.Valid

func (s Side) Valid() bool

Valid reports whether s is Debit or Credit.

Functions

MustNew

func MustNew(maxAccounts, maxEntries int) *Book

MustNew is New but panics on error.

New

func New(maxAccounts, maxEntries int) (*Book, error)

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.


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

A first-party dependency is imported from the SAME chain's namespace, so each deployment above depends on that chain's copy — the paths differ even where the code does not.

chain-attested
Importserrors, gno.land/p/nt/avl/v0, strconv
First-party dependenciesnone

Known limitations

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

curated

gno.land mainnet

onyx-1 testnet

Source

Source file (gnoland-1)mainnet/p/journal/journal.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytes (gnoland-1)yes — byte-identical
Source file (onyx-1)onyx/p/journal/journal.gno at commit c2e10d17ffbf in the project repository (not public — the digest command above is the check that needs no repository)
Matches the deployed bytes (onyx-1)yes — byte-identical