Transaction

1964E07814B68B…4E063E4430BC

Block 410,016 · index 0 · indexed

Summary

Hash
1964E07814B68B53FB26A3502031E0A881A80FDC6D4C8C34565F4E063E4430BC
Block
410,016
Size
186000 bytes
Gas used
198,501,645 / 645,328,800
Fee
1935986ugnot
Status
success

Messages

#1AddPackagegno.land/p/moul/gnopm/v021 arguments
Attached funds
17000000ugnot

Arguments · 21

  1. #1gnopm
  2. #2README.md
  3. #3# `gno.land/p/moul/gnopm/v0` The domain model of a **source registry** for gno packages: a map from a deployed package path back to the repository, commit and directory that produced it. The realm half is [`gno.land/r/moul/gnopm/registry/v0`](../../../r/moul/gnopm/registry). This package is pure: no chain imports, no realm state, and the one piece of chain knowledge it needs (who holds a namespace) arrives as a function parameter. ## Why this has to exist at all A Go import path *is* a repository URL, so pkg.go.dev needs no registry. A gno import path is a chain address, so nothing anywhere says which source produced the bytes running at `gno.land/p/nt/tinyavl/v0`. gno gave up that link deliberately, and this is the part that has to be rebuilt by hand. ## What it can and cannot promise It cannot promise anything, and the whole design is built around saying so rather than around hiding it. Two claims look alike and are not the same: | claim | provable | |---|---| | this package came from that repository | **no.** Not here and not anywhere: anyone may deploy any bytes and claim any repository | | the deployed bytes equal the `addpkg` payload of that directory at that commit | **yes**, by hashing both sides. Off chain, by anything that can clone | | the claimant owns the path's namespace | **yes**, from chain data, recomputed on every read | So a `Claim` is testimony under a signature, and it is stored in exactly the shape the second row needs: path, repository, commit and directory are the four inputs to "hash the payload of that directory and compare it with what the chain hands back". This package does not answer the question. It makes the question answerable by something that can clone, which [gnopm](https://github.com/moul/gnopm) already does against a local tree (`gnopm verify -deployed`). A mismatch, when a verifier does find one, is **not** evidence of malice. The likeliest cause by far is a repository that moved on after a deploy, which is the normal state of most repositories most of the time. ## Open, and tagged Anyone may claim any path, including one they had nothing to do with. `Claim.OwnedBy` reports whether a claim comes from the party that controls the path, so the two can be told apart at render time. Gating registration on namespace ownership was the alternative and it cannot bootstrap: on day one almost nothing has been registered by its own deployer, so the gated registry is empty and teaches nobody anything. Open-and-labelled keeps the map fillable and moves the defence to the renderer, which is why `OwnedBy` exists and why the realm ranks on it rather than hiding anything. `OwnedBy` takes the name resolver as a parameter and the answer is never stored: a name can be transferred, and a stored answer would rot into exactly the kind of stale claim this package exists to distinguish from a live one. ## API ```go r := gnopm.New() // A claimant states where a package came from. Calling it again replaces that // claimant's own claim and nobody else's. c, err := r.Register(claimant, height, pkgPath, repo, commit, dir, ref) err = r.Withdraw(claimant, pkgPath) // your own claim, never anyone else's p := r.Package(pkgPath) // every claim about one path, or nil p.Claim(claimant) // one of them p.IterateClaims(func(c *gnopm.Claim) bool { ... }) c.OwnedBy(resolve) // does this come from the namespace holder c.SourceURL() // a browsable link to the claimed source ``` `Height` dates the claim and `UpdatedAt` dates the last edit. They are separate so a reader can tell a claim that has been kept current from one made once at deploy time and abandoned. ## Validation is an allowlist, on purpose Every stored field is charset-validated at write time: `ValidPkgPath`, `ValidRepo`, `ValidCommit`, `ValidDir`, `ValidRef`. Each is an allowlist, so a stored field cannot hold a backtick, a pipe, a bracket, an ASCII control character or a bidi override. That is not belt-and-braces, it is the escaping strategy. One claim is rendered in several places (a listing row, a table cell, an inline-code span, a link title) and a consumer that forgets to escape at any one of them has a hole. A field that can only hold safe bytes needs no escaping anywhere. Two consequences worth knowing before they surprise you: - **`ValidRepo` accepts `https://` and nothing else.** Not a taste judgement about git transports: the set of URL schemes that are safe to hand a browser is exactly one, and `javascript:` and `data:` are the attack. A host with a port is also refused, because the userinfo check (`https://github.com@evil.example/x` fetches from `evil.example` while reading as GitHub) needs the host to contain no `@` or `:`. - **`ValidRef` is narrower than git's own rule.** git rejects a handful of metacharacters and permits the rest, which would leave a ref free to carry a pipe or a backtick and undo the allowlist for every other field. What stays expressible is every ref anyone actually has. ## What is deliberately not here - **No verification.** A realm cannot clone a repository. See the realm README for where that half is meant to live. - **No network of trust, no voting, no curation.** Whose claim to believe is a judgement, and the place to make it is a renderer that can see who deployed the package, not a data structure. - **No version resolution.** `Version` reads a trailing `vN` and that is all. An unversioned path is legal and is not an error: gnoweb serves `gno.land/u/<name>` by calling the realm at exactly `/r/<name>/home`, so that one path can never carry a version. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/p/moul/gnopm/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/p/moul/gnopm/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4errors.gno
  5. #5package gnopm import "errors" // Stable, machine-readable error values. Callers (realms, clients, indexers) // switch on these rather than on message text: a realm turns them into panics, // and the panic string is the only thing a user ever sees. var ( ErrInvalidPkgPath = errors.New("gnopm: invalid package path") ErrInvalidRepo = errors.New("gnopm: invalid repository URL") ErrInvalidCommit = errors.New("gnopm: invalid commit id") ErrInvalidDir = errors.New("gnopm: invalid directory") ErrInvalidRef = errors.New("gnopm: invalid ref name") ErrClaimNotFound = errors.New("gnopm: no such claim") ErrTooManyClaimants = errors.New("gnopm: too many claimants for this package") )
  6. #6gnomod.toml
  7. #7module = "gno.land/p/moul/gnopm/v0" gno = "0.9"
  8. #8helpers_test.gno
  9. #9package gnopm // Fixtures shared by the tests in this package. // // addrNS is moul's mainnet address, used wherever a test needs a real bech32 // address rather than a plausible-looking string: the address-namespace rule // turns on the bech32 alphabet, so a made-up "g1aaa..." would pass or fail for // the wrong reason. const ( addrNS = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" alice = address("g1ubwjz3jmrjwmpuvrsjzgy5qkxf5wrgphzmd2tk") bob = address("g1e8vw6gh284q7ggzqs66ug0zvyf42tzsf5zdhsk") carol = address("g1v9kxjcm9ta047h6lta047h6lta047h6lz7gmv8") sha1Zero = "0000000000000000000000000000000000000000" sha1One = "1111111111111111111111111111111111111111" ) // resolver builds an r/sys/users stand-in from a name -> address table, so the // ownership rule is tested without importing a realm. func resolver(table map[string]address) func(string) (address, bool) { return func(name string) (address, bool) { a, ok := table[name] return a, ok } }
  10. #10pkgpath.gno
  11. #11package gnopm import "strings" // Parsing a gno package path far enough to know who owns it. // // A path is "<domain>/<kind>/<namespace>/<name...>", optionally ending in a // "/vN" version element: "gno.land/p/moul/md/v1", "gno.land/r/sys/users", // "gno.land/r/g1abc.../scratch/v0". The namespace is the third element and it // is the whole reason this file exists: it decides whether a claim comes from // the party that controls the path or from a stranger, which is the strongest // signal the registry can offer about a claim it can never itself verify. // SplitPkgPath breaks a package path into its domain, kind ("p" or "r"), // namespace and the remainder. ok is false for anything that is not shaped like // a publishable gno path. func SplitPkgPath(s string) (domain, kind, ns, rest string, ok bool) { if !ValidPkgPath(s) { return "", "", "", "", false } parts := strings.Split(s, "/") return parts[0], parts[1], parts[2], strings.Join(parts[3:], "/"), true } // Namespace returns the owning namespace of a package path, or "" if the path // is not one. func Namespace(s string) string { _, _, ns, _, ok := SplitPkgPath(s) if !ok { return "" } return ns } // Version returns the trailing "vN" element of a package path, or "" when the // path carries none. Absent is legal and is not an error: gnoweb serves // gno.land/u/<name> by calling the realm at exactly /r/<name>/home, so that one // path can never carry a version. func Version(s string) string { parts := strings.Split(s, "/") if len(parts) < 4 { return "" } last := parts[len(parts)-1] if len(last) < 2 || last[0] != 'v' { return "" } for i := 1; i < len(last); i++ { if last[i] < '0' || last[i] > '9' { return "" } } return last } // ValidPkgPath reports whether s is shaped like a publishable gno package path. // // It does NOT say the path exists on any chain, and cannot: a realm has no way // to ask whether some other path was ever deployed. That gap is the registry's // central honesty problem and is documented on Registry. func ValidPkgPath(s string) bool { if s == "" || len(s) > MaxPkgPathLen { return false } if strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") || strings.Contains(s, "//") { return false } parts := strings.Split(s, "/") if len(parts) < 4 { return false } // The domain, which is a host and so must carry a dot. if !strings.Contains(parts[0], ".") { return false } if parts[1] != "p" && parts[1] != "r" { return false } for _, p := range parts { if p == "" || p == "." || p == ".." { return false } for i := 0; i < len(p); i++ { if !pathByteOK(p[i]) { return false } } } return true } // AddressNamespace reports whether ns is shaped like a gno bech32 address, the // namespace every account owns without registering anything. A caller still // has to check it is the claimant's own address; this only says which of the // two ownership rules applies. func AddressNamespace(ns string) bool { if len(ns) != 40 || !strings.HasPrefix(ns, "g1") { return false } for i := 2; i < len(ns); i++ { if !strings.ContainsRune(bech32Charset, rune(ns[i])) { return false } } return true } // bech32Charset is bech32's data alphabet: lowercase alphanumerics minus the // four characters it drops to avoid transcription errors (1, b, i, o). const bech32Charset = "023456789acdefghjklmnpqrstuvwxyz"
  12. #12pkgpath_test.gno
  13. #13package gnopm import ( "testing" "gno.land/p/nt/uassert/v0" ) func TestValidPkgPath(t *testing.T) { cases := []struct { name string in string want bool }{ {"versioned package", "gno.land/p/moul/md/v1", true}, {"versioned realm", "gno.land/r/moul/gnopm/registry/v0", true}, {"unversioned realm", "gno.land/r/sys/users", true}, {"address namespace", "gno.land/r/" + addrNS + "/scratch/v0", true}, {"empty", "", false}, {"too few elements", "gno.land/p/moul", false}, {"domain without a dot", "gnoland/p/moul/md/v1", false}, {"unknown kind", "gno.land/x/moul/md/v1", false}, {"leading slash", "/gno.land/p/moul/md/v1", false}, {"trailing slash", "gno.land/p/moul/md/v1/", false}, {"double slash", "gno.land/p//moul/md", false}, {"parent traversal", "gno.land/p/moul/../md", false}, {"space", "gno.land/p/moul/m d", false}, } for _, tc := range cases { uassert.Equal(t, tc.want, ValidPkgPath(tc.in), tc.name) } } func TestSplitPkgPath(t *testing.T) { domain, kind, ns, rest, ok := SplitPkgPath("gno.land/r/moul/gnopm/registry/v0") uassert.True(t, ok, "splits") uassert.Equal(t, "gno.land", domain) uassert.Equal(t, "r", kind) uassert.Equal(t, "moul", ns) uassert.Equal(t, "gnopm/registry/v0", rest) _, _, _, _, ok = SplitPkgPath("nonsense") uassert.False(t, ok, "rejects a non-path") uassert.Equal(t, "moul", Namespace("gno.land/p/moul/md/v1")) uassert.Equal(t, "", Namespace("nonsense")) } func TestVersion(t *testing.T) { cases := []struct { in, want string }{ {"gno.land/p/moul/md/v1", "v1"}, {"gno.land/p/moul/md/v0", "v0"}, {"gno.land/p/moul/md/v10", "v10"}, {"gno.land/r/sys/users", ""}, // unversioned is legal {"gno.land/r/moul/home", ""}, // the one path that can never carry one {"gno.land/p/moul/md/vx", ""}, // not a number {"gno.land/p/moul/md/v", ""}, // no digits {"short", ""}, } for _, tc := range cases { uassert.Equal(t, tc.want, Version(tc.in), tc.in) } } func TestAddressNamespace(t *testing.T) { uassert.True(t, AddressNamespace(addrNS), "a real address") uassert.False(t, AddressNamespace("moul"), "a name") uassert.False(t, AddressNamespace(""), "empty") // b, i, o and 1 are exactly the characters bech32 drops. uassert.False(t, AddressNamespace("g1bbbfred47kzduec920z88wfr64ylksmdcedlf5"), "wrong length") }
  14. #14registry.gno
  15. #15// Package gnopm is the domain model of a source registry for gno packages: a // map from a deployed package path back to the repository, commit and directory // that produced it. // // It exists because gno deliberately broke the link Go gets for free. A Go // import path IS a repository URL, so pkg.go.dev needs no registry; a gno // import path is a chain address, so nothing anywhere says which source // produced the bytes running at gno.land/p/nt/tinyavl/v0. // // # What this can and cannot promise // // It cannot promise anything, and the design is built around saying so rather // than around hiding it. Two claims look alike and are not the same: // // 1. "this package came from that repository". UNPROVABLE, here or anywhere. // Anyone may deploy any bytes and anyone may claim any repository. No // registry design fixes this, and one that implies otherwise is worse than // none. // 2. "the bytes live at path P equal the addpkg payload of directory D in // repository R at commit C". Mechanically provable, by hashing both sides. // It is NOT provable here: a realm cannot clone a repository. It is // provable off chain by anything that can, and gnopm already does exactly // this against a local tree (`gnopm verify -deployed`). // // So this package stores (1), labelled as the claim it is, in the form (2) // needs to be checkable. A Claim is testimony under a signature. It becomes // evidence only once something outside the chain has gone and looked. // // # What a signature does buy // // The chain knows who signed a claim, and that is not nothing. A claim whose // claimant owns the package path's namespace comes from the party that controls // the path, which is the closest thing to a promise available without a // chain-level feature. OwnedBy reports that relationship; it is deliberately // computed on demand from a caller-supplied resolver and never stored, because // a name can be transferred and a stored answer would be a claim of its own. // // Anyone may still claim any path. That is the "open but tagged" choice, and it // is made on purpose: gating registration on namespace ownership would mean // that on day one, when almost nothing is registered by its own deployer, the // registry is empty and teaches nobody anything. The defence against a // misleading claim is that it renders as a stranger's claim, ranked below, and // never as a fact. package gnopm import ( "strings" "gno.land/p/nt/avl/v0" ) // Registry maps a package path to the claims made about its source. // // One path may carry several claims, at most one per claimant. That is not a // flaw to be resolved: a fork is a legitimate second answer, and so is a third // party filling in the map for a package whose author never will. type Registry struct { packages *avl.Tree // package path -> *Package claims int } // New returns an empty registry. func New() *Registry { return &Registry{packages: avl.NewTree()} } // Package is every claim made about one package path. type Package struct { PkgPath string claims *avl.Tree // claimant address string -> *Claim } // Claim is one address's statement about where a package came from. // // Every field except Claimant, Height and UpdatedAt is attacker-controlled. // They are charset-validated at write time rather than escaped at read time, so // that a consumer which forgets to escape is still safe: the fields cannot hold // a byte that means anything to markdown, to a terminal or to a URL parser. type Claim struct { PkgPath string Repo string // https:// URL of the repository Commit string // 40 or 64 lowercase hex Dir string // subdirectory holding the package, "" for the repo root Ref string // fully-qualified ref the commit was on, optional Claimant address Height int64 // block height of the first claim by this claimant // UpdatedAt is the height of the most recent write. Equal to Height until // the claimant re-registers, which is how a reader tells a claim that has // been kept current from one made once and abandoned. UpdatedAt int64 } // Register records or replaces claimant's claim about pkgPath. // // Re-registering the same path overwrites that claimant's own previous claim // and nobody else's, so the ordinary "I deployed a new commit" flow is one call // with no read first. Height is preserved across an update: it dates the claim, // not the edit. func (r *Registry) Register(claimant address, height int64, pkgPath, repo, commit, dir, ref string) (*Claim, error) { if !ValidPkgPath(pkgPath) { return nil, ErrInvalidPkgPath } if !ValidRepo(repo) { return nil, ErrInvalidRepo } if !ValidCommit(commit) { return nil, ErrInvalidCommit } if !ValidDir(dir) { return nil, ErrInvalidDir } if !ValidRef(ref) { return nil, ErrInvalidRef } p := r.Package(pkgPath) if p == nil { p = &Package{PkgPath: pkgPath, claims: avl.NewTree()} r.packages.Set(pkgPath, p) } key := claimant.String() c := &Claim{ PkgPath: pkgPath, Repo: repo, Commit: commit, Dir: dir, Ref: ref, Claimant: claimant, Height: height, UpdatedAt: height, } if prev := p.Claim(claimant); prev != nil { c.Height = prev.Height } else { if p.claims.Size() >= MaxClaimants { return nil, ErrTooManyClaimants } r.claims++ } p.claims.Set(key, c) return c, nil } // Withdraw removes claimant's own claim about pkgPath. A claimant can always // take back what they said; nobody can remove anyone else's. func (r *Registry) Withdraw(claimant address, pkgPath string) error { p := r.Package(pkgPath) if p == nil { return ErrClaimNotFound } if _, removed := p.claims.Remove(claimant.String()); !removed { return ErrClaimNotFound } r.claims-- if p.claims.Size() == 0 { r.packages.Remove(pkgPath) } return nil } // Package returns every claim about a path, or nil. func (r *Registry) Package(pkgPath string) *Package { v := r.packages.Get(pkgPath) if v == nil { return nil } return v.(*Package) } // Size is the number of package paths carrying at least one claim. func (r *Registry) Size() int { return r.packages.Size() } // Claims is the total number of claims across every path. func (r *Registry) Claims() int { return r.claims } // IteratePackages walks paths in lexical order, which groups a namespace's // packages together without needing a second index. The callback returns true // to STOP, following avl's own convention rather than inverting it here. func (r *Registry) IteratePackages(offset, count int, cb func(*Package) bool) { r.packages.IterateByOffset(offset, count, func(_ string, v any) bool { return cb(v.(*Package)) }) } // IterateNamespace walks every claimed path under "<domain>/<kind>/<ns>/". func (r *Registry) IterateNamespace(domain, kind, ns string, cb func(*Package) bool) { prefix := domain + "/" + kind + "/" + ns + "/" r.packages.Iterate(prefix, "", func(k string, v any) bool { if !strings.HasPrefix(k, prefix) { return true // past the prefix: stop } return cb(v.(*Package)) }) } // Claim returns claimant's claim about this package, or nil. func (p *Package) Claim(claimant address) *Claim { v := p.claims.Get(claimant.String()) if v == nil { return nil } return v.(*Claim) } // Count is how many addresses have claimed this package. func (p *Package) Count() int { return p.claims.Size() } // IterateClaims walks the claims in claimant-address order. Order is not // significance: a caller that wants the owner's claim first must ask OwnedBy, // because the registry has no opinion about which claim is true. func (p *Package) IterateClaims(cb func(*Claim) bool) { p.claims.Iterate("", "", func(_ string, v any) bool { return cb(v.(*Claim)) }) } // OwnedBy reports whether c comes from the party that controls the package // path's namespace: either the address whose own namespace it is, or whoever // resolve says holds the registered name. // // resolve maps a namespace name to the address holding it and whether that name // is held at all. It is a parameter rather than an import because this package // stays pure: the realm supplies r/sys/users, and a test supplies a table. // // This is the one label in the whole design that is derived rather than // declared, so it is computed here and never stored. A name can be transferred; // a stored answer would age into a lie of exactly the kind this package exists // to avoid. func (c *Claim) OwnedBy(resolve func(name string) (address, bool)) bool { _, _, ns, _, ok := SplitPkgPath(c.PkgPath) if !ok { return false } if AddressNamespace(ns) { return ns == c.Claimant.String() } if resolve == nil { return false } addr, held := resolve(ns) return held && addr == c.Claimant } // SourceURL builds a browsable link to the claimed source: the commit, plus the // directory when the package is not at the repository root. // // GitHub's "/tree/<commit>/<dir>" layout is also GitLab's, Gitea's, Forgejo's // and Codeberg's, so one construction covers every forge this registry is // likely to meet. It is a convenience, not a promise the link resolves: the // repository may be gone, private or renamed since the claim was made, which is // itself something a verifier reports rather than something a realm can know. func (c *Claim) SourceURL() string { u := strings.TrimSuffix(c.Repo, "/") + "/tree/" + c.Commit if c.Dir != "" { u += "/" + c.Dir } return u }
  16. #16registry_test.gno
  17. #17package gnopm import ( "testing" "gno.land/p/nt/uassert/v0" ) const pkgA = "gno.land/p/moul/md/v1" func TestRegisterAndRead(t *testing.T) { r := New() uassert.Equal(t, 0, r.Size()) uassert.Equal(t, 0, r.Claims()) c, err := r.Register(alice, 100, pkgA, "https://github.com/moul/gno-contracts", sha1Zero, "p/moul/md", "refs/heads/main") uassert.NoError(t, err) uassert.Equal(t, pkgA, c.PkgPath) uassert.Equal(t, int64(100), c.Height) uassert.Equal(t, int64(100), c.UpdatedAt) uassert.Equal(t, 1, r.Size()) uassert.Equal(t, 1, r.Claims()) p := r.Package(pkgA) uassert.True(t, p != nil, "package exists") uassert.Equal(t, 1, p.Count()) uassert.True(t, p.Claim(alice) != nil, "alice's claim is there") uassert.True(t, p.Claim(bob) == nil, "bob has not claimed") uassert.True(t, r.Package("gno.land/p/moul/nope/v0") == nil, "unclaimed path is nil") } // A claimant re-registering must move their own commit and nothing else, and // must not restart the clock: Height dates the claim, UpdatedAt dates the edit, // and telling them apart is how a reader spots a claim nobody has touched since // the first deploy. func TestReregisterUpdatesInPlace(t *testing.T) { r := New() r.Register(alice, 100, pkgA, "https://github.com/moul/gno-contracts", sha1Zero, "p/moul/md", "") c, err := r.Register(alice, 250, pkgA, "https://github.com/moul/gno-contracts", sha1One, "p/moul/md", "refs/tags/v1") uassert.NoError(t, err) uassert.Equal(t, sha1One, c.Commit, "commit moved") uassert.Equal(t, "refs/tags/v1", c.Ref, "ref moved") uassert.Equal(t, int64(100), c.Height, "height still dates the original claim") uassert.Equal(t, int64(250), c.UpdatedAt, "updated at dates the edit") uassert.Equal(t, 1, r.Claims(), "still one claim, not two") uassert.Equal(t, 1, r.Package(pkgA).Count()) } // Open but tagged: a second address may claim the same path. The registry has // no opinion about which is true, and storing both is the point. func TestSeveralClaimantsCoexist(t *testing.T) { r := New() r.Register(alice, 100, pkgA, "https://github.com/moul/gno-contracts", sha1Zero, "p/moul/md", "") r.Register(bob, 110, pkgA, "https://github.com/someone/fork", sha1One, "md", "") p := r.Package(pkgA) uassert.Equal(t, 2, p.Count()) uassert.Equal(t, 2, r.Claims()) uassert.Equal(t, 1, r.Size(), "still one package path") uassert.Equal(t, sha1Zero, p.Claim(alice).Commit) uassert.Equal(t, sha1One, p.Claim(bob).Commit) } func TestMaxClaimants(t *testing.T) { r := New() // Fill the path to the cap with distinct claimants. base := "g1ubwjz3jmrjwmpuvrsjzgy5qkxf5wrgphzmd2t" suffix := "023456789acdefgh" for i := 0; i < MaxClaimants; i++ { a := address(base + string(suffix[i])) _, err := r.Register(a, int64(100+i), pkgA, "https://example.com/x", sha1Zero, "", "") uassert.NoError(t, err, "claimant within the cap") } uassert.Equal(t, MaxClaimants, r.Package(pkgA).Count()) _, err := r.Register(carol, 999, pkgA, "https://example.com/x", sha1Zero, "", "") uassert.ErrorIs(t, err, ErrTooManyClaimants, "one past the cap") // An address already at the cap can still update: the cap bounds how many // claimants a path carries, never how often one of them corrects itself. existing := address(base + string(suffix[0])) _, err = r.Register(existing, 999, pkgA, "https://example.com/x", sha1One, "", "") uassert.NoError(t, err, "an existing claimant updates at the cap") } func TestWithdraw(t *testing.T) { r := New() r.Register(alice, 100, pkgA, "https://example.com/x", sha1Zero, "", "") r.Register(bob, 101, pkgA, "https://example.com/y", sha1One, "", "") uassert.ErrorIs(t, r.Withdraw(carol, pkgA), ErrClaimNotFound, "withdrawing what you never said") uassert.ErrorIs(t, r.Withdraw(alice, "gno.land/p/moul/nope/v0"), ErrClaimNotFound, "unknown path") uassert.NoError(t, r.Withdraw(alice, pkgA)) uassert.Equal(t, 1, r.Claims()) uassert.Equal(t, 1, r.Size(), "bob's claim keeps the path alive") uassert.True(t, r.Package(pkgA).Claim(alice) == nil, "alice is gone") uassert.True(t, r.Package(pkgA).Claim(bob) != nil, "bob is untouched") uassert.NoError(t, r.Withdraw(bob, pkgA)) uassert.Equal(t, 0, r.Claims()) uassert.Equal(t, 0, r.Size(), "the last withdrawal removes the path") uassert.True(t, r.Package(pkgA) == nil) } func TestRegisterRejectsInvalidInput(t *testing.T) { good := func() (string, string, string, string, string) { return pkgA, "https://github.com/moul/gno-contracts", sha1Zero, "p/moul/md", "refs/heads/main" } cases := []struct { name string mut func(p, repo, commit, dir, ref *string) want error }{ {"bad path", func(p, _, _, _, _ *string) { *p = "nonsense" }, ErrInvalidPkgPath}, {"bad repo scheme", func(_, repo, _, _, _ *string) { *repo = "javascript:alert(1)" }, ErrInvalidRepo}, {"abbreviated commit", func(_, _, c, _, _ *string) { *c = "0123456" }, ErrInvalidCommit}, {"absolute dir", func(_, _, _, d, _ *string) { *d = "/etc" }, ErrInvalidDir}, {"unqualified ref", func(_, _, _, _, rf *string) { *rf = "main" }, ErrInvalidRef}, } for _, tc := range cases { r := New() p, repo, commit, dir, ref := good() tc.mut(&p, &repo, &commit, &dir, &ref) _, err := r.Register(alice, 100, p, repo, commit, dir, ref) uassert.ErrorIs(t, err, tc.want, tc.name) uassert.Equal(t, 0, r.Claims(), tc.name+": nothing stored") } } func TestOwnedBy(t *testing.T) { names := resolver(map[string]address{"moul": alice}) r := New() c, _ := r.Register(alice, 100, pkgA, "https://example.com/x", sha1Zero, "", "") uassert.True(t, c.OwnedBy(names), "the namespace holder claiming their own package") c2, _ := r.Register(bob, 101, pkgA, "https://example.com/y", sha1One, "", "") uassert.False(t, c2.OwnedBy(names), "a stranger claiming someone else's package") // An address namespace belongs to that account with nothing registered, so // it resolves with no name lookup at all. own := "gno.land/r/" + addrNS + "/scratch/v0" c3, err := r.Register(address(addrNS), 102, own, "https://example.com/z", sha1Zero, "", "") uassert.NoError(t, err) uassert.True(t, c3.OwnedBy(nil), "an address namespace needs no resolver") c4, _ := r.Register(bob, 103, own, "https://example.com/w", sha1One, "", "") uassert.False(t, c4.OwnedBy(nil), "a stranger in someone's address namespace") // An unregistered name is held by nobody, so nobody owns it. other := "gno.land/p/nobody/thing/v0" c5, _ := r.Register(alice, 104, other, "https://example.com/v", sha1Zero, "", "") uassert.False(t, c5.OwnedBy(names), "an unheld namespace is owned by no one") uassert.False(t, c5.OwnedBy(nil), "and a nil resolver never invents ownership") // A resolver may return an address AND report the name as not held: that is // what r/sys/users does for a name whose record survives but is no longer // current. The address matching is not enough, the name has to still be // held, and dropping the second half of that test is a silent promotion of // a stranger's claim. stale := func(string) (address, bool) { return alice, false } uassert.False(t, c.OwnedBy(stale), "a resolved but unheld name confers nothing") } func TestSourceURL(t *testing.T) { r := New() c, _ := r.Register(alice, 100, pkgA, "https://github.com/moul/gno-contracts", sha1Zero, "p/moul/md", "") uassert.Equal(t, "https://github.com/moul/gno-contracts/tree/"+sha1Zero+"/p/moul/md", c.SourceURL()) c2, _ := r.Register(bob, 100, pkgA, "https://github.com/x/y/", sha1One, "", "") uassert.Equal(t, "https://github.com/x/y/tree/"+sha1One, c2.SourceURL(), "root package, trailing slash trimmed") } func TestIteration(t *testing.T) { r := New() paths := []string{ "gno.land/p/alice/one/v0", "gno.land/p/moul/md/v1", "gno.land/p/moul/ulist/v0", "gno.land/r/moul/home", "gno.land/p/zed/last/v0", } for i, p := range paths { _, err := r.Register(alice, int64(100+i), p, "https://example.com/x", sha1Zero, "", "") uassert.NoError(t, err, p) } var seen []string r.IteratePackages(0, 10, func(p *Package) bool { seen = append(seen, p.PkgPath) return false }) uassert.Equal(t, 5, len(seen)) uassert.Equal(t, "gno.land/p/alice/one/v0", seen[0], "lexical order") uassert.Equal(t, "gno.land/r/moul/home", seen[4], "r sorts after p") // A namespace walk must stop at the prefix boundary rather than running to // the end of the tree: "moul" is followed by "zed" and by the r/ half. var ns []string r.IterateNamespace("gno.land", "p", "moul", func(p *Package) bool { ns = append(ns, p.PkgPath) return false }) uassert.Equal(t, 2, len(ns), "only p/moul") uassert.Equal(t, "gno.land/p/moul/md/v1", ns[0]) uassert.Equal(t, "gno.land/p/moul/ulist/v0", ns[1]) // Stopping early stops. count := 0 r.IteratePackages(0, 10, func(p *Package) bool { count++ return true }) uassert.Equal(t, 1, count, "returning true stops the walk") }
  18. #18validate.gno
  19. #19package gnopm import "strings" // Size caps. Every string the chain stores is bounded: an unbounded field is an // unbounded storage deposit, and on gno.land the deposit is paid per byte by // whoever writes it. const ( MaxPkgPathLen = 256 MaxRepoLen = 512 MaxDirLen = 256 MaxRefLen = 255 // git's own limit for a single ref name // MaxClaimants bounds how many addresses may claim one package path. // Unbounded, it is a spam surface: one path with ten thousand claims is a // Render that never returns and a listing nobody can read. MaxClaimants = 16 ) // ValidCommit reports whether s is a git object id: 40 (SHA-1) or 64 (SHA-256) // lowercase hex characters. Case is fixed so the same object always produces // the same stored bytes, and so a verifier comparing two claims compares two // comparable strings. func ValidCommit(s string) bool { if len(s) != 40 && len(s) != 64 { return false } for i := 0; i < len(s); i++ { c := s[i] if (c < '0' || c > '9') && (c < 'a' || c > 'f') { return false } } return true } // ValidRepo reports whether s is a repository URL this registry accepts. // // Deliberately narrow: "https://" only, a host, and a path. Not a taste // judgement about git transports, a safety one. Whatever is stored here is // eventually rendered as a link by this realm, by gnoweb and by every explorer // that reads the registry, and the set of schemes that are safe to hand a // browser is exactly one. "javascript:", "data:" and "file:" are the attack; // "git://" and "ssh://" are merely unreachable from a web page, and a claimant // who needs one can point at the https mirror every forge already serves. func ValidRepo(s string) bool { if s == "" || len(s) > MaxRepoLen { return false } const scheme = "https://" if !strings.HasPrefix(s, scheme) { return false } rest := s[len(scheme):] if rest == "" || strings.HasPrefix(rest, "/") { return false } // A host must be present and must not be a userinfo trick // ("https://github.com@evil.example/x" fetches from evil.example while // reading as GitHub). slash := strings.IndexByte(rest, '/') host := rest if slash >= 0 { host = rest[:slash] } if host == "" || strings.ContainsAny(host, "@:") { return false } if !strings.Contains(host, ".") { return false } for i := 0; i < len(s); i++ { if !urlByteOK(s[i]) { return false } } return !strings.Contains(s, "..") } // urlByteOK is an allowlist, not a denylist: the printable ASCII that appears // in a real repository URL. Everything else, control characters and the bidi // overrides in particular, is refused rather than escaped, because a field that // can only hold safe bytes needs no escaping at any of its render sites. func urlByteOK(c byte) bool { switch { case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9': return true } switch c { case '-', '.', '_', '~', ':', '/', '%', '+': return true } return false } // ValidDir reports whether s names the subdirectory of the repository holding // the package. Empty means the repository root, which is the common case for a // single-package repo and must stay expressible. func ValidDir(s string) bool { if s == "" { return true } if len(s) > MaxDirLen { return false } if strings.HasPrefix(s, "/") || strings.HasSuffix(s, "/") { return false } for _, p := range strings.Split(s, "/") { if p == "" || p == "." || p == ".." { return false } for i := 0; i < len(p); i++ { if !pathByteOK(p[i]) { return false } } } return true } func pathByteOK(c byte) bool { switch { case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9': return true } switch c { case '-', '.', '_': return true } return false } // ValidRef reports whether s is a fully-qualified git ref, or empty. // // The ref is optional and is never the thing that is verified: a ref moves, a // commit does not. It is recorded so a reader can tell a claim pinned to a // released tag from one pinned to a commit on nobody's branch, and so a // verifier can check the claimed commit is still reachable from it rather than // dangling behind a force-push. func ValidRef(s string) bool { if s == "" { return true } if len(s) > MaxRefLen || !strings.HasPrefix(s, "refs/") { return false } if strings.Contains(s, "..") || strings.Contains(s, "@{") { return false } if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") { return false } parts := strings.Split(s, "/") if len(parts) < 2 { return false } for _, p := range parts { if p == "" || p == "@" || strings.HasPrefix(p, ".") || strings.HasSuffix(p, ".lock") { return false } for i := 0; i < len(p); i++ { if !refByteOK(p[i]) { return false } } } return true } // refByteOK is an allowlist, and deliberately narrower than git's own rule. // // git rejects a handful of metacharacters and permits everything else, which // leaves a ref free to contain a backtick, a pipe or a bracket. Every other // field here is an allowlist precisely so that no consumer has to escape // anything, and one denylist field would undo that for all of them: a ref // carrying a pipe breaks out of a markdown table cell, and one carrying a // backtick breaks out of the inline-code span a renderer wraps it in. // // The cost is refs nobody writes. What stays expressible is every ref anyone // actually has: alphanumerics, and the four separators git tooling puts in a // name. func refByteOK(c byte) bool { switch { case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9': return true } switch c { case '-', '.', '_', '/', '+': return true } return false }
  20. #20validate_test.gno
  21. #21package gnopm import ( "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestValidCommit(t *testing.T) { sha1 := strings.Repeat("a", 40) sha256 := strings.Repeat("0", 64) cases := []struct { name string in string want bool }{ {"sha1", sha1, true}, {"sha256", sha256, true}, {"mixed hex", "0123456789abcdef0123456789abcdef01234567", true}, {"empty", "", false}, {"too short", strings.Repeat("a", 39), false}, {"between the two lengths", strings.Repeat("a", 41), false}, {"uppercase", strings.Repeat("A", 40), false}, {"non hex", strings.Repeat("g", 40), false}, {"abbreviated", "0123456", false}, } for _, tc := range cases { uassert.Equal(t, tc.want, ValidCommit(tc.in), tc.name) } } func TestValidRepo(t *testing.T) { cases := []struct { name string in string want bool }{ {"github", "https://github.com/gnolang/gno", true}, {"no path", "https://example.com", true}, {"trailing slash", "https://example.com/x/", true}, {"port is not a host character here", "https://example.com:8443/x", false}, {"empty", "", false}, {"http", "http://github.com/gnolang/gno", false}, {"git scheme", "git://github.com/gnolang/gno", false}, {"ssh scheme", "ssh://git@github.com/gnolang/gno", false}, {"javascript", "javascript:alert(1)", false}, {"data url", "data:text/html,<script>", false}, {"scheme relative", "//github.com/gnolang/gno", false}, {"no host", "https:///gnolang/gno", false}, {"bare host with no dot", "https://localhost/x", false}, {"userinfo disguise", "https://github.com@evil.example/gnolang/gno", false}, {"parent traversal", "https://example.com/../x", false}, {"space", "https://example.com/a b", false}, {"markdown link break-out", "https://example.com/a)[x](y", false}, {"newline", "https://example.com/a\nb", false}, {"bidi override", "https://example.com/a‮b", false}, } for _, tc := range cases { uassert.Equal(t, tc.want, ValidRepo(tc.in), tc.name) } } func TestValidDir(t *testing.T) { cases := []struct { name string in string want bool }{ {"repo root", "", true}, {"one element", "examples", true}, {"nested", "examples/gno.land/p/nt/avl", true}, {"dots in a name", "p/moul.io/x", true}, {"absolute", "/examples", false}, {"trailing slash", "examples/", false}, {"empty element", "a//b", false}, {"dot element", "a/./b", false}, {"parent traversal", "a/../b", false}, {"backslash", "a\\b", false}, {"space", "a b", false}, } for _, tc := range cases { uassert.Equal(t, tc.want, ValidDir(tc.in), tc.name) } } func TestValidRef(t *testing.T) { cases := []struct { name string in string want bool }{ {"absent is legal", "", true}, {"branch", "refs/heads/main", true}, {"tag", "refs/tags/v1.2.0", true}, {"unqualified branch", "main", false}, {"double dot", "refs/heads/a..b", false}, {"reflog syntax", "refs/heads/a@{1}", false}, {"trailing slash", "refs/heads/", false}, {"lock suffix", "refs/heads/main.lock", false}, {"leading dot", "refs/heads/.hidden", false}, {"caret", "refs/heads/a^b", false}, {"space", "refs/heads/a b", false}, // Narrower than git on purpose: these are legal ref names that would // break out of a markdown table cell or an inline-code span, and every // other field in this package is an allowlist so that no consumer has // to escape anything. {"pipe breaks a table cell", "refs/heads/a|b", false}, {"backtick breaks a code span", "refs/heads/a`b", false}, {"brackets open a link", "refs/heads/a[b]c", false}, {"parens close a link", "refs/heads/a(b)c", false}, {"hash opens a heading", "refs/heads/a#b", false}, } for _, tc := range cases { uassert.Equal(t, tc.want, ValidRef(tc.in), tc.name) } }
