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

v0 source pure

Package curated is the engine behind a curated list where being on the list costs something: anyone may list an entry...

Readme View source

gno.land/p/moul/x/social/curated/v0

The engine behind a list where being on it costs something: New, Apply, Challenge, Vote, Resolve, Unlist, Withdraw, plus the reads a page needs.

1r := curated.New(1_000_000, 1000)              // deposit, challenge window
2r.Apply("gnoswap", "/r/gnoswap", "the DEX", owner, paid, height)
3r.Challenge("gnoswap", challenger, bond, height) // bond matches the deposit
4r.Vote("gnoswap", voter, false, height)
5r.Resolve("gnoswap", height+1001)                // credits the winner

A list anybody can write to for free is a list nobody can read, because the cheapest way to be on it is to be on it a thousand times. A deposit does not make an entry good. It makes a bad entry expensive to leave standing, because somebody who disagrees can put the same amount at risk and take yours. The list is worth reading in proportion to what it would cost to pollute it.

This package moves no coins. Every payout is a credit in an internal ledger the payee collects with Withdraw, which zeroes it before returning the amount; the realm sends afterwards. A registry that looped over winners and sent to each one would fail entirely when one of them could not be paid.

Three rules worth knowing before you read the code:

  • A tie keeps the entry. The incumbent wins ties, so a challenge that convinces nobody costs the challenger their bond. A challenge has to be worth making, which means losing one has to hurt.
  • An owner may not challenge their own entry. It is free for them (a kept entry credits the owner the bond, which would be their own money back) and it would make the entry immune to a real challenge for the whole window.
  • A removed key is free to apply for again. Burning the name forever punishes the name rather than the entry.

Votes are one address, one vote, and that is sybil-prone: a hundred addresses are cheap and nothing here can tell them apart. Gating who counts is the job of a vouch graph, r/moul/x/social/vouch in this family, and wiring the two together is the first real upgrade this package wants. Voters are also paid nothing in v0, which is the second.

Locked() plus Owed() is the solvency invariant: what the registry is holding as deposits and bonds, plus what it owes people who have not collected yet, should equal the realm's balance. The realm's test suite asserts exactly that against the chain at every step of a full round.

Live realm: r/moul/x/social/curated, which carries the reasoning for why the bonds are GNOT and not a token of its own.


Part of 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.

Overview

Package curated is the engine behind a curated list where being on the list costs something: anyone may list an entry by locking a deposit, anyone may challenge an entry by matching that deposit with a bond, and the loser of the challenge pays the winner.

Why a deposit

A list anybody can write to for free is a list nobody can read, because the cheapest way to be on it is to be on it a thousand times. A deposit does not make an entry good; it makes a bad entry expensive to leave standing, because somebody who disagrees can put the same amount at risk and take yours. The list is worth reading in proportion to what it would cost to pollute it.

The model

Example
1Registry  every entry, plus the credit ledger the payouts land in
2Entry     a key, a URL, a description, an owner, the deposit, the height
3          it was listed at, and one of three states
4Challenge a challenger, a matching bond, a deadline, and the votes

A key is unique while it is on the list (ValidKey bounds it to a slug), and a removed key is free again: a challenge that wins removes an entry, it does not burn the name forever.

The money, and why nothing is ever sent from here

This package moves no coins. Every payout is a credit in an internal ledger that the payee collects with Registry.Withdraw, which is the pull-payment shape: a realm that loops over winners and sends to each one fails entirely when one of them cannot be paid, and hands a griefer a cheap denial of service. Registry.Locked plus Registry.Owed is what the holding realm must have at its address, and a realm can assert exactly that.

Voting is sybil-prone, deliberately and visibly

Registry.Vote is one address, one vote, unweighted. An address is free, so a challenge outcome is a poll of whoever bothered to make keys, not of anybody in particular. That is not a gap this package can close: deciding who counts as a person is a different problem with a different realm behind it, r/moul/x/social/vouch, a sibling in this family. Until a vote is gated on a vouched identity, read a resolution as "nobody with a stake objected enough", not as a verdict.

