PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/grants/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / grants
Files
9 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/grants/v0
gno
0.9

Files (9)

  • README.mdmarkdown
  • gnomod.tomltoml
  • grants.gnogno
  • lifecycle.gnogno
  • program.gnogno
  • render.gnogno
  • grants_test.gnogno
  • program_test.gnogno
  • render_test.gnogno
  • grants.gnogno
    1// Package grants is a grant board governed by a member set: anyone may ask2// the board for money, the members vote in the open, and the money leaves in3// tranches that each have to be earned by producing a proof the members4// accept.5//6// The package is pure. It holds no coins, reads no chain state and imports7// nothing from `chain`: the caller supplies the acting address and the block

    Functions

    not supported for pure packages by the node (vm/qfuncs)

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    8// height, and the caller performs the transfer. That is what makes the whole
    9// state machine unit-testable without a node, and it is what lets the same
    10// board be driven by a realm, by a test, or by a different payment rail.
    11// Live board: gno.land/r/moul/grant/v0.
    12//
    13// # The shape of a grant
    14//
    15// A Request is a title, a body, and an ordered list of Milestones, each with
    16// its own amount. Approving a request does not pay anything: it only makes the
    17// first milestone claimable. To get a tranche the applicant submits a Proof
    18// (a URL, a hash, or plain text) and the members review that specific proof.
    19// A rejected proof does not kill the grant, it just closes one Attempt; the
    20// applicant submits another. Every attempt, accepted or not, stays on the
    21// record with the ballots that decided it.
    22//
    23// # Asking for someone else
    24//
    25// A request has an applicant, who files it and answers for it, and a
    26// beneficiary, who gets paid. SubmitFor separates the two and takes a reason
    27// for the detour; Submit is the common case where they are the same address.
    28//
    29// The split is not a convenience. An account with nothing in it cannot pay the
    30// gas to ask for its first coins, so on a board whose whole purpose is to fund
    31// empty accounts, "someone else files it" is the ONLY path that works. Both
    32// addresses are then barred from voting on the request, and either of them may
    33// submit a proof, for the same reason: the payee may not be able to transact
    34// at all until the first tranche lands.
    35//
    36// # Who may vote
    37//
    38// Every member, once, per decision. The party the decision is about is
    39// excluded: the applicant does not vote on their own grant, and the subject of
    40// a membership change does not vote on their own membership. The bar is a
    41// majority of the addresses that are actually eligible, recomputed on each
    42// ballot, so a board that grows mid-vote raises its own bar.
    43//
    44// # What this deliberately does not do
    45//
    46// No weights, no delegation, no quadratic anything, no deadline. A request
    47// with no majority either way simply stays Pending until someone breaks the
    48// tie or the applicant withdraws it. Those are all reasonable things to add on
    49// top; none of them is needed to show the shape.
    50package grants
    51
    52import (
    53 "errors"
    54 "strconv"
    55
    56 "gno.land/p/nt/avl/v0"
    57)
    58
    59// Errors returned by the mutators. They are sentinels so a realm can map them
    60// onto its own messages, or simply panic with them.
    61var (
    62 ErrNotMember = errors.New("grants: not a board member")
    63 ErrNotApplicant = errors.New("grants: only the applicant may do this")
    64 ErrNoRequest = errors.New("grants: no such request")
    65 ErrNoMilestone = errors.New("grants: no such milestone")
    66 ErrNotPending = errors.New("grants: the request is not pending")
    67 ErrNotApproved = errors.New("grants: the request is not approved")
    68 ErrAlreadyVoted = errors.New("grants: this member already voted")
    69 ErrConflict = errors.New("grants: the party a decision is about may not vote on it")
    70 ErrOutOfOrder = errors.New("grants: milestones are claimed in order")
    71 ErrProofPending = errors.New("grants: a proof is already under review")
    72 ErrNoProof = errors.New("grants: no proof is under review")
    73 ErrBadTitle = errors.New("grants: title must be 1 to 100 characters")
    74 ErrBadBody = errors.New("grants: body must be at most 2000 characters")
    75 ErrBadMilestones = errors.New("grants: a grant needs 1 to 10 milestones")
    76 ErrBadAmount = errors.New("grants: every milestone must ask for a positive amount")
    77 ErrBadProof = errors.New("grants: proof kind must be url, hash or text, with a reference")
    78 ErrBadReason = errors.New("grants: reason must be at most 300 characters")
    79 ErrNeedReason = errors.New("grants: filing for someone else needs a reason")
    80 ErrBadAddress = errors.New("grants: invalid address")
    81 ErrIsMember = errors.New("grants: already a board member")
    82 ErrLastMember = errors.New("grants: the last member cannot be removed")
    83 ErrNothingSent = errors.New("grants: a donation must be a positive amount")
    84 ErrUnderfunded = errors.New("grants: the treasury cannot cover this tranche")
    85 ErrBadSpec = errors.New("grants: milestones must read \"title:amount,title:amount\"")
    86)
    87
    88// Limits every input is checked against. Exported so a UI can pre-validate.
    89const (
    90 MaxTitle = 100
    91 MaxBody = 2000
    92 MaxReason = 300
    93 MaxRef = 300
    94 MaxNote = 500
    95 MaxMilestones = 10
    96)
    97
    98// Status is where a request sits in its lifecycle.
    99type Status int
    100
    101const (
    102 Pending Status = iota // open for member ballots
    103 Approved // carried; milestones can be earned
    104 Rejected // a majority voted no
    105 Completed // every milestone released
    106 Withdrawn // the applicant pulled it before a decision
    107)
    108
    109func (s Status) String() string {
    110 switch s {
    111 case Pending:
    112 return "pending"
    113 case Approved:
    114 return "approved"
    115 case Rejected:
    116 return "rejected"
    117 case Completed:
    118 return "completed"
    119 case Withdrawn:
    120 return "withdrawn"
    121 }
    122 return "unknown"
    123}
    124
    125// Kind says what carrying a request actually does.
    126type Kind int
    127
    128const (
    129 KindGrant Kind = iota // pays out, milestone by milestone
    130 KindMember // adds or removes a board member, immediately
    131)
    132
    133func (k Kind) String() string {
    134 if k == KindMember {
    135 return "membership"
    136 }
    137 return "grant"
    138}
    139
    140// Outcome is the result of one proof attempt.
    141type Outcome int
    142
    143const (
    144 UnderReview Outcome = iota // members are still voting on this proof
    145 Accepted // the tranche is earned
    146 Refused // this proof did not convince; another may
    147)
    148
    149func (o Outcome) String() string {
    150 switch o {
    151 case Accepted:
    152 return "accepted"
    153 case Refused:
    154 return "refused"
    155 }
    156 return "under review"
    157}
    158
    159// Ballot is one member's vote on one decision, with the reason they gave.
    160// Reasons are not optional decoration: they are the only part of a vote that
    161// tells an applicant what to fix.
    162type Ballot struct {
    163 Voter address
    164 Approve bool
    165 Reason string
    166 Height int64
    167}
    168
    169// Proof is the evidence an applicant offers for one milestone. Kind is "url",
    170// "hash" or "text"; the board decides what it is worth.
    171type Proof struct {
    172 Kind string
    173 Ref string
    174 Note string
    175 Height int64
    176}
    177
    178// Attempt is one proof and the review it got. A milestone keeps every attempt,
    179// so a refused proof and the reasons it was refused stay readable forever.
    180type Attempt struct {
    181 Proof Proof
    182 Reviews []Ballot
    183 Outcome Outcome
    184 ClosedAt int64
    185}
    186
    187// Milestone is one tranche of a grant: what has to be shown, and what it pays.
    188type Milestone struct {
    189 Title string
    190 Amount int64 // in the smallest unit of whatever the realm pays in
    191 Attempts []*Attempt
    192 Released bool
    193 ReleasedAt int64
    194}
    195
    196// Current returns the attempt still under review, or nil.
    197func (m *Milestone) Current() *Attempt {
    198 if len(m.Attempts) == 0 {
    199 return nil
    200 }
    201 last := m.Attempts[len(m.Attempts)-1]
    202 if last.Outcome == UnderReview {
    203 return last
    204 }
    205 return nil
    206}
    207
    208// Request is a grant application or a membership change.
    209type Request struct {
    210 ID int
    211 Kind Kind
    212 Applicant address // who filed it and answers for it
    213 Beneficiary address // who gets paid; equal to Applicant on a self request
    214 Reason string // why the applicant is asking on someone else's behalf
    215 Title string
    216 Body string
    217 Milestones []*Milestone
    218 Subject address // KindMember only: the address being added or removed
    219 Add bool // KindMember only: true adds, false removes
    220 Votes []Ballot
    221 Status Status
    222 CreatedAt int64
    223 DecidedAt int64
    224}
    225
    226// OnBehalf reports whether this grant was filed by one address for another.
    227func (r *Request) OnBehalf() bool {
    228 return r.Kind == KindGrant && r.Beneficiary != r.Applicant
    229}
    230
    231// Payee is the address a released tranche pays. It is the beneficiary, which
    232// is the applicant unless the request was filed for someone else.
    233func (r *Request) Payee() address {
    234 if r.Beneficiary.IsValid() {
    235 return r.Beneficiary
    236 }
    237 return r.Applicant
    238}
    239
    240// Total is the sum of every milestone, released or not.
    241func (r *Request) Total() int64 {
    242 var n int64
    243 for _, m := range r.Milestones {
    244 n += m.Amount
    245 }
    246 return n
    247}
    248
    249// Paid is what the milestones released so far add up to.
    250func (r *Request) Paid() int64 {
    251 var n int64
    252 for _, m := range r.Milestones {
    253 if m.Released {
    254 n += m.Amount
    255 }
    256 }
    257 return n
    258}
    259
    260// Outstanding is what an approved request can still cost the treasury. It is
    261// zero for anything not approved, which is what makes it safe to sum across
    262// the board as the committed amount.
    263func (r *Request) Outstanding() int64 {
    264 if r.Status != Approved {
    265 return 0
    266 }
    267 return r.Total() - r.Paid()
    268}
    269
    270// Next is the index of the first unreleased milestone, or -1 when the grant is
    271// fully paid. Milestones are earned in order.
    272func (r *Request) Next() int {
    273 for i, m := range r.Milestones {
    274 if !m.Released {
    275 return i
    276 }
    277 }
    278 return -1
    279}
    280
    281// Tally counts the ballots cast on the request itself.
    282func (r *Request) Tally() (yes, no int) {
    283 return count(r.Votes)
    284}
    285
    286// BallotOf returns the member's ballot on the request, or nil.
    287func (r *Request) BallotOf(voter address) *Ballot {
    288 return find(r.Votes, voter)
    289}
    290
    291// Excludes reports whether addr is barred from voting on this request because
    292// the decision is about them.
    293//
    294// A grant excludes BOTH parties, not just the one who typed the transaction:
    295// letting a member be paid by a request a friend filed for them is the same
    296// conflict with one more step in it.
    297func (r *Request) Excludes(addr address) bool {
    298 if r.Kind == KindMember {
    299 return addr == r.Subject
    300 }
    301 return addr == r.Applicant || addr == r.Payee()
    302}
    303
    304func count(bs []Ballot) (yes, no int) {
    305 for _, b := range bs {
    306 if b.Approve {
    307 yes++
    308 } else {
    309 no++
    310 }
    311 }
    312 return yes, no
    313}
    314
    315func find(bs []Ballot, voter address) *Ballot {
    316 for i := range bs {
    317 if bs[i].Voter == voter {
    318 return &bs[i]
    319 }
    320 }
    321 return nil
    322}
    323
    324// Board is the whole state of one grant program: who decides, and what has
    325// been asked of them.
    326type Board struct {
    327 members *avl.Tree // address string -> int64 join height
    328 requests *avl.Tree // zero-padded id -> *Request
    329 nextID int
    330}
    331
    332// New returns a board whose founding members are the given addresses, all
    333// recorded as having joined at height 0. Duplicates and invalid addresses are
    334// skipped; a board with no members accepts no decisions until one is added, so
    335// callers normally pass at least one.
    336func New(founders ...address) *Board {
    337 b := &Board{members: avl.NewTree(), requests: avl.NewTree(), nextID: 1}
    338 for _, f := range founders {
    339 if f.IsValid() {
    340 b.members.Set(f.String(), int64(0))
    341 }
    342 }
    343 return b
    344}
    345
    346// IsMember reports whether addr sits on the board.
    347func (b *Board) IsMember(addr address) bool { return b.members.Has(addr.String()) }
    348
    349// MemberCount is the number of addresses on the board.
    350func (b *Board) MemberCount() int { return b.members.Size() }
    351
    352// Members lists the board in address order, which is stable across calls.
    353func (b *Board) Members() []address {
    354 out := make([]address, 0, b.members.Size())
    355 b.members.Iterate("", "", func(k string, _ any) bool {
    356 out = append(out, address(k))
    357 return false
    358 })
    359 return out
    360}
    361
    362// JoinedAt returns the height a member joined, or -1 for a non-member.
    363func (b *Board) JoinedAt(addr address) int64 {
    364 v := b.members.Get(addr.String())
    365 if v == nil {
    366 return -1
    367 }
    368 return v.(int64)
    369}
    370
    371// Eligible is how many members may vote on a request: everyone but the parties
    372// the decision is about.
    373func (b *Board) Eligible(r *Request) int {
    374 n := 0
    375 b.members.Iterate("", "", func(k string, _ any) bool {
    376 if !r.Excludes(address(k)) {
    377 n++
    378 }
    379 return false
    380 })
    381 return n
    382}
    383
    384// Majority is the number of concurring ballots a decision on r needs. It is
    385// recomputed on every ballot, so a board that changes size mid-vote moves its
    386// own bar rather than freezing a stale one.
    387func (b *Board) Majority(r *Request) int {
    388 n := b.Eligible(r)
    389 if n <= 0 {
    390 return 1 // an unreachable bar beats a bar of zero
    391 }
    392 return n/2 + 1
    393}
    394
    395// Get returns a request by id, or nil.
    396func (b *Board) Get(id int) *Request {
    397 v := b.requests.Get(idKey(id))
    398 if v == nil {
    399 return nil
    400 }
    401 return v.(*Request)
    402}
    403
    404// Size is the number of requests ever filed.
    405func (b *Board) Size() int { return b.requests.Size() }
    406
    407// List returns every request in id order.
    408func (b *Board) List() []*Request {
    409 out := make([]*Request, 0, b.requests.Size())
    410 b.requests.Iterate("", "", func(_ string, v any) bool {
    411 out = append(out, v.(*Request))
    412 return false
    413 })
    414 return out
    415}
    416
    417// Committed is what the treasury still owes on approved grants. A realm that
    418// keeps its balance at or above this number can always pay a released tranche.
    419func (b *Board) Committed() int64 {
    420 var n int64
    421 b.requests.Iterate("", "", func(_ string, v any) bool {
    422 n += v.(*Request).Outstanding()
    423 return false
    424 })
    425 return n
    426}
    427
    428// Disbursed is everything the board has ever released.
    429func (b *Board) Disbursed() int64 {
    430 var n int64
    431 b.requests.Iterate("", "", func(_ string, v any) bool {
    432 n += v.(*Request).Paid()
    433 return false
    434 })
    435 return n
    436}
    437
    438// idKey pads an id so avl iteration is numeric, not lexicographic: unpadded
    439// keys sort "1","10","2" and every listing would lose its order past nine
    440// entries. ufmt has no width flags in gno, so the padding is by hand.
    441func idKey(id int) string {
    442 s := strconv.Itoa(id)
    443 for len(s) < 8 {
    444 s = "0" + s
    445 }
    446 return s
    447}
    448