#2AddPackagegno.land/p/moul/kit/num/v09 arguments
Attached funds
6000000ugnot

Arguments · 9

  1. #1num
  2. #2README.md
  3. #3# `gno.land/p/moul/kit/num` Numbers, formatted for a human to read: fixed-point amounts, percentages, zero padding. Third package of the `p/moul/kit/*` layer ([moul/gno-contracts#151](https://github.com/moul/gno-contracts/issues/151)), after [`kit/ui`](../ui) and [`kit/store`](../store). `kit` composes the existing packages, it does not replace them. ## Why it exists Unlike the rest of `kit`, this one is not deduplication. **Nothing in `p/moul` or `p/nt` turns 1234567 ugnot into `1.234567 GNOT`**, so every team that needed it wrote it. Measured 2026-09-28 across the 828 `.gno` files deployed on `gnoland-1` by somebody other than moul (`gnolang/tx-exports`, mainnet, genesis excluded): | who | what they wrote | copies | |---|---|---:| | the `bubblerumble` realms | `GNOT(ugnot int64)` | 5, byte-identical | | `kourt` | `ccAmt(n int64)` | 2 | | GnoSwap | `gnsDisplayScale` + `zeroPadUnsigned` + `p/gnoswap/utils.FormatUint` | 3 | | `bubbletreasury` | `one = int64(1_000_000)` | 1 | Five independent answers to "show money to a human". GnoSwap wrote a type switch over `any` to turn an integer into a string, because there was nothing to import. ## The two live implementations disagree on every input that is not positive Both were run verbatim, 2026-09-28, not read: | ugnot | `bubblerumble`'s `GNOT` | `kourt`'s `ccAmt` | correct | |---:|---|---|---| | 1234567 | `1.234567 GNOT` | `1.234567` | ✅ both | | 100 | `0.0001 GNOT` | `0.0001` | ✅ both | | **-100** | **`0.999 GNOT`** | `-0.0001` | sign dropped, digits wrong | | **-1500000** | **`-1. GNOT`** | `-1.5` | not a number | | **-1** | **`0.99999 GNOT`** | `-0.000001` | sign dropped, digits wrong | | **int64 min** | `-9223372036854.24192` | **infinite recursion** | one wrong, one halts | Two different failures from the same 8 lines: - `GNOT` computes `frac := ugnot % 1_000_000`, which is **negative** for a negative amount, then feeds it to a trick (`FormatInt(1_000_000+frac)[1:]`) that only works on a positive remainder. Below one GNOT the sign vanishes entirely and the digits become the ten's complement. - `ccAmt` handles the sign correctly and then recurses on `-n`. On `math.MinInt64`, `-n` is `math.MinInt64` again, still negative, so it calls itself forever. Measured: stack overflow. Neither is reachable from a user-supplied negative today as far as I could tell, and I did not try to prove reachability either way. The point is that this is 8 lines everyone rewrites, half of them get it wrong, and nobody ever tests the sign. ## This repo is not the exception, it is the sixth case Six more copies live here, in five more implementations, and they disagree with each other as well: | where | shape | `gnot(-1500000)` | |---|---|---| | [`r/moul/vesting`](../../../../r/moul/vesting/vesting.gno) | `u = -u`, never trims | `-1.500000 GNOT` | | [`r/moul/faucet`](../../../../r/moul/faucet/render.gno) | `u = -u`, trims | `-1.5` | | [`p/moul/x/storagecost`](../../x/storagecost/storagecost.gno) | `amount = -amount`, trims | `-1.5` | | [`r/moul/x/games/lastwords`](../../../../r/moul/x/games/lastwords/lastwords.gno) | no sign handling | **`-1.-5`** | | [`p/moul/x/wiki`](../../x/wiki/render.gno) | no sign handling | **`-1.-5 GNOT`** | | [`r/moul/x/daily/wrapped`](../../../../r/moul/x/daily/wrapped/wrapped.gno) | no decimals at all | n/a | `vesting` also pads to six digits where the other four trim, so the same amount reads `1.500000` in one realm and `1.5` in the next. All six are live on mainnet, so porting them is a bump each and a separate decision, not part of this package. ## What is here | | | |---|---| | `Dec(v, decimals)` | fixed point, trailing zeros trimmed. `Dec(1234500, 6)` is `1.2345` | | `DecFixed(v, decimals)` | same, padded to full width, for a column | | `GNOT(ugnot)` / `GNOTf(ugnot)` | `Dec(v, 6)`, without and with the unit | | `Pct(bps)` | basis points to a percentage. `Pct(1234)` is `12.34%` | | `Pad(v, width)` | `Pad(7, 3)` is `007`, because `ufmt` has no width flags | For a GRC20, pass the token's own decimals to `Dec`: six is GNOT's, not everyone's. ## The contract **Every function is total.** No input panics, including `math.MinInt64`, and no input returns a string that is not a number. `Dec` panics on a `decimals` outside `[0, 19]`, which is a programming error rather than a value. That contract is the package. It is pinned by `TestEveryDecimalStringParsesBack`, which parses every output back and checks the sign survived: swap in either live implementation and it goes red on four inputs, which is how it was verified rather than assumed. ## What is deliberately not here - **Thousands separators.** `1,234,567` belongs to [`p/moul/x/daily/humanize`](../../x/daily/humanize), which already owns `Comma`, `Bytes`, `Ordinal`, `Plural` and `Blocks`. One owner per fact. (Its `Comma` returns `--9,223,372,036,854,775,808` at the int64 minimum, the same `n = -n` shape as everything else on this page, and that is a bump for it rather than a reason to write a second one here.) - **Markdown.** `Dec` returns digits. Wrap it in [`kit/ui`](../ui) or [`p/moul/md`](../../md) if it needs to be bold or in a cell. - **Compact notation** (`1.2k`, `3.4M`). Nobody on chain has written one, so building it here would be speculative generality on a public path. - **avl keys.** `Pad` is for display. A key that must sort in insertion order belongs to [`kit/store`](../store), which has no ceiling to overflow. - **Parsing.** One direction only until something needs the other. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/p/moul/kit/num/v0" gno = "0.9"
  6. #6num.gno
  7. #7// Package num formats numbers for a human to read: fixed-point amounts, // percentages and zero padding. // // Thousands separators are NOT here. They belong to // [p/moul/x/daily/humanize](/p/moul/x/daily/humanize/v1), which already owns // Comma, Bytes, Ordinal, Plural and Blocks. // // It exists because nothing in p/moul or p/nt did. Measured across the 828 // .gno files deployed on gnoland-1 by someone other than moul, five // independent teams wrote their own ugnot-to-GNOT formatter, and the two that // are byte-level cousins disagree on every input that is not positive. The // README carries the full table. // // # The contract // // Every function here is total: no input panics, including the int64 minimum, // and no input returns a malformed string. That is the whole point of the // package, because it is precisely where the hand-rolled copies fail. // // # Where the decimals come from // // gno.land denominates GNOT in ugnot, one millionth of a GNOT, so [GNOT] is // [Dec] with decimals = 6. A GRC20 token declares its own decimals; pass that // to [Dec] rather than assuming six. package num import ( "strconv" "strings" ) // GnotDecimals is the number of decimal places between ugnot and GNOT. const GnotDecimals = 6 // maxDecimals caps the scale [Dec] accepts. An int64 holds at most 19 decimal // digits, so beyond that every value is pure fraction and the cap costs // nothing real while keeping the padding loop bounded. const maxDecimals = 19 // Dec renders v as a fixed-point decimal with the given number of places, // trailing zeros trimmed: Dec(1234500, 6) is "1.2345", Dec(2000000, 6) is "2". // // The sign is carried on the whole part, so Dec(-1, 6) is "-0.000001" and not // "0.999999". Dec panics only on a negative or out-of-range decimals, which is // a programming error rather than a value. func Dec(v int64, decimals int) string { return dec(v, decimals, false) } // DecFixed is [Dec] without the trailing-zero trim, so every value renders at // the same width: DecFixed(2000000, 6) is "2.000000". Use it in a column. func DecFixed(v int64, decimals int) string { return dec(v, decimals, true) } // GNOT renders an amount in ugnot as GNOT, with no unit: "1.234567". func GNOT(ugnot int64) string { return Dec(ugnot, GnotDecimals) } // GNOTf is [GNOT] with the unit appended: "1.234567 GNOT". func GNOTf(ugnot int64) string { return GNOT(ugnot) + " GNOT" } // Pct renders basis points as a percentage: Pct(1234) is "12.34%". // One basis point is a hundredth of a percent, which is the unit every fee on // this chain is already quoted in. func Pct(bps int64) string { return Dec(bps, 2) + "%" } // Pad renders v in base 10, left-padded with zeros to at least width // characters: Pad(7, 3) is "007". A negative value keeps its sign outside the // padding, so Pad(-7, 3) is "-007" and the string is width+1 long. // // It exists because ufmt supports no width flags: ufmt.Sprintf("%03d", 7) // returns "7", silently. // // Pad is for DISPLAY. For an avl key that must sort in insertion order, use // [p/moul/kit/store](/p/moul/kit/store/v0), which has no ceiling to overflow. func Pad(v int64, width int) string { neg, m := mag(v) s := strconv.FormatUint(m, 10) if n := width - len(s); n > 0 { s = strings.Repeat("0", n) + s } if neg { return "-" + s } return s } func dec(v int64, decimals int, fixed bool) string { if decimals < 0 || decimals > maxDecimals { panic("num: decimals out of range [0, 19]") } neg, m := mag(v) sign := "" if neg { sign = "-" } if decimals == 0 { return sign + strconv.FormatUint(m, 10) } digits := strconv.FormatUint(m, 10) if n := decimals + 1 - len(digits); n > 0 { digits = strings.Repeat("0", n) + digits } cut := len(digits) - decimals whole, frac := digits[:cut], digits[cut:] if !fixed { frac = strings.TrimRight(frac, "0") if frac == "" { return sign + whole } } return sign + whole + "." + frac } // mag splits v into a sign and an unsigned magnitude. // // -v on math.MinInt64 is math.MinInt64 again, so every shape that stays in // int64 is wrong there: `if v < 0 { v = -v }` silently keeps the value // negative, and a recursive `return "-" + f(-v)` never terminates at all. Both // are live on gnoland-1 today; the second halts the realm. // // `uint64(-v)` happens to give the right answer by twos-complement wraparound, // but it gets it from the overflow rather than despite it. -(v+1)+1 never // overflows, so it does not depend on that. func mag(v int64) (bool, uint64) { if v < 0 { return true, uint64(-(v+1)) + 1 } return false, uint64(v) }
  8. #8num_test.gno
  9. #9package num import ( "strconv" "testing" "gno.land/p/nt/uassert/v0" ) const minInt64 = -9223372036854775808 func TestDec(t *testing.T) { cases := []struct { name string v int64 decimals int want string }{ {"whole", 2000000, 6, "2"}, {"trimmed", 1234500, 6, "1.2345"}, {"full", 1234567, 6, "1.234567"}, {"zero", 0, 6, "0"}, {"sub-unit", 100, 6, "0.0001"}, {"smallest unit", 1, 6, "0.000001"}, {"no decimals", 42, 0, "42"}, {"negative whole", -2000000, 6, "-2"}, {"negative fraction", -1500000, 6, "-1.5"}, {"negative sub-unit", -100, 6, "-0.0001"}, {"negative smallest unit", -1, 6, "-0.000001"}, {"two decimals", 1234, 2, "12.34"}, {"int64 max", 9223372036854775807, 6, "9223372036854.775807"}, {"int64 min", minInt64, 6, "-9223372036854.775808"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { uassert.Equal(t, tc.want, Dec(tc.v, tc.decimals)) }) } } func TestDecFixedKeepsWidth(t *testing.T) { cases := []struct { v int64 want string }{ {2000000, "2.000000"}, {1234500, "1.234500"}, {0, "0.000000"}, {-1, "-0.000001"}, } for _, tc := range cases { uassert.Equal(t, tc.want, DecFixed(tc.v, 6)) } } // TestEveryDecimalStringParsesBack is the invariant the hand-rolled copies // break: whatever comes out must be a number again. "-1." and "0.999" for a // negative input both fail here, which is what the two live implementations // return. func TestEveryDecimalStringParsesBack(t *testing.T) { values := []int64{ 0, 1, -1, 100, -100, 999999, -999999, 1000000, -1000000, 1234567, -1234567, 9223372036854775807, minInt64, } for _, v := range values { for _, fixed := range []bool{false, true} { got := dec(v, 6, fixed) f, err := strconv.ParseFloat(got, 64) if err != nil { t.Errorf("dec(%d, 6, %t) = %q, not a number: %v", v, fixed, got, err) continue } if (v < 0) != (f < 0) && f != 0 { t.Errorf("dec(%d, 6, %t) = %q, sign flipped", v, fixed, got) } } } } // TestMagnitudeSurvivesTheInt64Minimum pins the one input that makes the // obvious implementations fail. `-v` on the minimum is the minimum again, so a // `if v < 0 { v = -v }` keeps it negative and a recursive `"-" + f(-v)` never // terminates. A mainnet realm does the second. func TestMagnitudeSurvivesTheInt64Minimum(t *testing.T) { neg, m := mag(minInt64) uassert.True(t, neg) uassert.Equal(t, "9223372036854775808", strconv.FormatUint(m, 10)) } func TestGNOT(t *testing.T) { uassert.Equal(t, "1.234567", GNOT(1234567)) uassert.Equal(t, "1.234567 GNOT", GNOTf(1234567)) uassert.Equal(t, "0 GNOT", GNOTf(0)) uassert.Equal(t, "-1.5 GNOT", GNOTf(-1500000)) } func TestPct(t *testing.T) { uassert.Equal(t, "12.34%", Pct(1234)) uassert.Equal(t, "1%", Pct(100)) uassert.Equal(t, "0.01%", Pct(1)) uassert.Equal(t, "0%", Pct(0)) uassert.Equal(t, "-12.34%", Pct(-1234)) } func TestPad(t *testing.T) { cases := []struct { v int64 width int want string }{ {7, 3, "007"}, {123, 3, "123"}, {1234, 3, "1234"}, {0, 2, "00"}, {7, 0, "7"}, {-7, 3, "-007"}, {minInt64, 3, "-9223372036854775808"}, } for _, tc := range cases { uassert.Equal(t, tc.want, Pad(tc.v, tc.width)) } } func TestDecRejectsAnImpossibleScale(cur realm, t *testing.T) { msg := "num: decimals out of range [0, 19]" uassert.PanicsWithMessage(t, cur, msg, func() { Dec(1, -1) }) uassert.PanicsWithMessage(t, cur, msg, func() { Dec(1, 20) }) uassert.NotPanics(t, cur, func() { Dec(1, 0) }) uassert.NotPanics(t, cur, func() { Dec(1, 19) }) }