What v0 does not do, in the order it should be fixed

  1. Voters are paid nothing. Voting costs gas and returns nothing, so the only addresses with a reason to vote are the two with money on the outcome. A share of the loser's stake for the winning side is the standard answer and it is the first thing to add.
  2. There is no application period: Registry.Apply lists immediately, so a bad entry is visible until somebody challenges it.
  3. A challenge cannot be withdrawn, and a vote cannot be changed.

Constants 2

const MaxKeyLen, MaxURLLen, MaxDescLen, MaxEntries, MaxPayees

 1const (
 2	// MaxKeyLen is the longest key accepted. A key is a name people type and
 3	// link to, not a payload.
 4	MaxKeyLen = 64
 5
 6	// MaxURLLen and MaxDescLen bound what one entry can lock up of somebody
 7	// else's storage deposit.
 8	MaxURLLen  = 240
 9	MaxDescLen = 240
10
11	// MaxEntries bounds the registry so a listing stays predictable in gas.
12	MaxEntries = 4096
13
14	// MaxPayees bounds the credit ledger for the same reason.
15	MaxPayees = 4096
16)
source

const StateListed, StateChallenged, StateRemoved

 1const (
 2	// StateListed is on the list and unchallenged.
 3	StateListed State = iota
 4
 5	// StateChallenged is on the list with a challenge open against it. It
 6	// still renders: a challenge is an objection, not a verdict.
 7	StateChallenged
 8
 9	// StateRemoved is off the list, either lost to a challenge or taken down
10	// by its own owner. The key is free for anyone to apply for again.
11	StateRemoved
12)
source

Variables 1

var ErrBadKey, ErrBadURL, ErrBadDescription, ErrTaken, ErrNoEntry, ErrWrongDeposit, ErrWrongBond, ErrChallenged, ErrSelfChallenge, ErrNoChallenge, ErrVotingClosed, ErrAlreadyVoted, ErrTooEarly, ErrNotOwner, ErrNothingOwed, ErrFull, ErrOverflow, ErrBadAmount

 1var (
 2	ErrBadKey         = errors.New("curated: not a key: 1 to 64 bytes of a-z 0-9 - _ . starting alphanumeric")
 3	ErrBadURL         = errors.New("curated: url is empty, too long, or has a space or a control character")
 4	ErrBadDescription = errors.New("curated: description is empty, too long, or not a single line")
 5	ErrTaken          = errors.New("curated: that key is already on the list")
 6	ErrNoEntry        = errors.New("curated: no such entry")
 7	ErrWrongDeposit   = errors.New("curated: the deposit must be paid exactly")
 8	ErrWrongBond      = errors.New("curated: the bond must match the entry's deposit")
 9	ErrChallenged     = errors.New("curated: that entry is under challenge")
10	ErrSelfChallenge  = errors.New("curated: an owner cannot challenge their own entry")
11	ErrNoChallenge    = errors.New("curated: that entry is not under challenge")
12	ErrVotingClosed   = errors.New("curated: the challenge deadline has passed")
13	ErrAlreadyVoted   = errors.New("curated: one address, one vote")
14	ErrTooEarly       = errors.New("curated: the challenge is still open")
15	ErrNotOwner       = errors.New("curated: only the entry's owner can do that")
16	ErrNothingOwed    = errors.New("curated: nothing to withdraw")
17	ErrFull           = errors.New("curated: the registry is full")
18	ErrOverflow       = errors.New("curated: the credit would overflow")
19	ErrBadAmount      = errors.New("curated: amount must be positive")
20)
source

The errors a caller can get back. A p/ returns them; the realm decides to abort.

Functions 4

func ValidDescription

1func ValidDescription(description string) bool
source

ValidDescription reports whether description can be stored: non-empty after trimming, within MaxDescLen, and a single line.

Newlines and tabs are refused rather than stripped, because a description is shown back to the person who wrote it and silently rewriting it is worse than telling them it was refused. It lives in a table cell, which a newline would break out of and an escaper would then have to repair.

func ValidKey

1func ValidKey(key string) bool
source

ValidKey reports whether key is usable: 1 to MaxKeyLen bytes of lowercase ASCII letters, digits, '-', '_' and '.', starting with a letter or a digit.

