inventory
gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/inventory on onyx-1 testnet
realmapplicationaccountinglive on onyx-1
Track stock on hand and the cost of what you have sold.
Cost of goods sold is drawn from the lots actually consumed, so margin is read out of the records rather than asserted.
In detail curated
A per-owner inventory sub-ledger whose cost of goods sold is DERIVED from the lots on hand, never supplied: it holds a p/lots.Pool per SKU and Ship takes a QUANTITY and returns the cost its costing method charged against those lots, so no entrypoint anywhere accepts an amount. The chart is THREE fixed accounts -- 1300 Inventory (asset) debited on receipt against 3900 Inventory contributed (equity), and 5000 Cost of goods sold (expense) debited on shipment against 1300 -- opened at OpenRegister with a hard cap of three so the chart cannot grow, which is what makes this realm its book's only writer and the cross-foots EXACT. 3900 is a PLUG this realm is forced to pick and says so in its own doc and Render: a debit to 1300 needs a credit leg, every candidate asserts something unobservable, and the contribution is the only one coherent without a follow-up event -- a register whose goods were bought on credit understates liabilities by exactly balance(3900). Costing method is per item and FIXED at creation via AddFIFO/AddLIFO/AddWAVG; a WAVG item merges into one lot and so escapes the live-lot cap but gains a merge-overflow refusal that is a property of its accumulated history, which is why LotCapacity is the wrong headroom figure for it. A zero-cost receipt and a zero-derived-cost shipment update the pool and post NOTHING, preserving the identities exactly. The weight-bearing checks are per-item Invariant (postedCOGS == pool.ChargedCost(), two independently maintained figures) and the three register cross-foots (sum over items == the ledger balance, EXACT), all FREE reads so any third party verifies a register from vm/qeval with no key -- the same choice r/assets makes and r/books does not. SKUs enumerates the items, closing the discovery gap r/books has. refuseCoins refuses an envelope; no custody, no write-off (shrinkage is indistinguishable from a sale, named not hidden), no revenue, no period close. Two IsCurrent() guards on non-crossing helpers are no-ops for every present caller, kept because the realm is immutable after addpkg.
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/inventory.Height()' \
--remote https://rpc.onyx.testnets.gno.land:443Height 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/inventory"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.
Deployments
onyx-1 testnet onyx-1
RenderedSourceCall builderState
chain-attested| Import path | gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/inventory |
|---|---|
| Namespace | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Deployed at height | 358,572 |
| Deploy transaction | 9a712373c4049922f275542806c4c3c4dfffcdd938a95652a3db4255f741c8e0 look it up on the RPC |
| Deployer | g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3 |
| Gas used | 76,848,736 |
| Storage | not attested per package on this chain — see the limitations below |
| Files on chain | inventory.gno gnomod.toml |
| Deployed bytes | inventory.gno — 58,192 bytes |
| sha256 | 91a2d2dcd86dcfe4b6307a17dfc0a4525c4aaae7eafef5f7e5f3293cdd9b37aa |
| Realm address | g1dx44e0g369xjntuqz4w8djlh8qazhdh3syxvyz 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/inventory$download&file=inventory.gno' | shasum -a 256Expected: 91a2d2dcd86dcfe4b6307a17dfc0a4525c4aaae7eafef5f7e5f3293cdd9b37aa — 58,192 bytes. Use curl: the gno.land edges answer Python's default user-agent with HTTP 403.
API
chain-derived 46 exported functions.
Every function below deep-links to gnoweb's call builder, which generates a ready-to-run gnokey maketx call for it:
AccountKindAddFIFOAddLIFOAddWAVGAddedAtBalanceCOGSDebitCapacityChargedContributedCreditCostOnHandCrossFootCrossFootCOGSCrossFootContributedCrossFootInventoryEntryCountEquationHasItemHasRegisterHeightInvariantInvariantReceiptsInventoryDebitIsBalancedItemCountItemCrossFootItemCrossFootQtyItemNameLotAtLotCapacityLotCountMethodOnHandOpenRegisterOpenedAtRawBalanceReceiptsReceiveReceivedRederiveRegistersRenameRenderSKUsShipTotals
Overview
Package inventory is a per-owner inventory sub-ledger whose cost of goods sold is DERIVED from the lots on hand rather than asserted by a caller.
What this realm is for
A general ledger takes a debit code, a credit code and an amount from whoever is posting. That is the right design for a ledger, and it means gno.land/r/.../books structurally CANNOT know that an entry was a shipment, or that the cost charged for that shipment is the cost the goods were actually bought at. Nothing stops a shipper from booking a flattering number, and nothing in the book can tell afterwards.
This realm closes that gap for one class of entry. It holds each item's cost lots as state, and when goods ship it computes the charge from those lots by the item's declared costing method. The amount is never a parameter: Ship takes a QUANTITY and returns the cost it derived.
The resulting claims are narrow and checkable by anyone, which is the point. Per item, from p/lots' own cross-foot:
receivedCost == chargedCost + costOnHand
and, across a whole register, against the ledger the realm posted to:
balance(1300) == sum of costOnHand over items balance(5000) == sum of chargedCost over items balance(3900) == sum of receivedCost over items
All four are exported — ItemCrossFoot, CrossFootInventory, CrossFootCOGS, CrossFootContributed — and all are FREE reads, deliberately; see "Why the invariants are free reads" below.
Note what the three ledger identities are NOT. Each compares a walk of the lots against an account balance, and the two are maintained by different code: the pool counts for itself inside p/lots, while this realm posts to the book. A disagreement is therefore a real signal and not a tautology. That is the same reason p/lots keeps counters beside its lots.
The chart is three accounts and is fixed
1300 Inventory (asset, debit-normal) 5000 Cost of goods sold (expense, debit-normal) 3900 Inventory contributed (equity, credit-normal)
A receipt is debit 1300 / credit 3900. A shipment is debit 5000 / credit 1300 for the DERIVED cost. There is no OpenAccount entrypoint and the underlying book is created with a hard cap of three accounts, so the chart cannot grow. The accounting equation offsets at 0 = 0 on an empty register and after every posting, because 1300 + 5000 == 3900 is the register-wide restatement of each pool's own cross-foot.
3900 IS A PLUG, AND THIS REALM IS FORCED TO PICK ONE
This is the design's one unavoidable assertion about the world, so it is stated here rather than buried.
gno.land/r/.../assets faced the same question about an asset's cost basis and answered by NOT BOOKING IT: the only honest credit legs were a liability ("2000 Acquisition payable") or an equity plug ("3000 Contributed capital"), both of which assert how the asset was funded, which that realm does not know. It kept the cost in register state as data instead.
That answer is not available here, and the difference is structural rather than a matter of taste. The product IS cost of goods sold. A shipment debits 5000 and credits 1300, so 1300 must carry a balance, so the receipt must reach the book — and a debit to 1300 needs a credit leg. The choice is therefore not "book it or not" but "which plug", and every candidate asserts something unobservable:
- 2000 Accounts payable asserts the goods were bought on credit and that a liability is outstanding. This realm cannot see a payment, so the liability would never be relieved and would grow without bound.
- 1000 Cash asserts the goods were paid for in cash from a balance this realm does not hold and cannot verify. It holds no coins at all.
- 3900 Inventory contributed asserts the goods were contributed by the owner. It is wrong in exactly the same way the other two are, and it is the only one of the three that is SELF-CONSISTENT without an unobservable second event: a contribution is complete on arrival.
So 3900 was chosen because it needs no follow-up transaction to stay coherent, not because it is true. Read it as "goods arrived from somewhere this ledger does not model". A register whose goods were in fact purchased on credit is understating liabilities and overstating equity by exactly balance(3900), and no code path here can detect that. Pairing this sub-ledger with a real payables ledger is out of scope for v1 and is the honest fix.
Costing method is per item and is FIXED at creation
An item is added by AddFIFO, AddLIFO or AddWAVG — three entrypoints rather than one taking a method code, following r/assets' Place / PlaceDeclining precedent, so a caller never has to know an enum's numbering.
p/lots fixes the method at pool construction and documents why: changing it mid-life would recost nothing already charged but would change what the next shipment costs, which is a restatement a sub-ledger must not be able to make silently. This realm inherits that and adds nothing: there is no entrypoint that changes an item's method. An owner who must switch opens a new SKU and discloses it.
The three methods differ observably, which is the point of having them: FIFO and LIFO charge different amounts whenever unit costs differ, and WAVG re-anchors every receipt into a single merged lot so it holds exactly one lot and can never hit the per-item lot cap.
That exemption is not a free win, and the trade is the other way round from how it reads. WAVG escapes the LOT cap and in exchange acquires a refusal FIFO and LIFO do not have: a merged lot must satisfy p/lots' per-lot overflow precondition `Cost <= maxInt64/Qty`, and two lots that each satisfy it separately can merge into one that does not. So under WAVG a receipt can be refused for the pool's ACCUMULATED HISTORY rather than for anything about the receipt — the same receipt a FIFO item would have taken. It is a far corner of the arithmetic and not a practical bound, but it is the reason LotCapacity is the wrong thing to watch for a WAVG item: its figure is a bound WAVG will never approach, so a caller sizing headroom from it is reading the one limit that does not apply. See p/lots' Pool.Receive.
A ZERO-COST POSTING IS SKIPPED, AND THAT IS NOT A SHORTCUT
p/journal rejects a posting of zero (ErrInvalidAmount) — correctly; a zero entry is not an event. But both of this realm's postings can legitimately be zero:
- Receive with cost 0 is free goods, which p/lots accepts (ErrBadCost is only for a NEGATIVE cost).
- Ship out of lots that cost nothing derives a charge of 0.
In both cases the pool is updated and NOTHING is posted. The identities above are preserved exactly, because a zero contribution to the ledger side is matched by a zero contribution to the walk side. The same choice r/assets makes for a degenerate declining schedule, for the same reason.
What this realm does NOT do
- No custody. It holds no coins, has no withdrawal path, and refuses a user call that arrives carrying any. A cost is a number an owner typed.
- No write-off, shrinkage or revaluation. Shrinkage is a real inventory event and its absence is a real limitation: there is currently no way to remove goods without charging them to 5000, so a loss is indistinguish- able from a sale. The posting it needs is a debit to a 5-series loss account against a credit to 1300, which means a fourth account and a fourth entrypoint. Deliberately deferred to keep the chart fixed at three for v1, and named here rather than discovered later.
- No revenue. A shipment books its COST, never a sale price. Revenue belongs to a receivables ledger (r/invoices) and tying the two together would require this realm to learn what was sold for how much.
- No period close. p/journal can seal a book up to a HEIGHT; sealing a derived book could only make the next derivation unpostable. The capability exists in the engine and is deliberately not exposed.
- No bulk receive or ship. One item per call — see the bound below.
- No truth. A sub-ledger is a set of claims by its owner. With no custody there is nothing tying "received 100 units at cost 5000" to a real holding. The invariants here prove that the postings follow from the stored lots; they prove nothing about the world.
Why the invariants are free reads, and what the bound costs
r/books exposes Rederive as an owner-only crossing function, to contain the cost of a full re-walk. This realm diverges, deliberately and for the same reason r/assets does: the derivation IS the product, and a claim only its owner can check is not a claim worth making. Rederive, ItemCrossFoot, the three register cross-foots and Render are all free reads, so any third party can verify a register from vm/qeval with no key.
MaxLotsPerItem is 64 against p/lots' own MaxLotsLimit of 1024, and the gap is not timidity. p/lots' audit left the cost of its O(live lots) walks explicitly UNPRICED and recorded that a realm rendering many pools must measure its own gas. This realm is that realm, so the figures are measured rather than reasoned about, under the pinned onyx toolchain.
There are TWO cost axes, not one, and this paragraph used to name only the first: MaxItemsPerRegister*MaxLotsPerItem live lots, and separately MaxEntriesPerRegister postings. It called 64 items of 64 live lots "the worst case this realm can hold", which is false — a register can sit at the lot cap and the entry cap AT ONCE. It was also claimed in review that the two are mutually exclusive by construction; that is wrong too. A one-unit FIFO shipment draws from the oldest lot, which holds 100 units, so 64 of them never exhaust it: 64 items x 64 receipts then 64 items x 64 one-unit shipments leaves 4096 live lots and exactly 8192 entries. The figures below were taken on a register asserted in-fixture to sit at all three caps simultaneously, so the combined shape is measured and not argued about.
Free reads, net of a trivial-read baseline on the same register:
Rederive 506,486,523 Render, register page 186,179,474 CrossFoot 96,213,741 Render, item page 6,467,621 Render, index 192,716
The dearest is 16.9% of the 3,000,000,000 maxGasQuery. The two axes do not compose in any single read's price, because no single read walks both: Rederive walks postings and reads no lot, while CrossFoot and Render walk lots and read three accounts plus EntryCount. Measured rather than inferred — Rederive at the lot cap agrees with the same read on a lot-cheap register (506,495,345) to 0.002%, which is what proves the lot axis does not price it. So the ceiling is the MAX of the two axes and never their product.
A bound is not a price, and one item per call is what keeps the write side cheap: Receive and Ship each touch exactly one pool. Measured on the same toolchain, net of the same kind of baseline:
Receive creating the 64th lot 930,676 Ship of all OnHand, dropping all 64 lots 11,837,593 Receive refused at the lot cap 304,454
The dearest write is 2.3% of the dearest read, so the read side is what the caps exist for. See DEPLOY_READINESS.md for the transcript, the per-header GAS pairing rule, and the two limits of these figures: they came from throwaway test files that were deleted rather than committed, so they are reproducible but NOT guarded by the suite; and gno test's GAS line covers a whole test function, so a figure is only a call's own price when the fixture that enables it lives in a separate test. Both halves bit: the refused receipt first measured at 53,123,019 — 174x its real price — because its 64-lot fixture shared its GAS line.
Deploy order
This realm imports p/journal and p/lots, so on any chain the order is p/journal, p/lots, then this realm. The order is stated and the deployment STATE of either import deliberately is not: a sentence about what is deployed is true at signing time at best, and addpkg freezes it — r/assets carries exactly that fossil. The import relation is the durable fact.
Imports
chain/runtimechain/runtime/unsafegno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journalgno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/lotsgno.land/p/nt/avl/v0gno.land/p/nt/markdown/sanitize/v0strconvstrings
Constants and variables
The fixed chart. The codes follow r/books' convention, where an account's LEADING DIGIT fixes its kind: 1 asset, 2 liability, 3 equity, 4 revenue, 5 expense. A six-series code would be an invalid account under that convention, which is why cost of goods sold is 5000 and not the 6000 some charts use.
const (
InventoryCode = "1300"
InventoryName = "Inventory"
COGSCode = "5000"
COGSName = "Cost of goods sold"
ContribCode = "3900"
ContribName = "Inventory contributed"
)
MaxEntriesPerRegister caps one owner's journal, far below journal's MaxEntriesLimit of 1000000 because Rederive is linear in the posting count and is a free read here.
A register that fills refuses further receipts and shipments rather than dropping history. There is deliberately no Archive — it would destroy the record the invariants are checked against — so the remedy is a second register under a second address, which is a real limitation and not a hidden one.
const MaxEntriesPerRegister = 8192
MaxItemNameLen bounds an item's human name. Every name is a candidate for Render output.
const MaxItemNameLen = 64
MaxItemsPerRegister caps one owner's SKU count. Every register-wide cross-foot walks every item's lots, and this bound is half of what keeps those reads affordable for everyone.
const MaxItemsPerRegister = 64
MaxLotsPerItem caps one item's LIVE lots, well below p/lots' MaxLotsLimit of 1024. It is the other half of the read bound: the worst case a register-wide walk must visit is MaxItemsPerRegister*MaxLotsPerItem lots.
Exhausted lots are dropped by p/lots, so this bounds concurrent lots and not the number of receipts an item may ever take. A WAVG item merges every receipt into one lot and so can never reach it — which means this is NOT the binding constraint for a WAVG item, and that item has a merge-overflow refusal of its own instead. See the package doc.
const MaxLotsPerItem = 64
MaxSKULen bounds a SKU, which is an avl key, a Render table cell and a Render path component all at once.
const MaxSKULen = 32
RealmPath is this realm's own path, used to build Render links.
const RealmPath = "/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/inventory"
RenderItemLimit caps how many items one register page tabulates.
const RenderItemLimit = 25
RenderLotLimit caps how many lots one item page tabulates.
const RenderLotLimit = 20
Functions
AccountKind
func AccountKind(owner address, code string) (string, bool)
AccountKind returns an account's kind as a word, and whether it exists.
AddFIFO
func AddFIFO(cur realm, sku, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAddFIFO(cross(cur), ...).
AddFIFO adds an item whose shipments are costed oldest-lot-first.
AddLIFO
func AddLIFO(cur realm, sku, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAddLIFO(cross(cur), ...).
AddLIFO adds an item whose shipments are costed newest-lot-first.
AddWAVG
func AddWAVG(cur realm, sku, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asAddWAVG(cross(cur), ...).
AddWAVG adds an item whose receipts are merged into a single re-anchored lot and whose shipments are costed at the blended unit cost.
A WAVG item holds exactly one lot however many receipts it takes, so it can never reach MaxLotsPerItem. It can still refuse a receipt: the MERGE has an overflow precondition the other two methods never evaluate, and it is a property of the pool's accumulated history rather than of the receipt. See the package doc; the practical consequence is that LotCapacity is not the figure to watch for a WAVG item.
AddedAt
func AddedAt(owner address, sku string) int64
AddedAt is the height an item was added at.
Balance
func Balance(owner address, code string) (int64, bool)
Balance returns an account's balance in its NORMAL direction and whether the code exists. All three of this realm's accounts are normal, so all three read non-negative.
COGSDebit
func COGSDebit(owner address) int64
COGSDebit is the 5000 balance: everything ever charged out as cost of goods sold.
Capacity
func Capacity(owner address) (itemsLeft, entriesLeft int)
Capacity reports how much room an owner's register has left.
Charged
func Charged(owner address, sku string) (qty, cost int64)
Charged is an item's cumulative shipped quantity and the cost charged out for it. The cost figure is derived: no caller has ever supplied it.
ContributedCredit
func ContributedCredit(owner address) int64
ContributedCredit is the 3900 balance. Read it as "goods arrived from somewhere this ledger does not model" — see the package doc on why 3900 is a plug this realm is forced to pick.
CostOnHand
func CostOnHand(owner address, sku string) int64
CostOnHand is the carrying value of an item's live lots: the figure this item contributes to the 1300 balance. Derived, for the same reason as OnHand.
CrossFoot
func CrossFoot(owner address) (inventoryAgrees, cogsAgrees, contributedAgrees, equationOffsets bool)
CrossFoot reports all four register-wide agreements as separate bools.
They are returned SEPARATELY and there is deliberately no single aggregate bool: a one-word verdict over four checks is the shape that gets quoted without its coverage, and a reader who wants the figures behind any one of these has CrossFootInventory, CrossFootCOGS, CrossFootContributed and Equation.
CrossFootCOGS
func CrossFootCOGS(owner address) (sumCharged, balance int64, agrees bool)
CrossFootCOGS is the register-wide check on 5000: the sum of every item's charged cost against the ledger balance.
CrossFootContributed
func CrossFootContributed(owner address) (sumReceived, balance int64, agrees bool)
CrossFootContributed is the register-wide check on 3900: the sum of every item's received cost against the ledger balance.
CrossFootInventory
func CrossFootInventory(owner address) (sumOnHand, balance int64, agrees bool)
CrossFootInventory is the register-wide check on 1300: the sum of every item's carrying value against the ledger balance.
The identity is EXACT, not approximate, and the reason is structural: this realm exposes no general posting entrypoint, so it is its book's only writer. Adding a general Post would destroy this check, which is the cost of adding one.
EntryCount
func EntryCount(owner address) int
EntryCount is how many entries an owner's book holds.
Equation
func Equation(owner address) (debitNormal, creditNormal int64, offsets bool)
Equation reports the accounting equation: debit-normal against credit-normal, and whether they offset. For this realm that is 1300 + 5000 against 3900, which offsets at 0 = 0 on an empty register and after every posting.
READ THE SIGN BEFORE PRESENTING EITHER FIGURE. journal partitions RAW DELTAS — debits minus credits — so creditNormal is NEGATIVE whenever 3900 carries a credit balance, and `offsets` means debitNormal + creditNormal == 0, not debitNormal == creditNormal. The textbook "assets = equity" has both sides positive; this does not. A caller rendering a balance sheet wants -creditNormal. The first test written against this function asserted the right magnitude with the wrong sign and was red for it, which is why this paragraph exists rather than a reader discovering it.
What `offsets` does NOT prove is journal's obligation 7, inherited here verbatim: the two subtotals are a partition of the same addends, so reclassifying every account in the chart changes both and leaves `offsets` true. A Kind set wrongly at Open time is invisible to it. What pins this realm's chart is the PAIR — debitNormal == InventoryDebit + COGSDebit and creditNormal == -ContributedCredit — and the suite asserts exactly that.
Sharper than that, because the partition is arithmetically inert: the two subtotals are added back together, so `offsets` reduces to Σ debits == Σ credits over the whole chart, which is the FIRST of the three conjuncts IsBalanced checks. A balanced book therefore always offsets, and `offsets` carries no fact IsBalanced has not already carried more strongly. The informative half of this function is the PAIR OF FIGURES, not the boolean — the figures move when a Kind changes and the boolean does not. (The two walks accumulate in different orders, so a mid-sum overflow could in principle separate them. At 8192 entries it would take amounts no register can reach, and it is not a second independent check.)
HasItem
func HasItem(owner address, sku string) bool
HasItem reports whether an owner's register holds a SKU.
HasRegister
func HasRegister(owner address) bool
HasRegister reports whether an address has opened a register.
Height
func Height() int64
Height is the current chain height, so a reader can date every other figure on this page without a second query.
Invariant
func Invariant(owner address, sku string) (posted, derived int64, agrees bool)
Invariant is the per-item derivation check, and it is this realm's central claim:
postedCOGS == pool.ChargedCost()
The left figure is what this realm posted to 5000 for the item; the right is what the pool says it charged. They are maintained by different code in different packages, so a false here means the realm posted an amount its own lots do not imply — which no code path in this realm can produce, and which is exactly why it is worth exporting. A claim is worth more checkable by a stranger than asserted in a comment.
The receipt side is checked the same way by InvariantReceipts.
InvariantReceipts
func InvariantReceipts(owner address, sku string) (posted, derived int64, agrees bool)
InvariantReceipts is Invariant for the receipt side:
postedRecv == pool.ReceivedCost()
InventoryDebit
func InventoryDebit(owner address) int64
InventoryDebit is the 1300 balance: the value of goods on hand per the ledger.
IsBalanced
func IsBalanced(owner address) bool
IsBalanced reports whether total debits equal total credits.
ItemCount
func ItemCount(owner address) int
ItemCount is how many items an owner's register holds.
ItemCrossFoot
func ItemCrossFoot(owner address, sku string) (received, charged, onHand int64, foots bool)
ItemCrossFoot is p/lots' own cost identity for one item:
receivedCost == chargedCost + costOnHand
Received and charged are counters; on-hand is a walk of the live lots. The identity therefore compares two independently maintained things, which is why p/lots keeps them apart.
ItemCrossFootQty
func ItemCrossFootQty(owner address, sku string) (received, consumed, onHand int64, foots bool)
ItemCrossFootQty is the quantity identity for one item:
receivedQty == consumedQty + qtyOnHand
It is not implied by ItemCrossFoot: costs and quantities can disagree independently, and a bug that moved cost without moving quantity would be invisible to the cost identity alone.
ItemName
func ItemName(owner address, sku string) string
ItemName is an item's human name.
LotAt
func LotAt(owner address, sku string, i int) (qty, cost, consumed, remaining, remainingCost int64, ok bool)
LotAt reports the i'th live lot of an item in receipt order, and whether i was in range. It returns primitives rather than a p/lots.Lot so that the figures are readable from vm/qeval without the caller knowing that type.
LotCapacity
func LotCapacity(owner address, sku string) int
LotCapacity reports how many further live lots one item can hold. A WAVG item merges into one lot, so its figure is a bound it will never approach — and reading headroom off it for a WAVG item is therefore reading the wrong limit, because the refusal that CAN bind there is a merge overflow this figure knows nothing about. See AddWAVG.
LotCount
func LotCount(owner address, sku string) int
LotCount is how many LIVE lots an item holds. Exhausted lots are dropped.
Method
func Method(owner address, sku string) string
Method is an item's costing method, as "fifo", "lifo" or "weighted-average".
OnHand
func OnHand(owner address, sku string) int64
OnHand is the quantity of an item on hand, DERIVED by walking its live lots rather than read from a counter — which is what makes ItemCrossFootQty a comparison rather than a tautology.
OpenRegister
func OpenRegister(cur realm)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asOpenRegister(cross(cur), ...).
OpenRegister creates the caller's register and opens its three accounts. It panics if the caller already has one.
The chart is opened HERE rather than on demand, and the book is created with a cap of exactly three accounts. Both facts are load-bearing: they are why this realm is its book's only writer and why the cross-foot identities are exact rather than approximate.
OpenedAt
func OpenedAt(owner address) int64
OpenedAt is the height an owner's register was opened at.
RawBalance
func RawBalance(owner address, code string) (int64, bool)
RawBalance returns debits minus credits regardless of kind. Summed over the three accounts it is exactly zero in a balanced book, which is what makes it the useful figure for a consistency check and the wrong one for a report.
Receipts
func Receipts(owner address, sku string) int64
Receipts is how many receipts an item has taken, including those whose lots are exhausted and dropped. It is NOT the live lot count — see LotCount.
Receive
func Receive(cur realm, sku string, qty, cost int64) uint64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asReceive(cross(cur), ...).
Receive takes qty units costing cost in total into the item's pool and posts debit 1300 / credit 3900, returning the entry id.
A cost of ZERO is accepted — free goods — and posts nothing; the returned id is then 0, which is not a valid entry id and is the signal that no entry was made. See the package doc.
Panics if the item does not exist, if qty is not positive, if cost is negative, if the pool's lot cap is reached, if a WAVG merge would breach the per-lot overflow precondition — a refusal that depends on the item's history and not on this receipt, see AddWAVG — or if the book is full. Every one of those aborts the whole transaction, which is what makes the pool-then-post order safe: an error return would NOT revert the pool update, a panic does.
Received
func Received(owner address, sku string) (qty, cost int64)
Received is an item's cumulative received quantity and cost.
Rederive
func Rederive(owner address) (debits, credits int64, agrees bool)
Rederive re-walks every posting in the book and reports whether the account totals agree with the journal.
It is a FREE read, unlike r/books' owner-only equivalent, for the reason given in the package doc. The cost is linear in the posting count, which is what MaxEntriesPerRegister bounds.
Registers
func Registers() int
Registers is how many registers exist across the realm.
Rename
func Rename(cur realm, sku, name string)
Crossing function. Callable from a transaction via
MsgCall, and from another realm asRename(cross(cur), ...).
Rename changes an item's human name. The SKU, the costing method and every lot are immutable; a name is presentation only and nothing derives from it.
Render
func Render(path string) string
Render shows the realm index at "/", one owner's register at "/<address>", and one item at "/<address>/<sku>".
SKUs
func SKUs(owner address, from string, limit int) []string
SKUs lists an owner's SKUs in key order, starting at or after from and returning at most limit of them. An empty from starts at the beginning.
This enumerator exists because its absence is a known defect elsewhere in this portfolio: r/books exposes no account enumerator, so a third party cannot discover what to ask it about without already knowing. A sub-ledger whose invariants are per-item is useless to a verifier who cannot list the items, so the list is part of the product rather than an afterthought.
A non-positive limit returns nothing rather than everything. The cap is the item cap, so a caller may always page the whole register.
Ship
func Ship(cur realm, sku string, qty int64) int64
Crossing function. Callable from a transaction via
MsgCall, and from another realm asShip(cross(cur), ...).
Ship consumes qty units from the item's pool and posts debit 5000 / credit 1300 for the cost it DERIVED, returning that cost.
The cost is never a parameter. It is whatever the item's costing method charges against the lots on hand, which is the whole product.
A derived cost of ZERO posts nothing — goods that cost nothing to acquire cost nothing to ship. The return value is then 0 and so is indistinguishable from "no entry made", deliberately: both facts are the same fact here.
Panics if the item does not exist, if qty is not positive, if qty exceeds the quantity on hand (p/lots fails closed with ErrInsufficient and consumes nothing), or if the book is full.
Totals
func Totals(owner address) (debits, credits int64)
Totals are the book's total debits and credits.
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| Imports | chain/runtime, chain/runtime/unsafe, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/lots, gno.land/p/nt/avl/v0, gno.land/p/nt/markdown/sanitize/v0, strconv, strings |
|---|---|
| First-party dependencies | gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal, gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/lots |
Known limitations
Recorded by the people who built and deployed it. This list is deliberately not empty where honesty costs something.
curated- THE DEPLOYED BYTES DIFFER FROM THE AUDITED/GATED COMMIT BY ONE DOC-COMMENT TOKEN, AND THE DEPLOYED BYTES ARE NOW THE COMMITTED ONES. The pre-deploy commit af363e8 gated inventory.gno at sha256 e43f390c...; the gno_addpkg body sent to the chain carried '[IsBalanced]' where that commit had '`IsBalanced`' on one line of the Equation doc comment -- a slip between two independent transcriptions (the /tmp copy that was hash-verified pre-broadcast was NOT the literal body sent, so the fidelity proof verified the wrong artifact). The post-broadcast vm/qfile byte-match CAUGHT it, which is exactly why that check exists. The deployed bytes are 91a2d2dc..., inside a // comment so compiled behaviour is provably identical, and the committed tree was reconciled to the chain and the full gate set re-run GREEN on the reconciled bytes (test 61/61, lint 0, fmt clean, sweep 141/0 FULLY COVERED, Gate A EQUIVALENT/8, Gate B EQUIVALENT/4). So committed == live and every behavioural verification transfers; what is lost is that the string the audit cited was e43f390c and the chain holds 91a2d2dc, differing only in that comment. Recorded in [an unpublished record] Group 17.
- THE INHERITED MALFORMED-Lot HAZARD IS NOT EXERCISABLE AT THIS BOUNDARY, BY STRUCTURE. p/lots' audit carried forward that realm-side rehydration is the one route to a malformed Lot. AUDIT.md Amendment B discharges it here: zero Lot constructions or field writes, *lots.Pool handles persisted directly with no serialize/rehydrate cycle, Pool's fields unexported with no setter, no exported function returning any lots.* type (LotAt returns primitives, Lots() returns copies), and the zero-value-Pool route absent. The hazard stays real for a FUTURE different consumer and stays documented in p/lots; this is a claim about one boundary verified on one tree.
- COST FIGURES ARE SELF-REPORTED AND NOTHING TIES ONE TO A HOLDING. Receive takes a qty and a cost from its caller and the realm custodies nothing, so 'received 100 units at cost 5000' is a claim by the register's owner and every derived COGS charge inherits that entirely while adding apparent authority. What this realm proves is arithmetic -- that the postings follow from the stored lots -- which is the whole product and is strictly less than truth. The realm says so in its own Render.
- 3900 IS A PLUG AND A REGISTER BOUGHT-ON-CREDIT UNDERSTATES LIABILITIES BY balance(3900). This is the design's one unavoidable assertion about the world, forced because a debit to 1300 needs a credit leg and every candidate asserts something unobservable. It is not a defect but it is a limitation on what the sub-ledger means, and no code path here can detect a register whose goods were in fact purchased on credit. Pairing with a real payables ledger is out of scope for v1.
- THE LIVE READS COVER AN EMPTY REGISTER ONLY. Registers() == 0 and the panic probe ran against a nonexistent register; no OpenRegister/AddFIFO/Receive/Ship was broadcast, so every costing-method branch, the cross-foots over populated items, the zero-cost skip, the WAVG merge-overflow refusal, and every panic but the no-register one are carried by the committed 61-test suite and the 141-case sabotage sweep under the pinned onyx toolchain, and by nothing on this chain. Skipped, not passed.
- THE GAS/READ FIGURES IN THE PACKAGE DOC CAME FROM DELETED THROWAWAY TEST FILES. The free-read figures (Rederive 506,486,523 at the triple cap, etc.) and the write figures are reproducible but NOT guarded by the committed suite, and gno test's GAS line covers a whole test function, so a figure is a call's own price only when its fixture lives in a separate test. Named in the doc and in DEPLOY_READINESS.md; the live register is far too small to measure any of them on chain.
- THE TWO IsCurrent() GUARDS ARE UNEXERCISED, here and anywhere. refuseCoins and mustOwnRegister read identity off a realm parameter through the AddFIFO/AddLIFO/AddWAVG -> addItem -> helper chain; every present caller forwards a crossing entrypoint's own current cur, so the guards are no-ops and the fixture that would fire them is a realm-to-realm caller forwarding a stale cur, which this harness cannot construct. They exist for a future caller and the realm is immutable, which is the whole reason they are present. AUDIT.md Amendment A.
- THE AUDIT'S FALSE-POSITIVE FILTER WAS SELF-ADMINISTERED. Task is unavailable inside a subagent, so the second pass was self-administered and the auditor said so. Mitigated, not discharged, by both candidates resting on an enumerable fact about the write surface rather than a severity judgement. AUDIT.md section 7.
- ONLY ONE OWNER EXISTS ON CHAIN. No register has been opened, so the per-owner partition (regs keyed by owner string, mustOwnRegister resolving cur.Previous().Address()) is proven by the committed tests alone. Authorization is structural -- no entrypoint takes an owner parameter -- so there is no address to forge, but 'untested on chain' is the honest word.
Source
| Source file | onyx/r/inventory/inventory.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 | yes — byte-identical |