#3AddPackagegno.land/p/moul/kit/tally/v09 arguments
Attached funds
6000000ugnot

Arguments · 9

  1. #1tally
  2. #2README.md
  3. #3# `gno.land/p/moul/kit/tally` Order a scoreboard: highest score first, ties broken by key. Fourth package of the `p/moul/kit/*` layer ([moul/gno-contracts#151](https://github.com/moul/gno-contracts/issues/151)), after [`ui`](../ui), [`store`](../store) and [`num`](../num). `kit` composes the existing packages, it does not replace them. ## Why it exists gno has **no `sort.Slice`**. Ordering anything therefore needs a named type with `Len`, `Less` and `Swap`, so every realm that shows a leaderboard writes one. Measured on `main`, 2026-09-28: **16 packages carry their own `sort.Interface` triple.** They are not all the same shape, so here is the honest split, by how many score clauses the comparator has before its final return: | shape | packages | fits this package? | |---|---|---| | **one score, then a tie-break** | `kudos`, `leaderboard`, `multiset`, `pixelcanvas`, `prorata`, `reactions`, `streak`, `tamagotchi` | **yes, 8 of the 16** | | two scores, then a tie-break | `collatz`, `idle`, `kingofdice`, `rpgroom`, `streaks`, `tipjar` | no | | not a scoreboard | `eggling`, `semverdemo` | no | So this package targets **8**, and it says so rather than claiming 16. A two-score board (`idle` ranks on prestige, then lifetime, then owner) has a genuinely domain-specific comparator, and flattening it into one `int64` would be a worse API than the triple it replaced. ## What the 16 disagree on: the tie The final clause of `Less` is what decides two equal scores, and it is the clause everyone writes differently: | final clause | realms | |---|---| | the key, as `addr.String()` | `idle`, `kingofdice`, `leaderboard`, `streaks`, `tamagotchi`, `tipjar` | | the key, as a bare string | `kudos`, `pixelcanvas`, `streak` | | a domain field (`Elem`, `N`, `emoji`, `index`) | `multiset`, `collatz`, `reactions`, `prorata` | | **the score again** | **`rpgroom`** | `rpgroom`'s third clause is `return r[i].Kills > r[j].Kills`, after `Level` and `XP`. That is **not a total order**: two heroes equal on all three compare `false` in both directions, so their order is whatever the sort algorithm did with the input. It is reproducible (`heroes.Iterate` walks an avl tree, and `sort.Sort` is deterministic), so this is not a consensus bug, but nobody chose that order and nothing pins it. `Sort` always ends on `Key`, so the answer is the same one every time and it is explicable to a reader. ## What is here | | | |---|---| | `Sort(entries)` | in place, highest first, ties by key | | `Top(entries, n)` | the n highest, **copied**, so the caller's slice is untouched | | `Rank(entries, key)` | 1-based, `0` if absent, and **never a tie**: equal scores still get distinct places | | `Board` | `NewBoard`, `Add`, `Set`, `Score`, `Has`, `Remove`, `Len`, `Entries`, `Top`, `Rank` | Two entry points on purpose. The 16 above already own their state and adopt by mapping it to `[]Entry`; a realm starting fresh reaches for `Board` and gets the storage too. `Board` is backed by an **avl tree, not a map**. gno map iteration order is unspecified, so a `Render` built by ranging a map is not reproducible. `TestBoardOrderDoesNotDependOnInsertion` pins it. ## What is deliberately not here - **Rendering.** A podium, a table and address shortening belong to [`kit/ui`](../ui). This package returns rows. - **Scores that are not `int64`.** Nothing on chain ranks by a float, and gno has no float determinism worth relying on for consensus output. - **Stable sort.** `Sort` is a total order, so stability is not observable. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/p/moul/kit/tally/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/p/moul/kit/tally/v0/deps.png) > ⚠️ **Disclaimer:** provided as-is, without warranty; not security-audited. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/p/moul/kit/tally/v0" gno = "0.9"
  6. #6tally.gno
  7. #7// Package tally orders a scoreboard: highest score first, ties broken by key. // // Sixteen realms in this repo carry their own sort.Interface triple, because // gno has no sort.Slice and every leaderboard therefore needs a named type // with Len, Less and Swap. Eight of them rank on ONE score and then break the // tie, which is what this package is; the rest rank on two scores and keep // their own comparator. They do not agree on what happens at a tie, and one of // them does not break ties at all. // // # The tie-break is the point // // A Render that reshuffles between identical calls is a bug, so the final // comparison must be a total order. Comparing the score again (as // r/moul/x/daily/rpgroom's third clause does) is not one: two rows with equal // scores compare false in both directions, and their order then falls out of // whatever the sort algorithm did with the input, which is reproducible but // nobody chose it. [Sort] always ends on the key, so equal scores render in a // stable, explicable order. // // # What is not here // // Storage: the caller keeps its own avl tree and maps it to [Entry]. That is // what makes this adoptable by realms that already have their state, and // [Board] is for new ones that do not. // // Rendering: a podium and a table already have an owner in // [p/moul/kit/ui](/p/moul/kit/ui/v0). package tally import ( "sort" "gno.land/p/nt/avl/v0" ) // Entry is one row of a scoreboard: an opaque key and its score. // // Key is whatever the caller ranks by, usually an address in its String form. // It is the tie-break, so it must be unique within one board. type Entry struct { Key string Score int64 } type byScore []Entry func (e byScore) Len() int { return len(e) } func (e byScore) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (e byScore) Less(i, j int) bool { if e[i].Score != e[j].Score { return e[i].Score > e[j].Score } return e[i].Key < e[j].Key } // Sort orders entries in place: highest score first, ties by key ascending. func Sort(entries []Entry) { sort.Sort(byScore(entries)) } // Top returns the n highest entries, sorted. A n at or below zero returns // nothing; a n past the end returns everything. The input is not modified. func Top(entries []Entry, n int) []Entry { if n <= 0 || len(entries) == 0 { return nil } out := make([]Entry, len(entries)) copy(out, entries) Sort(out) if n > len(out) { n = len(out) } return out[:n] } // Rank returns the 1-based position of key once entries are sorted, or 0 if // the key is absent. Equal scores still get distinct ranks, decided by the // same key tie-break Sort uses, so no two rows ever claim the same place. func Rank(entries []Entry, key string) int { sorted := make([]Entry, len(entries)) copy(sorted, entries) Sort(sorted) for i, e := range sorted { if e.Key == key { return i + 1 } } return 0 } // Board is a scoreboard that owns its storage, for a realm that does not // already have one. It is backed by a tree rather than a map: gno map // iteration order is unspecified, and a Render built on one is not // reproducible. type Board struct { scores *avl.Tree // key -> int64 } // NewBoard returns an empty Board. func NewBoard() *Board { return &Board{scores: avl.NewTree()} } // Add increases key's score by delta, which may be negative, and returns the // new score. A key that was absent starts at zero. func (b *Board) Add(key string, delta int64) int64 { n := b.Score(key) + delta b.scores.Set(key, n) return n } // Set replaces key's score outright. func (b *Board) Set(key string, score int64) { b.scores.Set(key, score) } // Score returns key's score, or zero if it has none. It does not distinguish // an absent key from one scoring zero; use [Board.Has] when that matters. func (b *Board) Score(key string) int64 { v := b.scores.Get(key) if v == nil { return 0 } return v.(int64) } // Has reports whether key has been scored at all. func (b *Board) Has(key string) bool { return b.scores.Get(key) != nil } // Remove drops key, reporting whether it was there. func (b *Board) Remove(key string) bool { _, removed := b.scores.Remove(key) return removed } // Len is the number of scored keys. func (b *Board) Len() int { return b.scores.Size() } // Entries returns every row, sorted by [Sort]. func (b *Board) Entries() []Entry { out := make([]Entry, 0, b.scores.Size()) b.scores.Iterate("", "", func(k string, v any) bool { out = append(out, Entry{Key: k, Score: v.(int64)}) return false }) Sort(out) return out } // Top returns the n highest rows, sorted. func (b *Board) Top(n int) []Entry { return Top(b.Entries(), n) } // Rank returns key's 1-based position, or 0 if it is not on the board. func (b *Board) Rank(key string) int { if !b.Has(key) { return 0 } return Rank(b.Entries(), key) }
  8. #8tally_test.gno
  9. #9package tally import ( "testing" "gno.land/p/nt/uassert/v0" ) func keys(entries []Entry) string { out := "" for i, e := range entries { if i > 0 { out += "," } out += e.Key } return out } func TestSortOrdersByScoreThenKey(t *testing.T) { cases := []struct { name string in []Entry want string }{ {"already ordered", []Entry{{"a", 3}, {"b", 2}, {"c", 1}}, "a,b,c"}, {"reversed", []Entry{{"c", 1}, {"b", 2}, {"a", 3}}, "a,b,c"}, {"all tied, key decides", []Entry{{"c", 5}, {"a", 5}, {"b", 5}}, "a,b,c"}, {"partial tie", []Entry{{"z", 1}, {"b", 9}, {"a", 9}}, "a,b,z"}, {"negatives sort below", []Entry{{"a", -1}, {"b", 0}, {"c", 1}}, "c,b,a"}, {"single", []Entry{{"only", 7}}, "only"}, {"empty", nil, ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { in := make([]Entry, len(tc.in)) copy(in, tc.in) Sort(in) uassert.Equal(t, tc.want, keys(in)) }) } } // TestSortIsATotalOrder is the defect this package exists to remove. A // comparator whose last clause repeats the score (rpgroom's third clause) is // not a total order: two equal rows compare false both ways and their order is // decided by the algorithm. Sorting twice from different inputs must agree. func TestSortIsATotalOrder(t *testing.T) { a := []Entry{{"x", 5}, {"y", 5}, {"z", 5}, {"w", 9}} b := []Entry{{"z", 5}, {"w", 9}, {"y", 5}, {"x", 5}} Sort(a) Sort(b) uassert.Equal(t, keys(a), keys(b)) uassert.Equal(t, "w,x,y,z", keys(a)) } func TestTop(t *testing.T) { in := []Entry{{"c", 1}, {"a", 3}, {"b", 2}} uassert.Equal(t, "a,b", keys(Top(in, 2))) uassert.Equal(t, "a,b,c", keys(Top(in, 10))) uassert.Equal(t, "", keys(Top(in, 0))) uassert.Equal(t, "", keys(Top(in, -1))) uassert.Equal(t, "", keys(Top(nil, 3))) } // TestTopDoesNotDisturbTheCaller pins that Top copies. A realm that renders a // Top(3) and then iterates its own slice must not find it reordered. func TestTopDoesNotDisturbTheCaller(t *testing.T) { in := []Entry{{"c", 1}, {"a", 3}, {"b", 2}} _ = Top(in, 2) uassert.Equal(t, "c,a,b", keys(in)) } func TestRank(t *testing.T) { in := []Entry{{"c", 1}, {"a", 3}, {"b", 2}} uassert.Equal(t, 1, Rank(in, "a")) uassert.Equal(t, 2, Rank(in, "b")) uassert.Equal(t, 3, Rank(in, "c")) uassert.Equal(t, 0, Rank(in, "absent")) } // TestRankNeverTies is what makes a rendered "#3" mean something: two equal // scores still get 2 and 3, never 2 and 2. func TestRankNeverTies(t *testing.T) { in := []Entry{{"a", 5}, {"b", 5}, {"c", 5}} seen := map[int]bool{} for _, e := range in { r := Rank(in, e.Key) if seen[r] { t.Errorf("rank %d claimed twice", r) } seen[r] = true } uassert.Equal(t, 3, len(seen)) } func TestBoardAddAndScore(t *testing.T) { b := NewBoard() uassert.Equal(t, 0, b.Len()) uassert.Equal(t, int64(5), b.Add("a", 5)) uassert.Equal(t, int64(8), b.Add("a", 3)) uassert.Equal(t, int64(6), b.Add("a", -2)) uassert.Equal(t, int64(6), b.Score("a")) uassert.Equal(t, int64(0), b.Score("never")) uassert.Equal(t, 1, b.Len()) } func TestBoardHasDistinguishesAbsentFromZero(t *testing.T) { b := NewBoard() b.Set("zero", 0) uassert.True(t, b.Has("zero")) uassert.False(t, b.Has("absent")) uassert.Equal(t, int64(0), b.Score("zero")) uassert.Equal(t, int64(0), b.Score("absent")) } func TestBoardRemove(t *testing.T) { b := NewBoard() b.Set("a", 1) uassert.True(t, b.Remove("a")) uassert.False(t, b.Remove("a")) uassert.Equal(t, 0, b.Len()) } func TestBoardEntriesAndTop(t *testing.T) { b := NewBoard() b.Set("c", 1) b.Set("a", 3) b.Set("b", 3) uassert.Equal(t, "a,b,c", keys(b.Entries())) uassert.Equal(t, "a,b", keys(b.Top(2))) uassert.Equal(t, 1, b.Rank("a")) uassert.Equal(t, 2, b.Rank("b")) uassert.Equal(t, 0, b.Rank("absent")) } // TestBoardOrderDoesNotDependOnInsertion is the reason Board is backed by a // tree and not a map: the same scores inserted in a different order must // render identically. func TestBoardOrderDoesNotDependOnInsertion(t *testing.T) { one := NewBoard() for _, e := range []Entry{{"a", 5}, {"b", 5}, {"c", 9}} { one.Set(e.Key, e.Score) } two := NewBoard() for _, e := range []Entry{{"c", 9}, {"b", 5}, {"a", 5}} { two.Set(e.Key, e.Score) } uassert.Equal(t, keys(one.Entries()), keys(two.Entries())) uassert.Equal(t, "c,a,b", keys(one.Entries())) }