The leading-alphanumeric rule is what stops "." and "..", and what stops a key that reads as punctuation in the list it is shown in. The charset is narrow so a key can be typed, linked and compared without surprises; it is still escaped at render time, because a validator and an escaper protect against different mistakes.

func ValidURL

1func ValidURL(url string) bool
source

ValidURL reports whether url can be stored: non-empty, within MaxURLLen, and free of spaces and control characters.

No scheme is required, because a list of on-chain things is the obvious use and those have no host. A renderer treats a schemeless URL as a path relative to the chain's web root, so an on-chain target is written "/r/moul/home" and an off-chain one carries its own "https://".

Nothing here checks that the target exists: a deposit backs the claim that the entry is worth listing, not the claim that it resolves.

func New

1func New(deposit, challengeBlocks int64) *Registry
source

New returns an empty registry where listing costs deposit and a challenge runs for challengeBlocks blocks.

Both are fixed for the life of the registry. An entry remembers the deposit it actually paid, so a future registry that can reprice itself still charges a challenger what the owner of that entry risked, and not today's number.

Types 5

type Challenge

struct
 1type Challenge struct {
 2	Challenger address
 3	Bond       int64
 4	Deadline   int64 // block height the voting stops at, exclusive
 5
 6	// Keep and Remove are the unweighted vote counts. See the package doc on
 7	// what they are and are not worth.
 8	Keep   int64
 9	Remove int64
10
11	// voters is the set of addresses that have voted, so one address votes
12	// once. It is unexported: a caller reads [Challenge.Voters].
13	voters map[string]bool
14}
source

Challenge is an open objection to one entry.

Methods on Challenge

func HasVoted

method on Challenge
1func (c *Challenge) HasVoted(who address) bool
source

HasVoted reports whether who has already voted in this challenge.

func Open

method on Challenge
1func (c *Challenge) Open(now int64) bool
source

Open reports whether votes are still being taken at height now.

func Voters

method on Challenge
1func (c *Challenge) Voters() int
source

Voters is how many distinct addresses have voted.

type Entry

struct
 1type Entry struct {
 2	Key         string
 3	URL         string
 4	Description string
 5	Owner       address
 6	Deposit     int64 // what the owner locked to list it
 7	At          int64 // the block height it was listed at
 8	State       State
 9
10	// challenge is the open objection, or nil. It is cleared on resolution:
11	// the outcome is in the entry's state and in the ledger, and keeping a
12	// resolved challenge would be a second place to read it from.
13	challenge *Challenge
14}
source

Entry is one row of the list.

Methods on Entry

func Live

method on Entry
1func (e *Entry) Live() bool
source

Live reports whether the entry is on the list, challenged or not.

type Outcome

struct
1type Outcome struct {
2	Key    string
3	Kept   bool
4	Winner address
5	Amount int64 // credited to the winner
6	Keep   int64
7	Remove int64
8}
source

Outcome is what a resolution decided and who it paid.

type Registry

struct
 1type Registry struct {
 2	deposit         int64
 3	challengeBlocks int64
 4
 5	entries map[string]*Entry
 6	order   []string // keys in the order they were first listed
 7
 8	credits map[string]int64
 9	owed    int64
10	locked  int64
11}
source

Registry is the whole list: the entries, and the credits waiting to be withdrawn.

Methods on Registry

func Apply

method on Registry
1func (r *Registry) Apply(key, url, description string, owner address, paid, now int64) error
source

Apply lists an entry immediately, in exchange for exactly the deposit.

There is no application period in v0: the entry is on the list the moment the deposit is paid, and the check on it is that anybody can challenge it.

A key whose entry was removed is free again, and applying for it writes a fresh entry in the same position in the listing order.

func BondFor

method on Registry
1func (r *Registry) BondFor(key string) (int64, bool)
source

BondFor is what challenging key would cost, and whether it can be challenged at all.

The realm reads this BEFORE it reads the envelope, so a caller who attaches coins to a challenge of something unchallengeable is refused on the entry and not on the amount.

func Challenge

method on Registry
1func (r *Registry) Challenge(key string, challenger address, bond, now int64) error
source

Challenge opens an objection to an entry, against a bond equal to that entry's deposit, and sets the deadline at now + ChallengeBlocks.