Attached funds
5000000ugnot

Arguments · 9

  1. #1humanize
  2. #2README.md
  3. #3# `gno.land/p/moul/x/daily/humanize/v1` **Human-facing number formatting** — `Bytes`, `Comma`, `Ordinal`, `Plural`, `Blocks`, `Truncate`. ```go import "gno.land/p/moul/x/daily/humanize/v1" humanize.Bytes(1536) // "1.5 KiB" humanize.Comma(1234567) // "1,234,567" humanize.Ordinal(12) // "12th" — not "12nd" humanize.Plural(2, "mouse", "mice") // "2 mice" humanize.Blocks(1234) // "~1,200 blocks" humanize.Truncate("ééé", 2) // "é…" ``` **v1 fixes `Comma` at the int64 minimum.** `v0` negated in `int64`, and `-n` on `math.MinInt64` is `math.MinInt64`, so `FormatInt` emitted its own sign and the `neg` branch prepended a second: `Comma(math.MinInt64)` returned `--9,223,372,036,854,775,808`. The magnitude now goes through `uint64`. `v0` stays resolvable for its existing importers. **Integer-only, on purpose.** No floats anywhere: a rendered value that differed between nodes would be a consensus bug, so the single decimal place in `Bytes` comes from scaling by 10 and taking a remainder. **Durations are in blocks, never seconds.** There is no wall clock on chain, so "about 2 hours" is a lie dressed as precision. `Blocks` says `~1,200 blocks` and lets the caller decide what that means on their chain — and rounds deliberately, because false precision is worse than none. `Ordinal` handles the teens (`11th`/`12th`/`13th`, not `11st`/`12nd`/`13rd`) — the case naive implementations get wrong. `Truncate` counts **runes**, so multi-byte text is never sliced mid-character. **Live demo:** [`r/moul/x/daily/humanizedemo`](https://github.com/moul/gno-contracts/tree/main/r/moul/x/daily/humanizedemo) · render it at [`/r/moul/x/daily/humanizedemo/v0`](https://gno.land/r/moul/x/daily/humanizedemo/v0). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/p/moul/x/daily/humanize/v1" gno = "0.9"
  6. #6humanize.gno
  7. #7// Package humanize formats numbers for people rather than machines, as a pure, // reusable package: byte sizes, thousands separators, ordinals, pluralisation // and block-height "durations". // // Everything is integer-only. There are no floats here on purpose: gno has no // float determinism guarantees worth relying on for consensus output, and a // rendered value that differs between nodes would be a consensus bug. One // decimal place is produced by scaling by 10 and taking a remainder. // // Durations are expressed in BLOCKS, not seconds. There is no wall clock on // chain, so "about 2 hours" is a lie dressed as precision; this package says // "~1200 blocks" and lets the caller decide what that means on their chain. // // A live demo of this package is at // [r/moul/x/daily/humanizedemo](/r/moul/x/daily/humanizedemo/v0). package humanize import ( "strconv" "strings" ) // Bytes renders n bytes with SI-ish binary units and one decimal place. // Negative input is rendered with a leading minus rather than rejected. func Bytes(n int64) string { neg := n < 0 if neg { n = -n } const unit = 1024 if n < unit { s := strconv.FormatInt(n, 10) + " B" if neg { return "-" + s } return s } units := []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} div := int64(unit) i := 0 for n/div >= unit && i < len(units)-1 { div *= unit i++ } // one decimal place without floats: scale by 10, then split scaled := n * 10 / div whole, freq := scaled/10, scaled%10 s := strconv.FormatInt(whole, 10) if freq != 0 { s += "." + strconv.FormatInt(freq, 10) } s += " " + units[i] if neg { return "-" + s } return s } // Comma inserts thousands separators: 1234567 -> "1,234,567". func Comma(n int64) string { // The magnitude goes through uint64 rather than -n: -n on math.MinInt64 is // math.MinInt64 again, so FormatInt emits its own sign and the neg branch // below prepended a second one. v0 returned "--9,223,372,036,854,775,808". neg := n < 0 var m uint64 if neg { m = uint64(-(n+1)) + 1 } else { m = uint64(n) } s := strconv.FormatUint(m, 10) var b strings.Builder for i, c := range []byte(s) { if i > 0 && (len(s)-i)%3 == 0 { b.WriteByte(',') } b.WriteByte(c) } if neg { return "-" + b.String() } return b.String() } // Ordinal renders 1 -> "1st", 2 -> "2nd", 11 -> "11th". // // The teens are the trap: 11/12/13 take "th" despite ending in 1/2/3, so the // 11–13 case must be checked before the last digit. func Ordinal(n int64) string { s := strconv.FormatInt(n, 10) a := n if a < 0 { a = -a } if a%100 >= 11 && a%100 <= 13 { return s + "th" } switch a % 10 { case 1: return s + "st" case 2: return s + "nd" case 3: return s + "rd" } return s + "th" } // Plural returns "1 block" / "2 blocks", using plural when given, else word+"s". func Plural(n int64, word, plural string) string { if n == 1 || n == -1 { return strconv.FormatInt(n, 10) + " " + word } if plural == "" { plural = word + "s" } return strconv.FormatInt(n, 10) + " " + plural } // Blocks renders a block count at a coarse magnitude — deliberately vague, // because a block count is not a wall-clock duration. func Blocks(n int64) string { switch { case n < 0: return "in the past" case n == 0: return "now" case n < 10: return Plural(n, "block", "") case n < 1000: return "~" + Comma(n/10*10) + " blocks" default: return "~" + Comma(n/100*100) + " blocks" } } // Truncate shortens s to at most max runes, appending "…" when it cut. // Counts RUNES, not bytes, so a multi-byte string is not sliced mid-character. func Truncate(s string, max int) string { if max <= 0 { return "" } r := []rune(s) if len(r) <= max { return s } if max == 1 { return "…" } return string(r[:max-1]) + "…" }
  8. #8humanize_test.gno
  9. #9package humanize import ( "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestBytes(t *testing.T) { cases := []struct { in int64 want string }{ {0, "0 B"}, {999, "999 B"}, {1023, "1023 B"}, {1024, "1 KiB"}, {1536, "1.5 KiB"}, {1048576, "1 MiB"}, {1572864, "1.5 MiB"}, {1073741824, "1 GiB"}, {-1536, "-1.5 KiB"}, {-10, "-10 B"}, } for _, c := range cases { uassert.Equal(t, c.want, Bytes(c.in)) } } func TestComma(t *testing.T) { cases := []struct { in int64 want string }{ {0, "0"}, {1, "1"}, {999, "999"}, {1000, "1,000"}, {1234567, "1,234,567"}, {-1234567, "-1,234,567"}, } for _, c := range cases { uassert.Equal(t, c.want, Comma(c.in)) } } // The teens are the trap: 11/12/13 take "th" despite ending in 1/2/3. func TestOrdinalTeens(t *testing.T) { cases := []struct { in int64 want string }{ {1, "1st"}, {2, "2nd"}, {3, "3rd"}, {4, "4th"}, {11, "11th"}, {12, "12th"}, {13, "13th"}, {21, "21st"}, {22, "22nd"}, {23, "23rd"}, {111, "111th"}, {112, "112th"}, {113, "113th"}, {101, "101st"}, {0, "0th"}, } for _, c := range cases { uassert.Equal(t, c.want, Ordinal(c.in)) } } func TestPlural(t *testing.T) { uassert.Equal(t, "1 block", Plural(1, "block", "")) uassert.Equal(t, "2 blocks", Plural(2, "block", "")) uassert.Equal(t, "0 blocks", Plural(0, "block", "")) uassert.Equal(t, "2 mice", Plural(2, "mouse", "mice")) uassert.Equal(t, "-1 block", Plural(-1, "block", "")) } func TestBlocks(t *testing.T) { uassert.Equal(t, "now", Blocks(0)) uassert.Equal(t, "1 block", Blocks(1)) uassert.Equal(t, "9 blocks", Blocks(9)) uassert.Equal(t, "~10 blocks", Blocks(15)) uassert.Equal(t, "~1,200 blocks", Blocks(1234)) uassert.Equal(t, "in the past", Blocks(-5)) } // Truncate counts runes, so multi-byte text is never cut mid-character. func TestTruncateCountsRunes(t *testing.T) { uassert.Equal(t, "hello", Truncate("hello", 10)) uassert.Equal(t, "hell…", Truncate("hello!", 5)) uassert.Equal(t, "…", Truncate("hello", 1)) uassert.Equal(t, "", Truncate("hello", 0)) uassert.Equal(t, "éé", Truncate("éé", 2)) uassert.Equal(t, "é…", Truncate("ééé", 2)) // not sliced mid-rune } // TestCommaSurvivesTheInt64Minimum pins the defect v0 shipped. -n on // math.MinInt64 is math.MinInt64, so FormatInt emitted its own sign and the // neg branch prepended a second: v0 returned // "--9,223,372,036,854,775,808". Break the mag split and this goes red. func TestCommaSurvivesTheInt64Minimum(t *testing.T) { const minInt64 = -9223372036854775808 uassert.Equal(t, "-9,223,372,036,854,775,808", Comma(minInt64)) uassert.Equal(t, "9,223,372,036,854,775,807", Comma(9223372036854775807)) } // TestCommaNeverDoublesTheSign is the invariant behind it, stated so a future // rewrite cannot reintroduce the shape without failing. func TestCommaNeverDoublesTheSign(t *testing.T) { const minInt64 = -9223372036854775808 for _, n := range []int64{0, 1, -1, 999, -1000, 1234567, -1234567, minInt64} { s := Comma(n) if strings.HasPrefix(s, "--") || strings.Contains(s, "-,") { t.Errorf("Comma(%d) = %q", n, s) } if (n < 0) != strings.HasPrefix(s, "-") { t.Errorf("Comma(%d) = %q, sign disagrees with the input", n, s) } } }
#5AddPackagegno.land/p/moul/x/envelope/v09 arguments
Attached funds
6000000ugnot

Arguments · 9

  1. #1envelope
  2. #2README.md
  3. #3# `gno.land/p/moul/x/envelope/v0` **Reading and forwarding the coins a transaction attached to a call.** The `-send` field of a `MsgCall`, credited to the called realm's address before a line of its code runs. "Payable" is not a keyword in gno, it is a runtime fact: the VM rejects a `MsgCall` that carried coins the callee never looked at, and reading the envelope is what counts as looking. So every realm taking payment writes the same three things by hand. This is those three things. ```go import "gno.land/p/moul/x/envelope/v0" func Publish(cur realm, slug string) { envelope.RequireExactly(Price, 500) // aborts, naming the flag, if unpaid // ... } func Route(cur realm, to address) int64 { return envelope.ForwardAll(0, cur, to, Denom) // spends the envelope, nothing else } ``` | | | |---|---| | `Amount` · `All` · `IsEmpty` | read it, and make the call payable | | `Require` · `RequireAtLeast` · `RequireExactly` | read it and abort usefully when it is wrong | | `Only` | refuse an envelope carrying anything else | | `Forward` · `ForwardAll` | spend it, and nothing but it | | `BalanceOf` | what an address holds of a denom, from the bank | ## Why the abort messages are long `Require` aborts with `attach coins with -send <amount>/gno.land/r/...:coin`. The abort is the realm's user interface at the moment somebody got it wrong: "insufficient funds" sends them to their wallet, naming the flag sends them to the fix. `RequireExactly` refuses an **over**payment too. A realm that silently keeps the excess has invented a fee nobody agreed to, and one that refunds it has to move coins back out, which is a second failure mode. ## Two things that are not obvious **`Forward` is about who entered, not how deep you are.** It mints a `BankerTypeOriginSend` banker, which `NewBanker` allows only when `rlm.Previous().IsUserCall()`, and that is literally `pkgPath == ""`. So a realm may pass its `cur` down through as many of its own helpers as it likes (measured at three nested calls, through a function value, and through a closure), and it may **not** forward an envelope handed to it by another realm, nor be driven from `gnokey maketx run`, whose entry package is a code realm. **`Forward` is tested from a realm, not from here.** Minting the banker calls `rlm.Previous()`, and a `p/` package's test has no realm frame to walk: it dies with `frame not found: cannot seek beyond origin caller override`. The tests live in [`r/moul/x/nativeify`](https://gno.land/r/moul/x/nativeify/v0), which is also the realm that uses it. Related: a native coin is push-only, so the envelope is its entire inbound path. There is no allowance for one, which is what [`r/moul/x/nativeify`](https://gno.land/r/moul/x/nativeify/v0) is about. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/p/moul/x/envelope/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/p/moul/x/envelope/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4envelope.gno
  5. #5// Package envelope reads and forwards the coins a transaction attached to a // call: the `-send` field of a `MsgCall`, credited to the called realm's address // before a line of its code runs. // // It exists because "payable" is not a keyword in gno, it is a runtime fact. The // VM rejects a `MsgCall` that carried coins the callee never looked at // (gno.land/pkg/sdk/vm/keeper.go, `if !send.IsZero() && !*OriginSendObserved`), // and reading the envelope is what counts as looking. So every realm that // accepts payment writes the same three things by hand: read the amount, abort // with something the caller can act on when it is zero or the wrong denom, and // forward or keep it. This package is those three things. // // # Native coins are push-only, which is why this matters // // A realm cannot pull a native coin. `banker.SendCoins` panics unless the // `from` address is the banker's own realm address, so there is no allowance // and no `TransferFrom` for a native denom. The envelope is the entire inbound // path: either the holder signs a bank send, or they attach coins to a call and // the callee reads them here. // // That is also why a GRC20 needs `Approve` and a native coin does not. The GRC20 // grants standing authority to a spender; the envelope grants authority over one // message and expires with it. // // # Who may forward, which is not about depth // // [Forward] mints a `BankerTypeOriginSend` banker, and `NewBanker` accepts that // type only when `rlm.Previous().IsUserCall()` holds. `Realm.IsUserCall` is // literally `pkgPath == ""` (gnovm/stdlibs/chain/runtime/frame.gno), so the test // is about WHO entered the realm, not about how deep inside it you are: // // - `maketx call` straight into the realm: fine, and the realm may pass its // `cur` down through as many of its own helpers as it likes. Measured at // three nested calls, and through a function value and a closure. // - realm A calls realm B, and B tries to forward: refused. B's previous is a // code realm, so the envelope it was handed is not B's to route. // - `maketx run`: refused. The run package is a code realm // (`<domain>/e/<addr>/run`), so `IsUserCall` is false there too, and every // payable function is therefore unreachable from a run script. // // The reading helpers have none of these conditions: they go through // `unsafe.OriginSend()`, a context read that works anywhere, including inside a // `Render`. // package envelope import ( "chain" "chain/banker" "chain/runtime/unsafe" "gno.land/p/nt/ufmt/v0" ) // Amount returns how much of denom this message's envelope carried, and zero // when it carried none. Reading it is what makes the call payable. func Amount(denom string) int64 { return unsafe.OriginSend().AmountOf(denom) } // All returns the whole envelope, every denom in it. // // A call can be paid in several denoms at once, which is a property of the bank // rather than of any realm: a `Coins` set holds up to 256 of them. A realm that // only wants one should use [Only]. func All() chain.Coins { return unsafe.OriginSend() } // IsEmpty reports whether nothing was attached to this call. func IsEmpty() bool { return len(unsafe.OriginSend()) == 0 } // Require returns [Amount] and aborts when it is zero, with a message naming the // flag the caller left out. // // The message is the point. "insufficient funds" sends somebody to their wallet; // naming the denom and the flag sends them to the fix. func Require(denom string) int64 { amount := Amount(denom) if amount <= 0 { panic("this call must be paid: attach coins with -send <amount>" + denom) } return amount } // RequireAtLeast is [Require] with a floor, and names both numbers when the // envelope falls short. func RequireAtLeast(denom string, min int64) int64 { amount := Require(denom) if amount < min { panic(ufmt.Sprintf("this call costs at least %d%s, the envelope carried %d", min, denom, amount)) } return amount } // RequireExactly is [Require] for a fixed price, and refuses an overpayment as // well as an underpayment. // // Refusing to be overpaid is the unusual half, and it is deliberate: a realm // that silently keeps the excess has invented a fee nobody agreed to, and a // realm that refunds it has to move coins back out, which is a second failure // mode. Aborting costs the caller one transaction and nothing else. func RequireExactly(denom string, price int64) int64 { amount := Require(denom) if amount != price { panic(ufmt.Sprintf("this call costs exactly %d%s, the envelope carried %d", price, denom, amount)) } return amount } // Only is [Require] plus a refusal of anything else in the envelope. // // Use it wherever the realm has no code that would ever move a second denom, // because coins it does not handle are coins stranded at its address forever: // they are not refunded, and only the realm's own code can ever send them on. func Only(denom string) int64 { amount := Require(denom) for _, coin := range unsafe.OriginSend() { if coin.Denom != denom { panic(ufmt.Sprintf("this call takes %s only, the envelope also carried %d%s", denom, coin.Amount, coin.Denom)) } } return amount } // Forward sends coins from the calling realm's address to `to`, bounded by this // message's envelope. // // The banker it mints can spend the envelope and nothing else: not the realm's // own balance, not a previous message's payment. That is what makes a routing // function safe to write, and it is enforced by the VM rather than by the code // here (`ctx.OriginSend.IsAllGTE(spent)`). // // It must be reached from a call a USER made into this realm; see the package // doc for what that rules out. // // The leading `_ int` is not decoration: a non-realm package may not declare a // crossing function, so a `p/` helper that needs a realm handle threads it as a // later parameter. That is the same shape `p/nt/grc20`'s tellers use, and gno // refuses the file outright without it ("crossing function (realm first // argument) declared in non-realm package"). // // This function has no test in this package either, for the same reason it has // that signature: minting the banker calls `rlm.Previous()`, and a `p/` test has // no realm frame to walk ("frame not found: cannot seek beyond origin caller // override"). It is exercised from `r/moul/x/nativeify`'s tests instead. func Forward(_ int, rlm realm, to address, coins chain.Coins) { banker.NewBanker(banker.BankerTypeOriginSend, rlm).SendCoins(rlm.Address(), to, coins) } // ForwardAll forwards everything the envelope carried of denom, and returns it. // It aborts when the envelope carried none, like [Require]. func ForwardAll(_ int, rlm realm, to address, denom string) int64 { amount := Require(denom) Forward(0, rlm, to, chain.NewCoins(chain.NewCoin(denom, amount))) return amount } // BalanceOf returns what addr holds of denom right now, straight from the bank. // // It takes an address rather than a realm handle because it needs no authority // at all: a readonly banker is constructible from anywhere, including a Render. // Pass `cur.Address()` for the realm's own balance, envelope included. func BalanceOf(addr address, denom string) int64 { return banker.NewReadonlyBanker().GetCoin(addr, denom) }
  6. #6envelope_test.gno
  7. #7package envelope import ( "chain" "testing" "gno.land/p/nt/uassert/v0" ) const ( coin = "/gno.land/r/moul/x/moultest/v0:moultest" other = "ugnot" ) func pay(coins ...chain.Coin) { testing.SetOriginSend(chain.NewCoins(coins...)) } func TestReadsAnEmptyEnvelope(cur realm, t *testing.T) { pay() uassert.True(t, IsEmpty()) uassert.Equal(t, int64(0), Amount(coin)) uassert.Equal(t, 0, len(All())) uassert.PanicsContains(t, cur, "must be paid", func() { Require(coin) }) } func TestReadsAPaidEnvelope(cur realm, t *testing.T) { pay(chain.NewCoin(coin, 250)) uassert.False(t, IsEmpty()) uassert.Equal(t, int64(250), Amount(coin)) uassert.Equal(t, int64(250), Require(coin)) uassert.Equal(t, int64(0), Amount(other), "a denom that did not arrive reads zero") pay() } func TestFloorsAndPrices(cur realm, t *testing.T) { pay(chain.NewCoin(coin, 100)) uassert.Equal(t, int64(100), RequireAtLeast(coin, 100), "the floor is inclusive") uassert.Equal(t, int64(100), RequireAtLeast(coin, 99)) uassert.PanicsContains(t, cur, "costs at least 101", func() { RequireAtLeast(coin, 101) }) uassert.Equal(t, int64(100), RequireExactly(coin, 100)) uassert.PanicsContains(t, cur, "costs exactly 99", func() { RequireExactly(coin, 99) }, "being overpaid is refused too, not quietly kept") uassert.PanicsContains(t, cur, "costs exactly 101", func() { RequireExactly(coin, 101) }) pay() } func TestOnlyRefusesAMixedEnvelope(cur realm, t *testing.T) { pay(chain.NewCoin(coin, 100)) uassert.Equal(t, int64(100), Only(coin)) pay(chain.NewCoin(coin, 100), chain.NewCoin(other, 7)) uassert.Equal(t, int64(100), Amount(coin), "the mixed envelope still reads fine") uassert.Equal(t, int64(7), Amount(other)) uassert.PanicsContains(t, cur, "also carried 7ugnot", func() { Only(coin) }) // Only still requires the denom it was asked about, mixed or not. pay(chain.NewCoin(other, 7)) uassert.PanicsContains(t, cur, "must be paid", func() { Only(coin) }) pay() } func TestRequireNamesTheFlagToFix(cur realm, t *testing.T) { // The abort is the realm's user interface at the moment somebody got it // wrong, so it carries the flag and the denom, not a status code. pay() uassert.PanicsContains(t, cur, "-send <amount>"+coin, func() { Require(coin) }) }
  8. #8gnomod.toml
  9. #9module = "gno.land/p/moul/x/envelope/v0" gno = "0.9"
#6AddPackagegno.land/p/moul/x/games/accrual/v09 arguments
Attached funds
9000000ugnot

Arguments · 9

  1. #1accrual
  2. #2README.md
  3. #3# `gno.land/p/moul/x/games/accrual/v0` **Stock that fills at a rate while nobody is playing**: `New`, `Rate.Advance`, `Rate.At`, `Rate.Full`. ```go import "gno.land/p/moul/x/games/accrual/v0" r, _ := accrual.New(1, 60, 120) // 1 ore a minute, the mine holds 120 stock, anchor, _ := r.Advance(0, t, t+3599) // → 59, t+3540 (the anchor is NOT t+3599) stock, _ = r.At(stock, anchor, t+7200) // → 120, capped, and nothing was written full, _ := r.Full(0, t) // → t+7200, when production starts spilling ``` An idle game's resource field, a 4X's warehouse, a pet's hunger: all the same shape. Store what you had and when, compute the rest on demand, and a player who is away for a month costs the chain nothing. No cron, no keeper, no per-block hook. **The anchor does not advance to `now`, deliberately.** It advances by the whole periods it actually paid for, and the part-period in progress stays on the clock. That single decision is the package: > `Advance(s, a, c) == Advance(Advance(s, a, b), b, c)` for `a <= b <= c` How often you call must not change where you end up. Re-anchor to `now` instead and the division truncates, so every call forfeits the fraction it landed in. Run backwards on a decaying stat it stops being unfair and starts being free: `r/moul/x/daily/tamagotchi` decayed by `elapsed/2` and `elapsed/3`, so at `elapsed == 1` it decayed by nothing, and feeding once per block made the pet immortal while every other cadence died inside 240 blocks. Fixed in #221. The test here asserts the invariant over pseudo-random partitions, because hand-picked spans are exactly the ones a wrong implementation already passes: reverting the anchor fix turns four tests red. Three more behaviours worth knowing before you use it: - **`At` is `Advance` with the anchor discarded.** One implementation, two call sites. The same realm's second bug was a Render with its own copy of the decay, so the page showed a state the next call would not honour; two implementations of one rule means one of them is the stale one somebody acts on. - **A capped rate never returns `ErrOverflow`.** However long the player was away, the answer is the cap, so a realm can size a warehouse without also bounding its own lifetime. Only an uncapped rate can run out of `int64`, and it says so rather than wrapping into a negative stock. - **`now` before the anchor is an error, not zero elapsed.** Time running backwards means a stored anchor from another clock or a test that rewound. Swallowing it hides the bug for as long as the stock looks plausible. Everything is O(1) in elapsed time, which is a requirement rather than an optimisation: `Render` runs under a query gas limit and may not write, so the player who comes back after a month is exactly the one whose page would time out if the projection were iterative. Times are `int64` in the caller's unit. Prefer a timestamp over a block height: a height-denominated rate reprices itself every time the chain's block time moves, and gno.land's has moved from about 4.1s to 3.405s inside a month. Live demo: [r/moul/x/games/idle](/r/moul/x/games/idle/v0). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4accrual.gno
  5. #5// Package accrual is stock that fills at a rate while nobody is playing: the // resource field of an idle game, the warehouse of a 4X, anything whose state // is a function of how long it has been left alone. // // The shape is always the same. Store what you had and when, and compute the // rest on demand; a player who does nothing for a month costs nothing, and the // realm needs no cron, no keeper and no per-block hook. That much is obvious. // What is not obvious is that the obvious implementation is wrong in three // ways, each of which has been observed in this repository rather than // reasoned about. // // # 1. Re-anchoring to now makes acting more often pay // // Written naively, a claim advances the anchor to now and computes // elapsed/Period whole periods of production. The division truncates, so every // claim silently forfeits the part-period it lands in, and a player who claims // twice forfeits twice. Run in reverse on a decaying stat it is worse than // unfair, it is free: [r/moul/x/daily/tamagotchi] decayed by elapsed/2 and // elapsed/3, so at elapsed == 1 it decayed by nothing at all, and feeding once // per block made the pet immortal while every other cadence died inside 240 // blocks. It was fixed in #221. // // The invariant that rules it out is worth stating on its own, because it is // the whole contract of this package: // // Advance(s, a, c) == Advance(Advance(s, a, b), b, c) for a <= b <= c // // How often you call must not change where you end up. [Rate.Advance] gets // there by advancing the anchor only by the WHOLE periods it paid for, leaving // the remainder on the clock rather than throwing it away. TestSplitInvariant // asserts it over pseudo-random partitions rather than over a handful of // hand-picked spans, because the hand-picked spans are exactly the ones a // wrong implementation already passes. // // # 2. A view that does not share the write path's arithmetic drifts from it // // The same realm had a second copy of the decay for its Render, so the page // showed a pet the next call would not honour. Two implementations of one rule // means one of them is the stale one somebody acts on. Here there is one // function: [Rate.At] is [Rate.Advance] with the anchor discarded, so a view // cannot disagree with a write even in principle. // // # 3. A projection that is not closed form cannot be rendered at all // // Render runs under a query gas limit and may not write, so whatever it // computes has to be bounded however long the player was away. Anything // iterative, stepping a simulation once per block, is fine in a transaction // and unusable in a page: the player who comes back after a month is exactly // the one whose page times out. Everything here is O(1) in elapsed time, which // is the property that makes a live-updating page possible, not an // optimisation. // // Times are int64 and the unit is the caller's, block heights or unix seconds, // as long as it is consistent. Prefer a timestamp: a block-height rate drifts // in wall-clock terms every time the chain's block time moves, and gno.land's // has moved from about 4.1s to 3.405s inside one month. Nothing here reads the // chain, so a realm can test a year of its own economy without one. // // A game built on this package is at // [r/moul/x/games/idle](/r/moul/x/games/idle/v0). // // [r/moul/x/daily/tamagotchi]: /r/moul/x/daily/tamagotchi/v0 package accrual import "errors" const maxInt64 = int64(9223372036854775807) var ( // ErrBadAmount is returned when the amount produced per period is not // positive. A zero rate is a bug in the caller, not a valid still life: // it makes Full unanswerable and every Advance a no-op. ErrBadAmount = errors.New("accrual: amount must be positive") // ErrBadPeriod is returned when the period is not positive. ErrBadPeriod = errors.New("accrual: period must be positive") // ErrBadCap is returned when the cap is negative. Zero is legal and // means unbounded. ErrBadCap = errors.New("accrual: cap must not be negative") // ErrNegativeStock is returned when the stock handed in is negative, // which would let a caller mint by going through zero. ErrNegativeStock = errors.New("accrual: stock must not be negative") // ErrBackwards is returned when now falls before the anchor. Time moving // backwards is a caller bug (a stored anchor from another clock, a test // that rewound), and silently treating it as zero elapsed would hide it. ErrBackwards = errors.New("accrual: now must not fall before the anchor") // ErrOverflow is returned when the arithmetic would wrap int64. ErrOverflow = errors.New("accrual: int64 overflow") ) // Rate is production per unit of time, against a ceiling. Construct with New // so the invariants are checked once instead of on every call. type Rate struct { Amount int64 // produced each whole Period Period int64 // time units in one period Cap int64 // ceiling on the stock; zero means unbounded } // New validates and returns a Rate. A zero cap means unbounded. func New(amount, period, capacity int64) (Rate, error) { if amount <= 0 { return Rate{}, ErrBadAmount } if period <= 0 { return Rate{}, ErrBadPeriod } if capacity < 0 { return Rate{}, ErrBadCap } return Rate{Amount: amount, Period: period, Cap: capacity}, nil } // Advance returns the stock and the new anchor at time now, given stock held // since anchor. // // The new anchor is NOT now. It is the anchor plus the whole periods actually // paid out, so the part-period in progress stays on the clock and splitting a // span into pieces yields exactly what advancing it in one go would: // // Advance(s, a, c) == Advance(Advance(s, a, b), b, c) for a <= b <= c // // Production past the cap is lost, which is what a cap is for. Saturation is // idempotent, so it does not break the equality above, and it means a CAPPED // rate can never return ErrOverflow: the answer is the cap however long the // player was away. Only an uncapped rate can run out of int64. func (r Rate) Advance(stock, anchor, now int64) (int64, int64, error) { if err := r.check(); err != nil { return 0, 0, err } if stock < 0 { return 0, 0, ErrNegativeStock } if now < anchor { return 0, 0, ErrBackwards } // now-anchor wraps only when the anchor is negative and now is far // enough above it. maxInt64+anchor is safe to compute precisely because // anchor is negative there. if anchor < 0 && now > maxInt64+anchor { return 0, 0, ErrOverflow } periods := (now - anchor) / r.Period // The anchor only moves by what was paid for. periods*r.Period cannot // overflow, it is at most now-anchor, and adding it back cannot pass now. newAnchor := anchor + periods*r.Period if r.Cap > 0 && stock >= r.Cap { return stock, newAnchor, nil } // Production is only computed when its exact value can matter. A capped // rate whose payout would wrap int64 has an answer regardless of what // that payout is, so it gets one instead of an error. if periods > maxInt64/r.Amount { return r.saturate(newAnchor) } produced := periods * r.Amount if produced > maxInt64-stock { return r.saturate(newAnchor) } total := stock + produced if r.Cap > 0 && total > r.Cap { total = r.Cap } return total, newAnchor, nil } // saturate answers an accrual too large for int64: the cap if there is one, // and an error if there is not. A capped Rate therefore never overflows, // which is the property that lets a realm size a warehouse without also // having to bound its own lifetime. func (r Rate) saturate(newAnchor int64) (int64, int64, error) { if r.Cap == 0 { return 0, 0, ErrOverflow } return r.Cap, newAnchor, nil } // At is Advance with the anchor discarded: the read-only view of the same // arithmetic, for a Render that must not write. It is defined in terms of // Advance on purpose, so a page can never show a number a write would not // honour. func (r Rate) At(stock, anchor, now int64) (int64, error) { got, _, err := r.Advance(stock, anchor, now) return got, err } // Full returns the time at which the stock first reaches the cap, for a realm // that wants to tell a player when their production starts being wasted. // // An uncapped rate returns zero, meaning never. A stock already at or past the // cap returns the anchor, meaning now. func (r Rate) Full(stock, anchor int64) (int64, error) { if err := r.check(); err != nil { return 0, err } if stock < 0 { return 0, ErrNegativeStock } if r.Cap == 0 { return 0, nil } if stock >= r.Cap { return anchor, nil } // Whole periods needed to cover the shortfall, rounded UP: the cap is // reached by the payout that crosses it, not by the one before. need := r.Cap - stock periods := need / r.Amount if need%r.Amount != 0 { periods++ } if periods > maxInt64/r.Period { return 0, ErrOverflow } span := periods * r.Period if span > maxInt64-anchor { return 0, ErrOverflow } return anchor + span, nil } // check rejects a zero Rate built by struct literal instead of New, so a // caller that skipped the constructor gets the same error it would have. func (r Rate) check() error { if r.Amount <= 0 { return ErrBadAmount } if r.Period <= 0 { return ErrBadPeriod } if r.Cap < 0 { return ErrBadCap } return nil }
  6. #6accrual_test.gno
  7. #7package accrual import ( "testing" "gno.land/p/nt/uassert/v0" ) func mustNew(t *testing.T, amount, period, capacity int64) Rate { t.Helper() r, err := New(amount, period, capacity) uassert.NoError(t, err) return r } func TestNewValidation(t *testing.T) { cases := []struct { name string amount, period, capacity int64 want error }{ {"zero amount", 0, 60, 100, ErrBadAmount}, {"negative amount", -1, 60, 100, ErrBadAmount}, {"zero period", 1, 0, 100, ErrBadPeriod}, {"negative period", 1, -60, 100, ErrBadPeriod}, {"negative cap", 1, 60, -1, ErrBadCap}, {"zero cap is unbounded", 1, 60, 0, nil}, {"all positive", 5, 60, 100, nil}, } for _, tc := range cases { _, err := New(tc.amount, tc.period, tc.capacity) if tc.want == nil { uassert.NoError(t, err, tc.name) continue } uassert.ErrorIs(t, err, tc.want, tc.name) } } func TestAdvance(t *testing.T) { cases := []struct { name string amount, period, capacity int64 stock, anchor, now int64 wantStock, wantAnchor int64 }{ {"nothing elapsed", 1, 60, 0, 0, 1000, 1000, 0, 1000}, {"less than one period", 1, 60, 0, 0, 1000, 1059, 0, 1000}, {"exactly one period", 1, 60, 0, 0, 1000, 1060, 1, 1060}, {"one period and change keeps the change", 1, 60, 0, 0, 1000, 1119, 1, 1060}, {"ten periods", 3, 60, 0, 0, 1000, 1600, 30, 1600}, {"adds to what was held", 3, 60, 0, 7, 1000, 1600, 37, 1600}, {"saturates at the cap", 1, 60, 10, 0, 1000, 9000, 10, 8980}, {"already full produces nothing", 1, 60, 10, 10, 1000, 9000, 10, 8980}, {"over the cap is left alone", 1, 60, 10, 25, 1000, 9000, 25, 8980}, {"uncapped keeps going", 1, 60, 0, 0, 0, 604800, 10080, 604800}, } for _, tc := range cases { r := mustNew(t, tc.amount, tc.period, tc.capacity) gotStock, gotAnchor, err := r.Advance(tc.stock, tc.anchor, tc.now) uassert.NoError(t, err, tc.name) uassert.Equal(t, tc.wantStock, gotStock, tc.name+": stock") uassert.Equal(t, tc.wantAnchor, gotAnchor, tc.name+": anchor") } } // TestAnchorKeepsTheRemainder pins the one decision the whole package rests // on: the new anchor is not now. Advancing to a time halfway through a period // leaves that half on the clock, which is what makes splitting free. func TestAnchorKeepsTheRemainder(t *testing.T) { r := mustNew(t, 1, 60, 0) _, anchor, err := r.Advance(0, 0, 599) uassert.NoError(t, err) uassert.Equal(t, int64(540), anchor, "anchor must stop at the last whole period, not at now") } // TestSplitInvariant is the contract of this package: // // Advance(s, a, c) == Advance(Advance(s, a, b), b, c) // // It runs over pseudo-random partitions rather than hand-picked ones, because // an implementation that re-anchors to now passes every round span and only // fails on the ones that land mid-period. The generator is a fixed LCG, so a // failure is reproducible. func TestSplitInvariant(t *testing.T) { rates := []Rate{ mustNew(t, 1, 60, 0), // one a minute, unbounded mustNew(t, 7, 13, 0), // coprime, so remainders never line up mustNew(t, 1, 60, 500), // capped mustNew(t, 250, 3600, 0), // coarse period mustNew(t, 1, 1, 0), // degenerate: every tick pays mustNew(t, 3, 1000, 1234), // capped and coarse } seed := uint64(0x9E3779B97F4A7C15) next := func(n int64) int64 { seed = seed*6364136223846793005 + 1442695040888963407 return int64(seed>>33) % n } for _, r := range rates { for i := 0; i < 400; i++ { start := next(10000) span := next(100000) stock := next(600) end := start + span wantStock, wantAnchor, err := r.Advance(stock, start, end) uassert.NoError(t, err) // Cut the span into between one and six pieces and walk them. cuts := next(6) + 1 gotStock, gotAnchor := stock, start at := start for c := int64(0); c < cuts; c++ { remaining := end - at if remaining <= 0 { break } step := next(remaining + 1) if c == cuts-1 { step = remaining } at += step gotStock, gotAnchor, err = r.Advance(gotStock, gotAnchor, at) uassert.NoError(t, err) } // Land on the end whatever the cuts did. gotStock, gotAnchor, err = r.Advance(gotStock, gotAnchor, end) uassert.NoError(t, err) uassert.Equal(t, wantStock, gotStock, "split stock diverged") uassert.Equal(t, wantAnchor, gotAnchor, "split anchor diverged") } } } // TestClaimingEveryTickIsNotFree is the regression for the bug this package // exists to make unrepresentable. r/moul/x/daily/tamagotchi truncated its // per-call decay, so acting every tick cost nothing and acting rarely cost // everything. Here the two agree exactly. func TestClaimingEveryTickIsNotFree(t *testing.T) { r := mustNew(t, 1, 60, 0) const span = 6000 // 100 whole periods oneShot, _, err := r.Advance(0, 0, span) uassert.NoError(t, err) for _, cadence := range []int64{1, 2, 7, 60, 61, 599, 3000} { stock, anchor := int64(0), int64(0) for at := cadence; at <= span; at += cadence { stock, anchor, err = r.Advance(stock, anchor, at) uassert.NoError(t, err) } stock, _, err = r.Advance(stock, anchor, span) uassert.NoError(t, err) uassert.Equal(t, oneShot, stock, "cadence changed the payout") } } // TestAtMatchesAdvance pins that the read path cannot drift from the write // path, which was the second and quieter half of the tamagotchi bug. func TestAtMatchesAdvance(t *testing.T) { r := mustNew(t, 5, 17, 900) for now := int64(3); now < 4000; now += 37 { want, _, err := r.Advance(11, 3, now) uassert.NoError(t, err) got, err := r.At(11, 3, now) uassert.NoError(t, err) uassert.Equal(t, want, got, "At diverged from Advance") } } func TestAdvanceRejects(t *testing.T) { r := mustNew(t, 1, 60, 0) _, _, err := r.Advance(-1, 0, 100) uassert.ErrorIs(t, err, ErrNegativeStock) _, _, err = r.Advance(0, 100, 99) uassert.ErrorIs(t, err, ErrBackwards) // A Rate built by literal instead of New is still checked. _, _, err = Rate{}.Advance(0, 0, 100) uassert.ErrorIs(t, err, ErrBadAmount) _, _, err = Rate{Amount: 1}.Advance(0, 0, 100) uassert.ErrorIs(t, err, ErrBadPeriod) _, _, err = Rate{Amount: 1, Period: 1, Cap: -1}.Advance(0, 0, 100) uassert.ErrorIs(t, err, ErrBadCap) } func TestAdvanceOverflow(t *testing.T) { // An uncapped rate that would pass int64 refuses rather than wrapping // into a negative stock, which is how a large vesting grant once // reported a negative balance. r := mustNew(t, maxInt64/2, 1, 0) _, _, err := r.Advance(0, 0, 100) uassert.ErrorIs(t, err, ErrOverflow) // The same rate with a cap always has an answer, so it gives one // instead of an error, whichever of the two guards it trips. capped := mustNew(t, maxInt64/2, 1, 1000) got, anchor, err := capped.Advance(0, 0, 100) uassert.NoError(t, err) uassert.Equal(t, int64(1000), got) uassert.Equal(t, int64(100), anchor, "a saturated advance still moves the anchor") got, _, err = capped.Advance(999, 0, 1) uassert.NoError(t, err) uassert.Equal(t, int64(1000), got, "the sum guard saturates too") // A negative anchor far enough below now wraps the elapsed span itself. slow := mustNew(t, 1, maxInt64, 0) _, _, err = slow.Advance(0, -10, maxInt64-5) uassert.ErrorIs(t, err, ErrOverflow) } func TestFull(t *testing.T) { cases := []struct { name string amount, period, capacity int64 stock, anchor int64 want int64 }{ {"uncapped never fills", 1, 60, 0, 0, 1000, 0}, {"already full is now", 1, 60, 10, 10, 1000, 1000}, {"past full is now", 1, 60, 10, 99, 1000, 1000}, {"empty takes the whole cap", 1, 60, 10, 0, 1000, 1600}, {"partly full takes the rest", 1, 60, 10, 4, 1000, 1360}, {"rounds up to the payout that crosses", 3, 60, 10, 0, 0, 240}, {"exact division does not round up", 5, 60, 10, 0, 0, 120}, } for _, tc := range cases { r := mustNew(t, tc.amount, tc.period, tc.capacity) got, err := r.Full(tc.stock, tc.anchor) uassert.NoError(t, err, tc.name) uassert.Equal(t, tc.want, got, tc.name) } } // TestFullAgreesWithAdvance checks the two halves of the API against each // other: at the time Full names, the stock is at the cap, and one period // earlier it is not. func TestFullAgreesWithAdvance(t *testing.T) { for _, r := range []Rate{ mustNew(t, 1, 60, 10), mustNew(t, 3, 60, 10), mustNew(t, 7, 13, 1000), mustNew(t, 250, 3600, 875), } { full, err := r.Full(0, 0) uassert.NoError(t, err) atFull, err := r.At(0, 0, full) uassert.NoError(t, err) uassert.Equal(t, r.Cap, atFull, "not full at the time Full named") before, err := r.At(0, 0, full-r.Period) uassert.NoError(t, err) if before >= r.Cap { t.Errorf("already full a period early: %d >= %d", before, r.Cap) } } } func TestFullRejects(t *testing.T) { r := mustNew(t, 1, 60, 10) _, err := r.Full(-1, 0) uassert.ErrorIs(t, err, ErrNegativeStock) _, err = Rate{}.Full(0, 0) uassert.ErrorIs(t, err, ErrBadAmount) far := mustNew(t, 1, maxInt64/2, 1000) _, err = far.Full(0, 0) uassert.ErrorIs(t, err, ErrOverflow) }
  8. #8gnomod.toml
  9. #9module = "gno.land/p/moul/x/games/accrual/v0" gno = "0.9"