An owner may not challenge their own entry. It would cost nothing (a kept entry credits its owner the bond, which here is their own) and it would make the entry immune to a real challenge for the whole window.

func ChallengeBlocks

method on Registry
1func (r *Registry) ChallengeBlocks() int64
source

ChallengeBlocks is how long a challenge takes to resolve.

func ChallengeOf

method on Registry
1func (r *Registry) ChallengeOf(key string) (*Challenge, bool)
source

ChallengeOf returns the open challenge against key, if there is one.

func Challenged

method on Registry
1func (r *Registry) Challenged() []*Entry
source

Challenged returns every entry with a challenge open against it, oldest listing first.

func Count

method on Registry
1func (r *Registry) Count() int
source

Count is how many entries are on the list right now.

func CreditOf

method on Registry
1func (r *Registry) CreditOf(who address) int64
source

CreditOf is what who can withdraw right now.

func Deposit

method on Registry
1func (r *Registry) Deposit() int64
source

Deposit is what listing costs.

func Get

method on Registry
1func (r *Registry) Get(key string) (*Entry, bool)
source

Get returns an entry by key, whatever its state, and whether it ever existed.

func IsListed

method on Registry
1func (r *Registry) IsListed(key string) bool
source

IsListed reports whether key is on the list right now. A challenged entry is still listed.

func Listed

method on Registry
1func (r *Registry) Listed() []*Entry
source

Listed returns every entry on the list, oldest first.

The order comes from a slice and not from iterating the map, so it is a stable sequence a Render can be pinned against rather than an insertion order that a delete-and-re-add would reshuffle.

func Locked

method on Registry
1func (r *Registry) Locked() int64
source

Locked is the deposits behind live entries plus the bonds behind open challenges.

Locked plus Registry.Owed is what the holding realm must have at its address: every ugnot it ever took is either still backing something or already assigned to somebody. A realm can assert that equality against its own balance, and this package's tests do.

func Owed

method on Registry
1func (r *Registry) Owed() int64
source

Owed is every credit not yet withdrawn.

func Records

method on Registry
1func (r *Registry) Records() int
source

Records is how many keys the registry has ever held, removed ones included.

func Resolve

method on Registry
1func (r *Registry) Resolve(key string, now int64) (Outcome, error)
source

Resolve closes a challenge whose deadline has passed. Anyone may call it: the two parties both have a reason to and neither can stall the other.

A majority of keep votes keeps the entry and credits its owner the challenger's bond. Otherwise the entry is removed and the challenger is credited the bond plus the deposit.

A TIE KEEPS THE ENTRY, including the tie of nobody voting at all. The incumbent paid first and is already at risk, so the burden is on the challenger to produce a reason; if ties went the other way, a challenge that convinced nobody would still win, and listing anything would be pointless. The cost of that choice is the one a challenger signs up for: being wrong costs the bond.

func Unlist

method on Registry
1func (r *Registry) Unlist(key string, owner address) error
source

Unlist takes an owner's own entry down and credits them the deposit back.

It is refused while a challenge is open, which is the whole point of the bond: an owner who could walk away mid-challenge would be risking nothing.

func Vote

method on Registry
1func (r *Registry) Vote(key string, voter address, keep bool, now int64) error
source

Vote records one address's opinion on an open challenge: keep the entry, or remove it.

One address, one vote, unweighted, and it cannot be changed. Anyone may vote, including the owner and the challenger, because excluding them would only move their vote to another address they control. See the package doc: this is sybil-prone on purpose rather than by oversight, and gating it is the job of the vouch realm.

func Withdraw

method on Registry
1func (r *Registry) Withdraw(who address) (int64, error)
source

Withdraw zeroes who's credit and returns what they were owed.

The holding realm sends the coins AFTER this returns. That ordering is the pattern: the credit is already gone from the ledger when control passes to the recipient, so a reentrant withdrawal finds ErrNothingOwed.

type State

ident
1type State uint8
source

State is where an entry stands.

Methods on State

func String

method on State
1func (s State) String() string
source

String names the state for a reader and for an event.

Imports 2

  • errors stdlib
  • strings stdlib

Source Files 4