#7AddPackagegno.land/r/moul/x/across/v09 arguments
Attached funds
10000000ugnot

Arguments · 9

  1. #1across
  2. #2README.md
  3. #3# `gno.land/r/moul/x/across/v0` One address, read across fifteen realms that were already on chain. Open `/r/moul/x/across/v0:<address>` and the page answers with everything fifteen other realms will say about that address: token balances, an LP position, stake and unclaimed rewards, a check-in streak, dice rolled, a rate-limit bucket, a lottery entry, a reaction. Rows that came back empty are counted and hidden. ## What it is demonstrating Not one of the fifteen source realms was changed. None of them knows this realm exists, none of them registered anywhere, and there is no shared interface, no base package, no version handshake. The only thing every source had to do was **not** declare `private = true` in its `gnomod.toml`, which is what lets another realm import it at all. That is the composability claim in its weakest, most testable form: a realm's exported reads are a permanent public API, because the path it lives at can never be replaced. What you compile against today is what is on chain forever. ## What it cost Eight of the sources hold a per-address balance. They agree on nothing: | realm | signature | | --- | --- | | `r/moul/x/daily/erc20/v0`, `…/vestoken/v0`, `…/wrapped/v0` | `BalanceOf(address) uint64` | | `r/moul/x/pairs/aaa/v0`, `…/bbb/v0` | `BalanceOf(address) int64` | | `r/moul/x/grc20faucet/v0` | `BalanceOf(symbol string, owner address) int64` | | `r/moul/demo/wikicoin/v0` | `BalanceOf(address) uint` | | `r/moul/x/daily/dice/v0` | `RollsOf(addr string) []int` | Three integer widths, two signednesses, one that wants a symbol before the address, one that wants the address as a `string`. Composition still works, it just costs one hand-written adapter line per source. `Report` is those lines and nothing else, which is also why this realm ships no `p/` library: extract one and both halves are empty. ## The two sources that could not be read - **`r/moul/x/amm/v0`** exports `SharesOf(keyA, keyB string, owner address)`, which routes through a helper that **panics** on an unknown pool. A getter that panics is not composable: one panicking source takes down a page assembled from fifteen, and `recover()` does not catch a cross-realm abort. Only its `PoolCount()` is called from here. - **`r/moul/demo/vault/v0`** exports `MyBalance()` with no address parameter, so it answers for its caller, which from here is this realm. Neither is a bug. Both are what a getter looks like before anyone has tried to call it from another realm. ## Private by default, and why this one is too `private = true`, the repo default, because nothing needs to import *this* realm. The flag is a one-way door decided before the first publish, and it is the single thing that determines whether a realm can ever appear in a page like this one. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/across/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/across/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4across.gno
  5. #5// Package across reads one address across fifteen other realms that are // already live, and renders everything they will say about it on one page. // // It is a composability demo with a working product hiding inside it. The // realms it reads were written months apart, by nobody coordinating, and none // of them knows this one exists. Not a line of them changed to make this // possible: the only requirement any of them had to meet was NOT declaring // `private = true`, which is what lets another realm import it at all. // // # Why this realm holds no state and extracts no library // // Every other realm here splits into a pure `p/` and a thin `r/`. This one // cannot, and that is the point: its entire content is the wiring. Fifteen // import lines, one call each, and a table. Extract a library and what is left // on both sides is empty. // // # What it measures, besides balances // // The signatures below are the honest half of the demo. Eight of these realms // hold a per-address balance and they agree on nothing: // // erc20, vestoken, wrapped BalanceOf(address) uint64 // pairs/aaa, pairs/bbb BalanceOf(address) int64 // grc20faucet BalanceOf(symbol string, owner address) int64 // wikicoin BalanceOf(address) uint // dice RollsOf(addr string) []int // // Three integer widths, two signednesses, one that takes the address as a // string, one that needs a symbol first. Composition works anyway, because a // gno import is a type-checked contract with a permanent artifact behind it and // not a call convention anyone has to agree on in advance. It just costs one // hand-written adapter per source, which is what the report function is. // // # The two sources that could not be read // // - r/moul/x/amm/v0 exports SharesOf(keyA, keyB string, owner address), and // it routes through mustPool, which PANICS when the pool does not exist. // A getter that panics is not composable: fifteen sources on one page means // one panicking source takes the whole page down, and a wrapper cannot // recover, because recover() does not catch a cross-realm abort. So amm // contributes PoolCount() to the index and nothing to a report. // - r/moul/demo/vault/v0 exports MyBalance() with no address parameter. It // answers for whoever is calling, which from here is this realm, so the // value is structurally unreadable from outside. // // Neither is a bug in those realms. Both are what a getter looks like when // nobody has tried to call it from another realm yet. package across import ( "strconv" "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/r/moul/demo/wikicoin/v0" "gno.land/r/moul/reactions/v0" "gno.land/r/moul/x/amm/v0" "gno.land/r/moul/x/daily/dice/v0" "gno.land/r/moul/x/daily/erc20/v0" "gno.land/r/moul/x/daily/lottery/v0" "gno.land/r/moul/x/daily/ratelimitdemo/v0" "gno.land/r/moul/x/daily/staking/v0" "gno.land/r/moul/x/daily/streak/v0" "gno.land/r/moul/x/daily/vestoken/v0" "gno.land/r/moul/x/daily/wrapped/v0" "gno.land/r/moul/x/grc20faucet/v0" "gno.land/r/moul/x/pairs/aaa/v0" "gno.land/r/moul/x/pairs/aaabbb/v0" "gno.land/r/moul/x/pairs/bbb/v0" ) // reactionPage is the page key handed to r/moul/reactions. Reactions are keyed // on an arbitrary page string rather than on a realm, so a reader of this // report is asked about the same page /u/moul shows. const reactionPage = "gno.land/r/moul/home" // Self is this realm's own package path, exported so a caller can build a link // back without hardcoding the version. const Self = "gno.land/r/moul/x/across/v0" // A fact is one answer from one source realm about one address. // // zero is carried separately from value because "0" and "none" are the common // answer and the table is worth reading only for the rows that are not that. type fact struct { source string // package path, without the gno.land/ prefix what string value string zero bool } // Report collects what every wired source says about who. // // Every call in here is a plain exported read on another realm: no cur realm, // no cross, nothing written. A read with no realm parameter is borrowed rather // than entered (AGENTS.md), so none of these realms even observes a caller. func Report(who address) []fact { out := []fact{} add := func(source, what string, n int64, unit string) { v := strconv.FormatInt(n, 10) if unit != "" { v += " " + unit } out = append(out, fact{source: source, what: what, value: v, zero: n == 0}) } // Balances. Eight sources, five signatures. add("r/moul/x/daily/erc20/v0", "balance", int64(erc20.BalanceOf(who)), erc20.Symbol()) add("r/moul/x/daily/vestoken/v0", "liquid balance", int64(vestoken.BalanceOf(who)), vestoken.Symbol()) add("r/moul/x/daily/wrapped/v0", "wrapped balance", int64(wrapped.BalanceOf(who)), "WGNOT") add("r/moul/demo/wikicoin/v0", "balance", int64(wikicoin.BalanceOf(who)), "WIKI") add("r/moul/x/pairs/aaa/v0", "balance", aaa.BalanceOf(who), "AAA") add("r/moul/x/pairs/bbb/v0", "balance", bbb.BalanceOf(who), "BBB") add("r/moul/x/grc20faucet/v0", "balance", grc20faucet.BalanceOf("RED", who), "RED") add("r/moul/x/grc20faucet/v0", "balance", grc20faucet.BalanceOf("BLUE", who), "BLUE") // A grant is three numbers, so it does not fit the add helper. total, claimed, claimable := vestoken.GrantOf(who) out = append(out, fact{ source: "r/moul/x/daily/vestoken/v0", what: "vesting grant", value: strconv.FormatUint(total, 10) + " total, " + strconv.FormatUint(claimed, 10) + " claimed, " + strconv.FormatUint(claimable, 10) + " claimable", zero: total == 0, }) // Positions. add("r/moul/x/pairs/aaabbb/v0", "LP shares", aaabbb.SharesOf(who), "") add("r/moul/x/daily/staking/v0", "staked", int64(staking.StakedOf(who)), "") add("r/moul/x/daily/staking/v0", "unclaimed rewards", int64(staking.Earned(who)), "") // Activity. add("r/moul/x/daily/streak/v0", "current streak", int64(streak.Current(who)), "day(s)") add("r/moul/x/daily/streak/v0", "best streak", int64(streak.Best(who)), "day(s)") add("r/moul/x/daily/dice/v0", "dice rolled", int64(len(dice.RollsOf(who.String()))), "") add("r/moul/x/daily/ratelimitdemo/v0", "rate-limit tokens left", int64(ratelimitdemo.Tokens(who)), "") entered := "no" if lottery.HasEntered(who) { entered = "yes, round " + strconv.Itoa(lottery.CurrentRound()) } out = append(out, fact{ source: "r/moul/x/daily/lottery/v0", what: "entered the lottery", value: entered, zero: !lottery.HasEntered(who), }) // reactions answers with a palette key, already validated at write time, so // it never carries caller markdown into this table. react := reactions.ReactionOf(reactionPage, who) shown := react if shown == "" { shown = "none" } out = append(out, fact{ source: "r/moul/reactions/v0", what: "reaction on " + reactionPage, value: shown, zero: react == "", }) return out } // sourceRealms is every realm this one imports for its data, in import order. // It is the single place the count comes from: the index table has one row per // entry and TestSourcesCoverReport checks that Report names nothing else. // // Fifteen entries, which is the number the prose says. TestSourceCount pins it. var sourceRealms = []string{ "r/moul/x/daily/erc20/v0", "r/moul/x/daily/vestoken/v0", "r/moul/x/daily/wrapped/v0", "r/moul/demo/wikicoin/v0", "r/moul/x/pairs/aaa/v0", "r/moul/x/pairs/bbb/v0", "r/moul/x/grc20faucet/v0", "r/moul/x/pairs/aaabbb/v0", "r/moul/x/amm/v0", "r/moul/x/daily/staking/v0", "r/moul/x/daily/streak/v0", "r/moul/x/daily/dice/v0", "r/moul/x/daily/lottery/v0", "r/moul/x/daily/ratelimitdemo/v0", "r/moul/reactions/v0", } // Sources reports how many realms this one reads. func Sources() int { return len(sourceRealms) } func Render(path string) string { if path == "" { return renderIndex() } addr := address(path) if !addr.IsValid() { return "# across\n\n" + ui.Empty(ui.Inline(ui.Short(path))+" is not a valid address.") + "\n[back to the index](/r/moul/x/across/v0)\n" } return renderAddress(addr) } func renderAddress(who address) string { var b strings.Builder b.WriteString("# across: " + ui.AddrFull(who) + "\n\n") b.WriteString("What " + strconv.Itoa(Sources()) + " independently deployed realms say about this address. ") b.WriteString("None of them was changed to answer.\n\n") facts := Report(who) live := ui.NewTable("source", "fact", "value") quiet := 0 for _, f := range facts { if f.zero { quiet++ continue } live.Row("[`"+f.source+"`](/"+f.source+")", f.what, f.value) } if live.Len() == 0 { b.WriteString(ui.Empty("Every one of the " + strconv.Itoa(len(facts)) + " reads came back empty. This address has never touched any of them.")) b.WriteString("\n") } else { b.WriteString("## Answers\n\n") b.WriteString(live.String()) b.WriteString("\n") b.WriteString(strconv.Itoa(quiet) + " of " + strconv.Itoa(len(facts)) + " reads came back empty and are hidden.\n\n") } b.WriteString("[back to the index](/r/moul/x/across/v0)\n") return b.String() } func renderIndex() string { var b strings.Builder b.WriteString("# across\n\n") b.WriteString("One address, read across " + strconv.Itoa(Sources()) + " realms that are already on chain.\n\n") b.WriteString("Open `/r/moul/x/across/v0:<address>` to get the report. ") b.WriteString("Example: [/r/moul/x/across/v0:" + exampleAddr + "](/r/moul/x/across/v0:" + exampleAddr + ").\n\n") b.WriteString("## Why this realm exists\n\n") b.WriteString("A gno realm that does not declare `private = true` can be imported by any ") b.WriteString("other realm, forever, at a path that can never be replaced. That makes every ") b.WriteString("exported read on it a permanent public API. This realm is what happens when ") b.WriteString("somebody actually uses " + strconv.Itoa(Sources()) + " of them at once.\n\n") b.WriteString("Nothing here was negotiated. No registry, no interface, no shared base ") b.WriteString("package, no version handshake, and no change to any source realm.\n\n") b.WriteString("## The sources, and what each one is showing right now\n\n") t := ui.NewTable("realm", "global reading") t.Row("[`r/moul/x/daily/erc20/v0`](/r/moul/x/daily/erc20/v0)", erc20.Symbol()+" supply "+strconv.FormatUint(erc20.TotalSupply(), 10)) t.Row("[`r/moul/x/daily/vestoken/v0`](/r/moul/x/daily/vestoken/v0)", vestoken.Symbol()+" supply "+strconv.FormatUint(vestoken.TotalSupply(), 10)) t.Row("[`r/moul/x/daily/wrapped/v0`](/r/moul/x/daily/wrapped/v0)", "wrapped supply "+strconv.FormatUint(wrapped.TotalSupply(), 10)) t.Row("[`r/moul/demo/wikicoin/v0`](/r/moul/demo/wikicoin/v0)", "per-address balances only") t.Row("[`r/moul/x/pairs/aaa/v0`](/r/moul/x/pairs/aaa/v0)", "AAA supply "+strconv.FormatInt(aaa.TotalSupply(), 10)) t.Row("[`r/moul/x/pairs/bbb/v0`](/r/moul/x/pairs/bbb/v0)", "BBB supply "+strconv.FormatInt(bbb.TotalSupply(), 10)) t.Row("[`r/moul/x/grc20faucet/v0`](/r/moul/x/grc20faucet/v0)", "RED supply "+strconv.FormatInt(grc20faucet.TotalSupply("RED"), 10)+ ", BLUE supply "+strconv.FormatInt(grc20faucet.TotalSupply("BLUE"), 10)) t.Row("[`r/moul/x/pairs/aaabbb/v0`](/r/moul/x/pairs/aaabbb/v0)", "LP shares "+strconv.FormatInt(aaabbb.TotalShares(), 10)) t.Row("[`r/moul/x/amm/v0`](/r/moul/x/amm/v0)", strconv.Itoa(amm.PoolCount())+" pool(s), index only (see below)") t.Row("[`r/moul/x/daily/staking/v0`](/r/moul/x/daily/staking/v0)", "per-address stake and rewards") t.Row("[`r/moul/x/daily/streak/v0`](/r/moul/x/daily/streak/v0)", "day "+strconv.FormatInt(streak.Day(), 10)) t.Row("[`r/moul/x/daily/dice/v0`](/r/moul/x/daily/dice/v0)", strconv.Itoa(dice.Total())+" roll(s) all time") t.Row("[`r/moul/x/daily/lottery/v0`](/r/moul/x/daily/lottery/v0)", "round "+strconv.Itoa(lottery.CurrentRound())+", "+ strconv.Itoa(lottery.NumEntrants())+" entrant(s)") t.Row("[`r/moul/x/daily/ratelimitdemo/v0`](/r/moul/x/daily/ratelimitdemo/v0)", "per-address bucket") t.Row("[`r/moul/reactions/v0`](/r/moul/reactions/v0)", strconv.Itoa(reactions.Pages())+" page(s) reacted to") b.WriteString(t.String()) b.WriteString("\n") b.WriteString("## What it cost\n\n") b.WriteString("Eight of these hold a per-address balance, and they share no signature: ") b.WriteString("`uint64` three times, `int64` twice, `uint` once, one that wants a symbol ") b.WriteString("before the address, one that wants the address as a `string`. Every source ") b.WriteString("therefore needs its own adapter line. That is the whole cost, and it is paid ") b.WriteString("once by the reader rather than up front by fifteen authors.\n\n") b.WriteString("## The two that could not be read\n\n") b.WriteString("- `r/moul/x/amm/v0` routes `SharesOf` through a helper that panics on an ") b.WriteString("unknown pool. One panicking source takes down a page built from " + strconv.Itoa(Sources()) + ", and ") b.WriteString("`recover()` does not catch a cross-realm abort, so only its `PoolCount()` is ") b.WriteString("safe to call from here.\n") b.WriteString("- `r/moul/demo/vault/v0` exports `MyBalance()` with no address parameter. It ") b.WriteString("answers for its caller, which from here is this realm.\n\n") b.WriteString("Neither is a bug. Both are what a getter looks like before anyone has tried ") b.WriteString("to call it from another realm.\n") return b.String() } // exampleAddr is moul's address, used only to give the index a link that // resolves to a real report instead of an empty one. const exampleAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5"
  6. #6across_test.gno
  7. #7package across import ( "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/moul/x/daily/erc20/v0" "gno.land/r/moul/x/daily/streak/v0" "gno.land/r/moul/x/pairs/aaa/v0" "gno.land/r/moul/x/pairs/bbb/v0" ) // seeded is the address the mutation tests build state for. Everything after // TestReportReadsCrossRealmState sees whatever they left in the imported // realms, which is the whole point of the demo but also makes test order load // bearing: nothing below may assume a clean source realm for this address. var seeded = testutils.TestAddress("across-seeded") // fresh is never written to by anything here, so every source answers it with // its zero value. ExampleRender pins that page. var fresh = testutils.TestAddress("across-fresh") func TestSourceCount(t *testing.T) { // The package doc and the rendered prose both say fifteen. If this fails, // one of them is now a lie. uassert.Equal(t, 15, Sources()) } // Report must never name a realm that sourceRealms does not list, or the index // table silently stops covering what the report reads. func TestSourcesCoverReport(t *testing.T) { known := map[string]bool{} for _, s := range sourceRealms { known[s] = true } for _, f := range Report(fresh) { uassert.True(t, known[f.source], "report names an unlisted source: "+f.source) } } // Every source answers, and no source panics. The count is asserted rather // than the values, so this stays true as the sources gain state. func TestReportAnswersFromEverySource(t *testing.T) { facts := Report(fresh) uassert.Equal(t, 18, len(facts)) seen := map[string]bool{} for _, f := range facts { seen[f.source] = true } // amm is the one listed source that contributes to the index only. uassert.Equal(t, 14, len(seen)) uassert.False(t, seen["r/moul/x/amm/v0"], "amm must not be read per address") } // The load-bearing test: write to four realms that know nothing about this one, // then read all four back through Report. Break any of the four reads and this // goes red. func TestReportReadsCrossRealmState(cur realm, t *testing.T) { // SetRealm governs only the crossing calls made from this frame, so the // switch has to stay inline with the calls it is meant to cover. testing.SetRealm(testing.NewUserRealm(seeded)) erc20.Mint(cross(cur), seeded, 4_200) aaa.Faucet(cross(cur)) bbb.Faucet(cross(cur)) days := streak.CheckIn(cross(cur)) uassert.Equal(t, 1, days, "first check-in is day 1") got := map[string]string{} for _, f := range Report(seeded) { if f.zero { continue } got[f.source+" "+f.what] = f.value } uassert.Equal(t, "4200 "+erc20.Symbol(), got["r/moul/x/daily/erc20/v0 balance"]) uassert.Equal(t, "1 day(s)", got["r/moul/x/daily/streak/v0 current streak"]) uassert.Equal(t, "1 day(s)", got["r/moul/x/daily/streak/v0 best streak"]) uassert.True(t, got["r/moul/x/pairs/aaa/v0 balance"] != "", "aaa faucet must show up") uassert.True(t, got["r/moul/x/pairs/bbb/v0 balance"] != "", "bbb faucet must show up") } // The rendered page for the seeded address must carry the same four facts, and // must hide the reads that came back empty rather than printing a wall of zeros. func TestRenderAddressShowsOnlyLiveFacts(t *testing.T) { out := Render(seeded.String()) uassert.True(t, strings.Contains(out, "4200 "+erc20.Symbol()), "erc20 balance on the page") uassert.True(t, strings.Contains(out, "current streak"), "streak on the page") uassert.True(t, strings.Contains(out, "r/moul/x/pairs/aaa/v0"), "aaa on the page") uassert.True(t, strings.Contains(out, "reads came back empty and are hidden"), "quiet rows hidden") uassert.False(t, strings.Contains(out, "vesting grant"), "an empty grant must not render") } // An address-shaped path that is not an address must not reach a source realm. func TestRenderRejectsNonAddress(t *testing.T) { out := Render("not-an-address") uassert.True(t, strings.Contains(out, "is not a valid address"), "explicit refusal") uassert.False(t, strings.Contains(out, "## Answers"), "no report for a bad path") } // The index reads a global from every source. Pinned by Contains rather than by // an example, because most of those globals move with chain state. func TestRenderIndexReadsEverySource(t *testing.T) { out := Render("") for _, s := range sourceRealms { uassert.True(t, strings.Contains(out, s), "index must list "+s) } uassert.True(t, strings.Contains(out, "pool(s), index only"), "amm is flagged index-only") uassert.True(t, strings.Contains(out, "roll(s) all time"), "dice global is read") uassert.True(t, strings.Contains(out, "page(s) reacted to"), "reactions global is read") } // ExampleRender pins the report for an address no test writes to. Every value // in it comes from a source realm's zero state, except the rate limiter, which // answers a full bucket for an address it has never seen: absence and a fresh // allowance are the same thing there, and the report says so rather than // pretending the read was empty. func ExampleRender() { print(Render(fresh.String())) // Output: // # across: `g1v93hymmnwvkkvun9wd597h6lta047h6l220muq` // // What 15 independently deployed realms say about this address. None of them was changed to answer. // // ## Answers // // | source | fact | value | // | --- | --- | --- | // | [`r/moul/x/daily/ratelimitdemo/v0`](/r/moul/x/daily/ratelimitdemo/v0) | rate-limit tokens left | 5 | // // 17 of 18 reads came back empty and are hidden. // // [back to the index](/r/moul/x/across/v0) }
  8. #8gnomod.toml
  9. #9module = "gno.land/r/moul/x/across/v0" gno = "0.9" private = true
#8AddPackagegno.land/r/moul/x/nativereg/v011 arguments
Attached funds
8000000ugnot

Arguments · 11

  1. #1nativereg
  2. #2README.md
  3. #3# `gno.land/r/moul/x/nativereg/v0` **The metadata a native coin does not have.** `/gno.land/r/moul/x/moultest/v0:moultest` is the entire object. The chain stores a string and a balance: no name, no symbol, no decimals, no icon, no link to the issuer. A wallet showing you that balance has only the path to print. This realm is where a denom says what it is. It is the mirror of [`r/nt/grc20reg`](https://gno.land/r/nt/grc20reg/v0), and the two solve opposite problems: a GRC20 carries its own name and is hard to **find**, so that registry maps a key to an object; a native denom is trivial to find and carries **no** name, so this one maps the denom to what it means. ## Registration is proved, not claimed A denom embeds the package path of the realm allowed to issue it, verbatim: `"/" + pkgPath + ":" + baseName`. So the issuer is readable from the string, and `Register` simply requires the caller to be it. No allowlist, no owner, no signature scheme. ```go import "gno.land/r/moul/x/nativereg/v0" func init(cur realm) { nativereg.Register(cross(cur), Denom, "My Coin", "MINE", 6, "one line about what it is for") } ``` A user account cannot register anything, not even a denom it holds every unit of: there is no package path in a user call to compare against. What is being described is the issuance, not the holding. `IssuerOf(denom)` answers the same question for a denom nobody ever registered, which is the case that matters: given a balance you did not ask for, that is how you find the code that can take it away. ## Facts and hints `Denom` and `Issuer` are facts, because the chain enforces the relation between them. Everything else is whatever the issuing realm chose to say, and `Decimals` in particular is a **display hint with nothing behind it**: the bank has no notion of divisibility, so a coin wrapped out of a 6-decimal GRC20 is still counted in whole units by every balance query. Trusting an entry means trusting the realm that wrote it, exactly as much as holding its coin already means trusting it not to call `RemoveCoin` on you. `?<denom>` shows one entry, `?<pkgpath>` shows everything one realm issues. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/nativereg/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/nativereg/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/nativereg/v0" gno = "0.9" # public: it exists to be imported. A realm registers the denom it issues by # calling Register from its own frame, which is the whole authorization model, # and a private realm cannot be imported at all.
  6. #6nativereg.gno
  7. #7// Package nativereg is the metadata a native coin does not have. // // A realm-issued coin is a string and a balance. `/gno.land/r/moul/x/moultest/v0:moultest` // is the entire object: no name, no symbol, no decimals, no icon, no link to // whoever issues it. The bank knows the number and nothing else, so a wallet or // an explorer showing you a balance has only that path to print. // // This realm is where a denom says what it is. It is the mirror image of // `r/nt/grc20reg`, and the two solve opposite problems: a GRC20 is an object // that carries its own name and is hard to FIND, so the registry maps a key to // the object; a native denom is trivial to find and carries no name, so the // registry maps the denom to what it means. // // # Registration is proved, not claimed // // A denom embeds the package path of the realm allowed to issue it, verbatim: // `"/" + pkgPath + ":" + baseName` (`chain.CoinDenom`). So the issuer is // readable from the denom, and [Register] simply requires the caller to be it. // A realm registers its own coins and nobody else's, with no allowlist, no // owner and no signature scheme. // // A user account cannot register anything, not even a denom it holds all of: // there is no package path in a user call to compare against. That is the // intended shape, since the thing being described is the issuance, not the // holding. // // # What is a hint and what is a fact // // `Denom` and `Issuer` are facts: the chain enforces the relation between them. // Everything else is whatever the issuing realm chose to say, and `Decimals` in // particular is a DISPLAY hint with nothing behind it. The bank has no notion of // divisibility; a coin wrapped out of a 6-decimal GRC20 is still counted in // whole units by every balance query. Rendering 1000000 as 1.000000 is a // convention between this realm and whoever reads it. // // So trusting an entry means trusting the realm that wrote it, exactly as much // as holding its coin already means trusting it not to call `RemoveCoin` on you. package nativereg import ( "chain/runtime" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/nt/ufmt/v0" ) // Entry is what one denom says about itself. type Entry struct { // Denom is the full chain denom, the key of this registry. Denom string // Issuer is the package path embedded in Denom: the only realm that can // issue, remove or re-describe this coin. Issuer string // Name, Symbol and Decimals are display metadata, chosen by the issuer. // Decimals is a hint; see the package doc. Name string Symbol string Decimals int // Doc is one line about what the coin is for. Doc string // Height is when the entry was last written. Height int64 // Revisions counts how many times the issuer has rewritten it. Revisions int } const ( // MaxName, MaxSymbol and MaxDoc bound what an issuer can store here. The // registry pays the storage for every byte a caller hands it, so the // limits are the realm's own cost control, not a style rule. MaxName = 40 MaxSymbol = 12 MaxDoc = 200 // MaxDecimals matches the GRC20 ceiling, which is where a wrapped coin's // hint comes from in practice. MaxDecimals = 18 ) var ( byDenom = avl.NewTree() // denom -> *Entry denoms []string // registration order, for a stable Render ) // Register records or rewrites what a denom means. The caller must be the realm // the denom names, which is the entire authorization. // // Calling it again overwrites the entry and bumps [Entry.Revisions]. An issuer // can always re-describe its own coin, and can never describe anybody else's. func Register(cur realm, denom, name, symbol string, decimals int, doc string) { issuer := issuerOf(denom) prev := cur.Previous() if prev.PkgPath() == "" { panic("only the issuing realm can register a denom; a user account has no package path to prove") } if prev.PkgPath() != issuer { panic(ufmt.Sprintf("%s is issued by %s, not by %s", denom, issuer, prev.PkgPath())) } assertLen("name", name, MaxName) assertLen("symbol", symbol, MaxSymbol) assertLen("doc", doc, MaxDoc) if decimals < 0 || decimals > MaxDecimals { panic(ufmt.Sprintf("decimals is %d, must be 0 to %d", decimals, MaxDecimals)) } e := &Entry{ Denom: denom, Issuer: issuer, Name: name, Symbol: symbol, Decimals: decimals, Doc: doc, Height: runtime.ChainHeight(), } if old := byDenom.Get(denom); old != nil { e.Revisions = old.(*Entry).Revisions + 1 } else { denoms = append(denoms, denom) } byDenom.Set(denom, e) } // Get returns the entry for a denom, and whether it has one. func Get(denom string) (Entry, bool) { v := byDenom.Get(denom) if v == nil { return Entry{}, false } return *v.(*Entry), true } // MustGet is [Get] for a caller that treats an unregistered denom as a bug. func MustGet(denom string) Entry { e, ok := Get(denom) if !ok { panic("no entry for " + denom) } return e } // IsRegistered reports whether a denom has said anything about itself. func IsRegistered(denom string) bool { return byDenom.Get(denom) != nil } // List returns every entry, in registration order. func List() []Entry { out := make([]Entry, 0, len(denoms)) for _, d := range denoms { out = append(out, MustGet(d)) } return out } // ByIssuer returns every entry issued by one realm, in registration order. func ByIssuer(pkgPath string) []Entry { out := []Entry{} for _, d := range denoms { if e := MustGet(d); e.Issuer == pkgPath { out = append(out, e) } } return out } // Count returns how many denoms are registered. func Count() int { return byDenom.Size() } // IssuerOf returns the package path embedded in a denom, without consulting the // registry: the relation is in the string itself. It aborts on anything that is // not a realm denom, which includes `ugnot`. // // It is exported because it is useful to anybody handling a denom, registered or // not: given a balance, this is how you find the code that can take it away. func IssuerOf(denom string) string { return issuerOf(denom) } func issuerOf(denom string) string { if !strings.HasPrefix(denom, "/") { panic("not a realm denom (it must start with /): " + denom) } i := strings.LastIndex(denom, ":") if i < 0 { panic("not a realm denom (it must contain :): " + denom) } issuer := denom[1:i] if issuer == "" || denom[i+1:] == "" { panic("malformed denom: " + denom) } return issuer } func assertLen(what, s string, max int) { if len(s) > max { panic(ufmt.Sprintf("%s is %d bytes, max %d", what, len(s), max)) } }
  8. #8nativereg_test.gno
  9. #9package nativereg import ( "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) const ( issuerPath = "gno.land/r/test/coiner" issuerCoin = "/gno.land/r/test/coiner:testcoin" otherPath = "gno.land/r/test/stranger" otherCoin = "/gno.land/r/test/stranger:othercoin" ) // testing.SetRealm governs only the crossing calls made from the frame that // called it, so each switch below is inline in the test body. func TestOnlyTheIssuerRegisters(cur realm, t *testing.T) { testing.SetRealm(testing.NewCodeRealm(issuerPath)) Register(cross(cur), issuerCoin, "Test Coin", "TEST", 6, "a coin for a test") e := MustGet(issuerCoin) uassert.Equal(t, issuerPath, e.Issuer, "the issuer is read out of the denom, not supplied") uassert.Equal(t, "TEST", e.Symbol) uassert.Equal(t, 6, e.Decimals) uassert.Equal(t, 0, e.Revisions) // The same realm cannot describe somebody else's coin. uassert.AbortsContains(t, cur, "is issued by "+otherPath, func() { Register(cross(cur), otherCoin, "Not Mine", "NOPE", 0, "") }) } func TestAUserAccountCannotRegister(cur realm, t *testing.T) { alice := testutils.TestAddress("nr-alice") testing.SetRealm(testing.NewUserRealm(alice)) // Even for a denom she holds every unit of: what is being described is the // issuance, and a wallet has no package path to prove. uassert.AbortsContains(t, cur, "no package path to prove", func() { Register(cross(cur), issuerCoin, "Mine Now", "MINE", 0, "") }) } func TestReregisteringBumpsRevisions(cur realm, t *testing.T) { testing.SetRealm(testing.NewCodeRealm(issuerPath)) before := Count() Register(cross(cur), issuerCoin, "Test Coin v2", "TEST2", 4, "renamed") e := MustGet(issuerCoin) uassert.Equal(t, "TEST2", e.Symbol, "an issuer can always re-describe its own coin") uassert.Equal(t, 1, e.Revisions) uassert.Equal(t, before, Count(), "a rewrite is not a second entry") uassert.Equal(t, 1, len(ByIssuer(issuerPath))) uassert.Equal(t, 0, len(ByIssuer(otherPath))) } func TestBounds(cur realm, t *testing.T) { testing.SetRealm(testing.NewCodeRealm(issuerPath)) uassert.AbortsContains(t, cur, "name is", func() { Register(cross(cur), issuerCoin, strings.Repeat("x", MaxName+1), "T", 0, "") }) uassert.AbortsContains(t, cur, "symbol is", func() { Register(cross(cur), issuerCoin, "n", strings.Repeat("x", MaxSymbol+1), 0, "") }) uassert.AbortsContains(t, cur, "doc is", func() { Register(cross(cur), issuerCoin, "n", "T", 0, strings.Repeat("x", MaxDoc+1)) }) uassert.AbortsContains(t, cur, "decimals is 19", func() { Register(cross(cur), issuerCoin, "n", "T", MaxDecimals+1, "") }) uassert.AbortsContains(t, cur, "decimals is -1", func() { Register(cross(cur), issuerCoin, "n", "T", -1, "") }) } func TestIssuerOfReadsTheDenom(cur realm, t *testing.T) { // The relation is in the string, so this answers for a denom nobody ever // registered, which is the case that matters: given a balance you did not // ask for, this is how you find the code that can take it away. uassert.Equal(t, "gno.land/r/nobody/here/v3", IssuerOf("/gno.land/r/nobody/here/v3:ghost")) uassert.Equal(t, issuerPath, IssuerOf(issuerCoin)) uassert.PanicsContains(t, cur, "must start with /", func() { IssuerOf("ugnot") }) uassert.PanicsContains(t, cur, "must contain :", func() { IssuerOf("/gno.land/r/a/v0") }) uassert.PanicsContains(t, cur, "malformed", func() { IssuerOf("/gno.land/r/a/v0:") }) uassert.PanicsContains(t, cur, "malformed", func() { IssuerOf("/:x") }) } func TestUnregisteredReadsCleanly(cur realm, t *testing.T) { _, ok := Get("/gno.land/r/nobody/here/v3:ghost") uassert.False(t, ok) uassert.False(t, IsRegistered("/gno.land/r/nobody/here/v3:ghost")) uassert.PanicsContains(t, cur, "no entry for", func() { MustGet("/gno.land/r/x/y/v0:zz") }) } func TestRender(cur realm, t *testing.T) { testing.SetRealm(testing.NewCodeRealm(otherPath)) Register(cross(cur), otherCoin, "Other Coin", "OTHER", 0, "the second one") index := Render("") uassert.True(t, strings.Contains(index, "# nativereg"), index) uassert.True(t, strings.Contains(index, "OTHER"), "the index lists entries:\n"+index) one := Render(otherCoin) uassert.True(t, strings.Contains(one, "the second one"), one) uassert.True(t, strings.Contains(one, otherPath), one) byIssuer := Render(issuerPath) uassert.True(t, strings.Contains(byIssuer, "TEST2"), byIssuer) uassert.False(t, strings.Contains(byIssuer, "OTHER"), "one issuer's page shows only its own") // A Render never aborts on a path a reader typed, registered or not. uassert.True(t, strings.Contains(Render("/gno.land/r/x/y/v0:nope"), "Not registered")) uassert.True(t, strings.Contains(Render("ugnot"), "registered nothing"), "a non-denom path falls through to the issuer view rather than aborting") uassert.True(t, strings.Contains(Render("/garbage"), "not a realm denom")) } func TestRenderEscapesWhatAnIssuerWrote(cur realm, t *testing.T) { // An issuer's own name and doc are still caller-supplied text, and a realm // that can register can write a table-breaking pipe as easily as a name. const nasty = "gno.land/r/test/nasty" testing.SetRealm(testing.NewCodeRealm(nasty)) Register(cross(cur), "/gno.land/r/test/nasty:nastycoin", "a|b", "x|y", 0, "") index := Render("") uassert.False(t, strings.Contains(index, "| a|b |"), "a raw pipe would open a column:\n"+index) uassert.True(t, strings.Contains(index, "a\\|b"), "it is escaped instead:\n"+index) }
  10. #10render.gno
  11. #11package nativereg import ( "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/md/v0" "gno.land/p/nt/ufmt/v0" ) // Render lists the registry. `?<denom>` shows one entry, `?<pkgpath>` shows // everything one realm issues. func Render(path string) string { if path == "" { return renderIndex() } if strings.HasPrefix(path, "/") { return renderDenom(path) } return renderIssuer(path) } func renderIndex() string { var sb strings.Builder sb.WriteString(md.H1("nativereg: what a native denom means")) sb.WriteString("\nA realm-issued coin is a string and a balance. The chain stores no name, " + "no symbol and no decimals for it, so this realm does. An issuer registers its own " + "denoms and nobody else's: the package path is embedded in the denom, and that is " + "the whole authorization.\n\n") t := ui.NewTable("denom", "symbol", "name", "decimals", "issuer") for _, e := range List() { t.Row(md.InlineCode(ui.ShortN(e.Denom, 22, 12)), ui.Cell(e.Symbol), ui.Cell(e.Name), ufmt.Sprintf("%d", e.Decimals), md.Link(ui.Short(e.Issuer), "/"+strings.TrimPrefix(e.Issuer, "gno.land/"))) } sb.WriteString(md.H2("Registered")) sb.WriteString("\n" + t.OrEmpty("Nothing registered yet.") + "\n") sb.WriteString(md.H2("Register your own")) sb.WriteString("\nFrom inside the realm that issues the coin, never from a wallet:\n\n") sb.WriteString(md.LanguageCodeBlock("go", "import \"gno.land/r/moul/x/nativereg/v0\"\n\n"+ "func init(cur realm) {\n"+ "\tnativereg.Register(cross(cur), Denom, \"My Coin\", \"MINE\", 6,\n"+ "\t\t\"one line about what it is for\")\n"+ "}")) sb.WriteString("\n" + md.Italic("Decimals is a display hint. The bank counts whole units and "+ "knows nothing about divisibility.") + "\n") return sb.String() } func renderDenom(denom string) string { e, ok := Get(denom) if !ok { return md.H1("Not registered") + "\n" + md.InlineCode(denom) + " has said nothing about itself.\n\nIts issuer is " + md.InlineCode(safeIssuer(denom)) + ", and only that realm can change it.\n" } t := ui.NewTable("", "") t.Row("denom", md.InlineCode(e.Denom)) t.Row("issuer", md.InlineCode(e.Issuer)) t.Row("name", ui.Cell(e.Name)) t.Row("symbol", ui.Cell(e.Symbol)) t.Row("decimals", ufmt.Sprintf("%d (display hint)", e.Decimals)) t.Row("registered at block", ufmt.Sprintf("%d", e.Height)) t.Row("revisions", ufmt.Sprintf("%d", e.Revisions)) s := md.H1(ui.Inline(e.Symbol)) + "\n" if e.Doc != "" { s += ui.Inline(e.Doc) + "\n\n" } s += t.String() + "\n" s += md.Blockquote("The issuer can rewrite every row but the first two, and can remove "+ "this coin from any balance at any time. That is what a realm-issued coin is.") + "\n" return s } func renderIssuer(pkgPath string) string { entries := ByIssuer(pkgPath) s := md.H1("Issued by "+ui.Inline(pkgPath)) + "\n" t := ui.NewTable("denom", "symbol", "name") for _, e := range entries { t.Row(md.InlineCode(ui.ShortN(e.Denom, 22, 12)), ui.Cell(e.Symbol), ui.Cell(e.Name)) } return s + t.OrEmpty("This realm has registered nothing here.") + "\n" } // safeIssuer is IssuerOf for a Render, which must never abort on a path a reader // typed into the URL bar. func safeIssuer(denom string) string { if !strings.HasPrefix(denom, "/") { return "unknown (not a realm denom)" } i := strings.LastIndex(denom, ":") if i < 1 { return "unknown (not a realm denom)" } return denom[1:i] }
Attached funds
5000000ugnot

Arguments · 11

  1. #1facade
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/adminreg/facade/v0` The **permanent facade** of pattern F. Implementations nominate themselves from `init` (`Propose`); the owner promotes one by path string (`Accept`). Nothing serves until both have happened. Accepting takes a string on purpose: an interface value cannot travel in a `maketx call` argument, so the original "admin hands the facade an object" shape was not reachable from a wallet at all. See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/adminreg/facade/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/adminreg/facade/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4facade.gno
  5. #5// untrusted-render: every path Render echoes was read off a crossing frame in // Propose (cur.Previous().PkgPath(), which a caller cannot forge or type), and // Accept can only promote a key that is already in that tree. // // Package facade is the permanent entry point of the "propose and accept" // upgrade pattern (pattern F of the exploration; see ../../README.md). // // Pattern E lets a deploy take the realm over on the spot. This one splits that // into two steps that different people can hold: an implementation realm // NOMINATES itself from its own init, and the owner ACCEPTS a package path in a // separate transaction. Nothing serves until both have happened. // // The split exists because of a mechanical limit, not just a governance // preference. An implementation is an interface value, and a wallet cannot put // one in a `maketx call` argument: only strings and numbers travel. The // original shape of this pattern (the owner hands the facade an object) is // therefore reachable only from `maketx run` or from another realm. Proposing // from init and accepting by PATH makes both halves ordinary transactions. package facade import ( "strings" "gno.land/p/nt/avl/v0" "gno.land/p/nt/ownable/v0" "gno.land/p/nt/ufmt/v0" ) const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul // prefix bounds who may even nominate itself. Accepting is still a separate, // owner-gated decision. const prefix = "gno.land/r/moul/x/upgrade/adminreg/impl/" // Impl is the contract an implementation realm must satisfy. type Impl interface { Greet(name string) string Version() string } // The stage ladder, copied in shape from Sui's UpgradeCap: compatible, additive, // dependency-only, immutable, where a policy can only ever become MORE // restrictive and make_immutable discards the cap. CosmWasm (a contract with no // admin) and Solana (an upgrade authority set to None) reach the same place with // one bit; the ladder is better because the interesting states are between // "anything may take this over" and "nothing may ever change again". // // This pattern has an owner and a candidate list, so it has a middle rung the // owner-less selfreg cannot express: no new code, but still free to roll back // among what is already deployed. // // What the top rung does NOT do on its own: freezing this realm ends changes to // the POINTER, not to the code behind it. A private implementation realm can be // re-added at its own path, which would swap behaviour under a frozen facade. // It holds here only because every implementation is public by construction: // handing the facade its own object is exactly what forbids private (see // ../../README.md). const ( StageOpen = 0 // anything under the prefix may propose, the owner may accept any candidate StageClosed = 1 // no new candidates; the owner may still accept among those already proposed StageFrozen = 2 // nothing may be accepted again, whatever is live is final ) var ( Ownable = ownable.NewWithAddress(owner) stage = StageOpen candidates = avl.NewTree() // pkgpath -> Impl live Impl livePath string ) // Propose nominates the calling realm. Called from the implementation's init, // so deploying makes a candidate and nothing more. func Propose(cur realm, impl Impl) { if stage != StageOpen { panic("adminreg/facade/v0 is " + StageName() + ", no new candidate may be proposed") } caller := cur.Previous().PkgPath() if !strings.HasPrefix(caller, prefix) { panic("unauthorized: " + caller + " is not under " + prefix) } if impl == nil { panic("implementation must not be nil") } candidates.Set(caller, impl) } // Accept promotes a proposed path to live. Owner-gated, and it takes a STRING, // so it is callable straight from a wallet. func Accept(cur realm, pkgPath string) { Ownable.AssertOwnedBy(cur.Previous().Address()) if stage == StageFrozen { panic("adminreg/facade/v0 is frozen, " + livePath + " is final") } v := candidates.Get(pkgPath) if v == nil { panic("no candidate at " + pkgPath) } live, livePath = v.(Impl), pkgPath } // Close stops new candidates. The owner may still accept among those already // proposed, so a rollback stays possible while new code does not. func Close(cur realm) { tighten(cur, StageClosed) } // Freeze ends this realm's upgradeability, forever. There is no rung above it // and nothing takes it back: that is the whole point, and it is the only way out // of every caller trusting the owner rather than the code. func Freeze(cur realm) { tighten(cur, StageFrozen) } // tighten is the ratchet. Owner-gated, and it refuses to loosen: the stage is // the one piece of state here that a later owner cannot undo. func tighten(cur realm, to int) { Ownable.AssertOwnedBy(cur.Previous().Address()) if to <= stage { panic("the stage ladder only tightens, and this realm is already " + StageName()) } stage = to } // Stage is the rung this realm is on. It only ever goes up. func Stage() int { return stage } // StageName is Stage as the word a caller reads in Render. func StageName() string { switch stage { case StageFrozen: return "frozen" case StageClosed: return "closed" default: return "open" } } // Live is the package path currently serving, or "" before the first Accept. func Live() string { return livePath } // Candidates lists every path that has nominated itself, in order. func Candidates() []string { out := []string{} candidates.Iterate("", "", func(k string, _ any) bool { out = append(out, k) return false }) return out } // Greet forwards to the accepted implementation. func Greet(name string) string { assertLive() return live.Greet(name) } // Version reports the accepted implementation's own version string. func Version() string { assertLive() return live.Version() } func assertLive() { if live == nil { panic("no implementation accepted") } } func Render(_ string) string { out := ufmt.Sprintf("adminreg/facade/v0 [%s]\n", StageName()) if live == nil { out += "live: none accepted\n" } else { out += ufmt.Sprintf("live: %s (%s)\n%s\n", live.Version(), livePath, live.Greet("world")) } out += ufmt.Sprintf("candidates: %d\n", candidates.Size()) for _, p := range Candidates() { out += "- " + p + "\n" } return out }
  6. #6gnomod.toml
  7. #7module = "gno.land/r/moul/x/upgrade/adminreg/facade/v0" gno = "0.9" # public: every implementation realm imports it to nominate itself from init
  8. #8render_example_test.gno
  9. #9package facade // ExampleRender pins the facade before any implementation realm exists. Nothing // is imported here, so there is no candidate and nothing accepted. func ExampleRender() { print(Render("")) // Output: // adminreg/facade/v0 [open] // live: none accepted // candidates: 0 }
  10. #10z_outsider_filetest.gno
  11. #11// PKGPATH: gno.land/r/moul/main package main import "gno.land/r/moul/x/upgrade/adminreg/facade/v0" // Nominating is bounded by path even though accepting is bounded by owner: a // realm outside the prefix cannot even fill the candidate list with noise. func main(cur realm) { facade.Propose(cross(cur), nil) } // Error: // unauthorized: gno.land/r/moul/main is not under gno.land/r/moul/x/upgrade/adminreg/impl/
#10AddPackagegno.land/r/moul/x/upgrade/lazy/v09 arguments
Attached funds
5000000ugnot

Arguments · 9

  1. #1lazy
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/lazy/v0` Version 0 of the **lazy migration** pattern (pattern D). An ordinary record store, written without any knowledge of a successor. `Size()` never shrinks: migration copies forward, so this realm keeps paying for every record it ever held. See [the pattern](../README.md) and [the exploration](../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/lazy/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/lazy/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/upgrade/lazy/v0" gno = "0.9" # public: imported by .../lazy/v1, which reads records out of it to migrate them
  6. #6lazy.gno
  7. #7// Package lazy is version 0 of the "lazy migration" upgrade pattern // (pattern D of the exploration; see ../README.md). // // This version is an ordinary record store. It is written without any // knowledge of a successor: what makes the pattern work is only that its // records are readable from outside, which any avl-backed realm already is. package lazy import ( "gno.land/p/nt/avl/v0" "gno.land/p/nt/ufmt/v0" ) // Record is v0's shape. v1 changes it, which is the whole reason for a new // version: a stored-layout change is never done in place. type Record struct { Name string Score int } func (r *Record) String() string { if r == nil { return "nil" } return ufmt.Sprintf("v0{%s %d}", r.Name, r.Score) } var records = avl.NewTree() func init() { records.Set("ada", &Record{Name: "ada", Score: 1}) records.Set("bob", &Record{Name: "bob", Score: 2}) } // Get returns a record or nil. Non-crossing: v1 reads through it while // migrating, and gnoweb can call it without a transaction. func Get(key string) *Record { v := records.Get(key) if v == nil { return nil } return v.(*Record) } // Size is how many records still live here. It never shrinks: lazy migration // COPIES forward, it does not move, so v0 keeps paying for every record it // ever held. func Size() int { return records.Size() } func Render(_ string) string { out := ufmt.Sprintf("lazy/v0: %d records\n", records.Size()) records.Iterate("", "", func(k string, v any) bool { out += ufmt.Sprintf("- %s: %s\n", k, v.(*Record).String()) return false }) return out }
  8. #8render_example_test.gno
  9. #9package lazy // ExampleRender pins the render of the seeded v0 store. func ExampleRender() { print(Render("")) // Output: // lazy/v0: 2 records // - ada: v0{ada 1} // - bob: v0{bob 2} }
#11AddPackagegno.land/r/moul/x/upgrade/lock/v09 arguments
Attached funds
5000000ugnot

Arguments · 9

  1. #1lock
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/lock/v0` Version 0 of the **retire the predecessor** upgrade pattern (pattern B). Writable until the owner calls `Retire(successor)`, which is one-way: from then on it is a read-only archive that names where callers should go. See [the pattern](../README.md) and [the exploration](../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/lock/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/lock/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/upgrade/lock/v0" gno = "0.9" # public: imported by .../lock/v1, which folds its frozen total in after retirement
  6. #6lock.gno
  7. #7// untrusted-render: successor is the only stored string Render echoes, and // Retire validates it with assertPkgPath before storing it. // // Package lock is version 0 of the "retire the predecessor" upgrade pattern // (pattern B of the exploration; see ../README.md). // // Unlike pattern A, this version knows it can be superseded. Retire() freezes // it and names its successor, and the freeze is one-way: once a successor has // absorbed this version's total, re-opening it here would double-count. package lock import ( "strings" "gno.land/p/nt/ownable/v0" "gno.land/p/nt/ufmt/v0" ) const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul var ( Ownable = ownable.NewWithAddress(owner) counter int successor string // pkgpath that replaced this one; "" while this version is live ) // Inc adds n. It aborts once a successor has been declared. func Inc(cur realm, n int) { if successor != "" { panic("lock/v0 is retired, use " + successor) } counter += n } // Get returns this version's final (or current) total. func Get() int { return counter } // Successor is the path callers should move to, or "" while this version is live. func Successor() string { return successor } // Retire freezes this version and names its replacement. Owner-gated, one-way. func Retire(cur realm, pkgPath string) { Ownable.AssertOwnedBy(cur.Previous().Address()) if successor != "" { panic("lock/v0 is already retired, successor is " + successor) } assertPkgPath(pkgPath) successor = pkgPath } func Render(_ string) string { if successor == "" { return ufmt.Sprintf("lock/v0: %d (live)\n", counter) } return ufmt.Sprintf("lock/v0: %d (retired, use %s)\n", counter, successor) } // assertPkgPath rejects anything that is not a gno.land realm path. // // It exists for Render, not for correctness of the upgrade: the stored path is // echoed into markdown, so validating it at write time is what lets Render // print it raw. ui.Inline would escape the dots in "gno.land" and turn the one // string a reader needs to copy into "gno\.land". func assertPkgPath(p string) { if !strings.HasPrefix(p, "gno.land/r/") { panic("not a realm path: " + p) } for _, c := range p { ok := c == '/' || c == '.' || c == '_' || c == '-' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') if !ok { panic("illegal character in realm path") } } }
  8. #8render_example_test.gno
  9. #9package lock // ExampleRender pins the render of a freshly deployed, still-live version. func ExampleRender() { print(Render("")) // Output: // lock/v0: 0 (live) }
Attached funds
5000000ugnot

Arguments · 11

  1. #1facade
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/selfreg/facade/v0` The **permanent facade** of pattern E. Holds an `Impl` interface value and forwards to it. Implementations register themselves from their own `init`, gated on an explicit path prefix (`nestedpkg` does not fit version-last paths). See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/selfreg/facade/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/selfreg/facade/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4facade.gno
  5. #5// Package facade is the permanent entry point of the "self-registering // implementation" upgrade pattern (pattern E of the exploration; see // ../../README.md). // // The path callers import never changes and holds no business logic: it holds // an interface value. A new implementation realm takes over simply by being // deployed, because it registers itself from its own init. Nobody has to send // a transaction to switch, which is the pattern's whole appeal and also its // whole risk: whoever can deploy under the guarded prefix can take the realm. package facade import ( "strings" "gno.land/p/nt/ufmt/v0" ) // Impl is the contract an implementation realm must satisfy. This interface is // the one thing here that can never change: it is compiled into every caller. type Impl interface { Greet(name string) string Version() string } // prefix is the only gate. An implementation must live under it. // // gno ships p/nt/nestedpkg/v0 for exactly this check, but its AssertCallerIsSubPath // wants the implementation nested UNDER the facade's own path, and with the // version segment last (facade/v0, impl/v0) no sibling is a sub-path of another. // IsSameNamespace is the other shipped option and is far too loose: it would let // any realm in the whole namespace seize this one. Hence an explicit prefix. const prefix = "gno.land/r/moul/x/upgrade/selfreg/impl/" // The stage ladder. Sui gates package upgrades with an UpgradeCap whose policy // runs compatible, additive, dependency-only, immutable, and the rule that makes // it worth copying is that a policy can only ever become MORE restrictive. // CosmWasm and Solana land on the same primitive from different directions: a // contract with no admin, a program whose upgrade authority is None. All three // say the same thing, that the way out of "you are trusting the owner rather // than the code" is an authority you can drop, permanently. // // This pattern has no owner, so its ladder has two rungs rather than four: the // only actor the facade already trusts is whichever implementation is live. const ( StageOpen = 0 // any realm under the prefix takes over by deploying StageSealed = 1 // nothing may register again, the live implementation is final ) var ( live Impl livePath string stage = StageOpen ) // Register makes the calling realm the live implementation. Called from the // implementation's own init, so deploying IS the upgrade. func Register(cur realm, impl Impl) { if stage != StageOpen { panic("selfreg/facade/v0 is sealed, " + livePath + " is final") } caller := cur.Previous().PkgPath() if !strings.HasPrefix(caller, prefix) { panic("unauthorized: " + caller + " is not under " + prefix) } if impl == nil { panic("implementation must not be nil") } live, livePath = impl, caller } // Seal ends this realm's upgradeability, forever. Callable only by the // implementation currently serving, because with no owner that is the only // actor the facade already trusts. It grants nothing new: whoever could deploy // under the prefix could already take the realm over, and this only lets them // make that the last word. // // One-way, and there is no rung above it. func Seal(cur realm) { caller := cur.Previous().PkgPath() if livePath == "" || caller != livePath { panic("unauthorized: only the live implementation may seal, and that is " + livePath) } stage = StageSealed } // Stage is the rung this realm is on. It only ever goes up. func Stage() int { return stage } // StageName is Stage as the word a caller reads in Render. func StageName() string { if stage == StageSealed { return "sealed" } return "open" } // Live is the package path currently serving, or "" before the first deploy. func Live() string { return livePath } // Greet forwards to the live implementation. func Greet(name string) string { assertLive() return live.Greet(name) } // Version reports the live implementation's own version string. func Version() string { assertLive() return live.Version() } func assertLive() { if live == nil { panic("no implementation registered") } } func Render(_ string) string { if live == nil { return ufmt.Sprintf("selfreg/facade/v0 [%s]: no implementation registered\n", StageName()) } return ufmt.Sprintf("selfreg/facade/v0 [%s]: %s (%s)\n%s\n", StageName(), live.Version(), livePath, live.Greet("world")) }
  6. #6gnomod.toml
  7. #7module = "gno.land/r/moul/x/upgrade/selfreg/facade/v0" gno = "0.9" # public: every implementation realm imports it to register itself from init
  8. #8render_example_test.gno
  9. #9package facade // ExampleRender pins what the facade renders before anything has registered. // No implementation realm is imported here, so the facade stands alone. func ExampleRender() { print(Render("")) // Output: // selfreg/facade/v0 [open]: no implementation registered }
  10. #10z_outsider_filetest.gno
  11. #11// PKGPATH: gno.land/r/moul/main package main import "gno.land/r/moul/x/upgrade/selfreg/facade/v0" // A realm outside the guarded prefix cannot seize the facade. The path is read // off the crossing frame, so there is no argument to lie about. func main(cur realm) { facade.Register(cross(cur), nil) } // Error: // unauthorized: gno.land/r/moul/main is not under gno.land/r/moul/x/upgrade/selfreg/impl/
Attached funds
5000000ugnot

Arguments · 9

  1. #1root
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/store/root/v0` The **data realm** of pattern C. Holds the counter and grants write access to exactly one logic realm at a time, identified by the package path read off the crossing frame. `SetLive(path)` is the upgrade. See [the pattern](../../README.md) and [the exploration](../../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/store/root/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/store/root/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/upgrade/store/root/v0" gno = "0.9" # public: every logic realm in this pattern imports it; it is the shared store
  6. #6render_example_test.gno
  7. #7package root // ExampleRender pins the render of a freshly deployed data realm. func ExampleRender() { print(Render("")) // Output: // store/root/v0: 0 (live logic: gno.land/r/moul/x/upgrade/store/logic/v0) }
  8. #8root.gno
  9. #9// untrusted-render: live is the only stored string Render echoes, and SetLive // validates it with assertPkgPath before storing it. // // Package root is the data realm of the "state realm + swappable logic" upgrade // pattern (pattern C of the exploration; see ../../README.md). // // root holds the state and nothing else worth changing. It grants write access // to exactly one logic realm at a time, identified by package path off the // crossing frame rather than passed in as an argument, so a logic realm cannot // claim to be a path it does not occupy. Upgrading is a single SetLive call: // no migration, no downtime, and the data never moves. package root import ( "strings" "gno.land/p/nt/ownable/v0" "gno.land/p/nt/ufmt/v0" ) const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul var ( Ownable = ownable.NewWithAddress(owner) counter int live = "gno.land/r/moul/x/upgrade/store/logic/v0" ) // Inc adds n to the stored counter. Only the live logic realm may call it. func Inc(cur realm, n int) int { assertCallerIsLive(cur) counter += n return counter } // Get is open to everyone: the data is public, only writes are gated. func Get() int { return counter } // Live is the package path currently allowed to write. func Live() string { return live } // SetLive hands write access to another logic realm. This is the upgrade. func SetLive(cur realm, pkgPath string) { Ownable.AssertOwnedBy(cur.Previous().Address()) assertPkgPath(pkgPath) live = pkgPath } func assertCallerIsLive(cur realm) { caller := cur.Previous().PkgPath() if caller != live { panic("unauthorized: " + caller + " is not the live logic realm (" + live + ")") } } func Render(_ string) string { return ufmt.Sprintf("store/root/v0: %d (live logic: %s)\n", counter, live) } // assertPkgPath rejects anything that is not a gno.land realm path. // // It exists for Render, not for correctness of the upgrade: the stored path is // echoed into markdown, so validating it at write time is what lets Render // print it raw. ui.Inline would escape the dots in "gno.land" and turn the one // string a reader needs to copy into "gno\.land". func assertPkgPath(p string) { if !strings.HasPrefix(p, "gno.land/r/") { panic("not a realm path: " + p) } for _, c := range p { ok := c == '/' || c == '.' || c == '_' || c == '-' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') if !ok { panic("illegal character in realm path") } } }
#14AddPackagegno.land/r/moul/x/upgrade/wrap/v09 arguments
Attached funds
5000000ugnot

Arguments · 9

  1. #1wrap
  2. #2README.md
  3. #3# `gno.land/r/moul/x/upgrade/wrap/v0` Version 0 of the **wrapping versions** upgrade pattern (pattern A). A plain counter that knows nothing about any successor and stays writable forever. See [the pattern](../README.md) and [the exploration](../../README.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/upgrade/wrap/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/upgrade/wrap/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/upgrade/wrap/v0" gno = "0.9" # public: imported by .../wrap/v1, which reads its counter; that import IS the pattern
  6. #6render_example_test.gno
  7. #7package wrap // ExampleRender pins the render of a freshly deployed v0. func ExampleRender() { print(Render("")) // Output: // wrap/v0: 0 }
  8. #8wrap.gno
  9. #9// Package wrap is version 0 of the "wrapping versions" upgrade pattern // (pattern A of the exploration; see ../README.md). // // Nothing here knows that a successor exists. v0 keeps its own counter and // stays writable forever: callers pinned to this path keep working, and keep // diverging from whatever the newer version reports. package wrap import "gno.land/p/nt/ufmt/v0" var counter int // Inc adds n to this version's own counter. func Inc(cur realm, n int) { counter += n } // Get returns this version's counter. Non-crossing on purpose: a successor // reads it without entering this realm as a writer. func Get() int { return counter } func Render(_ string) string { return ufmt.Sprintf("wrap/v0: %d\n", counter) }

Result log

msg:0,success:true,log:,events:[]
msg:1,success:true,log:,events:[]
msg:2,success:true,log:,events:[]
msg:3,success:true,log:,events:[]
msg:4,success:true,log:,events:[]
msg:5,success:true,log:,events:[]
msg:6,success:true,log:,events:[]
msg:7,success:true,log:,events:[]
msg:8,success:true,log:,events:[]
msg:9,success:true,log:,events:[]
msg:10,success:true,log:,events:[]
msg:11,success:true,log:,events:[]
msg:12,success:true,log:,events:[]
msg:13,success:true,log:,events:[]

← Back to block 410,016