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
  • lifecycle.gnogno
    1package grants23// This file holds every state transition of the board. Each one takes the4// acting address and the block height from the caller rather than reading5// them from the chain, which is what keeps the package pure and the whole6// lifecycle exercisable in a plain unit test.78

    Functions

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

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

    // NewMilestone is a small constructor so callers do not build the struct (and
    9// its Attempts slice) by hand.
    10func NewMilestone(title string, amount int64) *Milestone {
    11 return &Milestone{Title: title, Amount: amount}
    12}
    13
    14// Submit files a grant application for the applicant themselves. Anyone may
    15// apply; being a member is not required and does not help, since a member is
    16// barred from voting on their own request.
    17func (b *Board) Submit(applicant address, title, body string, ms []*Milestone, height int64) (*Request, error) {
    18 return b.SubmitFor(applicant, applicant, "", title, body, ms, height)
    19}
    20
    21// SubmitFor files a grant application whose money goes to beneficiary rather
    22// than to the applicant, with a reason for the detour.
    23//
    24// The reason is required, and it is required because it is the only thing on
    25// the page that answers the question a reader will have: why is this person
    26// asking for someone else's money. The usual honest answer is that the payee
    27// has an empty account and cannot pay the gas to ask, which is exactly the
    28// case a grant board exists to serve and exactly the case an impersonation
    29// looks like. Making the claim explicit is what lets a member check it.
    30//
    31// Passing the applicant's own address is the same as Submit and needs no
    32// reason.
    33func (b *Board) SubmitFor(applicant, beneficiary address, reason, title, body string, ms []*Milestone, height int64) (*Request, error) {
    34 if !applicant.IsValid() || !beneficiary.IsValid() {
    35 return nil, ErrBadAddress
    36 }
    37 if len(reason) > MaxReason {
    38 return nil, ErrBadReason
    39 }
    40 if beneficiary != applicant && reason == "" {
    41 return nil, ErrNeedReason
    42 }
    43 if err := checkTitle(title); err != nil {
    44 return nil, err
    45 }
    46 if len(body) > MaxBody {
    47 return nil, ErrBadBody
    48 }
    49 if len(ms) == 0 || len(ms) > MaxMilestones {
    50 return nil, ErrBadMilestones
    51 }
    52 for _, m := range ms {
    53 if m == nil || m.Amount <= 0 {
    54 return nil, ErrBadAmount
    55 }
    56 if err := checkTitle(m.Title); err != nil {
    57 return nil, err
    58 }
    59 }
    60 return b.file(&Request{
    61 Kind: KindGrant,
    62 Applicant: applicant,
    63 Beneficiary: beneficiary,
    64 Reason: reason,
    65 Title: title,
    66 Body: body,
    67 Milestones: ms,
    68 CreatedAt: height,
    69 }), nil
    70}
    71
    72// SubmitMemberChange files a request to add or remove a board member. Only a
    73// member may file one: opening the board's own composition to anyone with a
    74// keypair is how a grant board gets captured.
    75func (b *Board) SubmitMemberChange(proposer, subject address, add bool, body string, height int64) (*Request, error) {
    76 if !b.IsMember(proposer) {
    77 return nil, ErrNotMember
    78 }
    79 if !subject.IsValid() {
    80 return nil, ErrBadAddress
    81 }
    82 if len(body) > MaxBody {
    83 return nil, ErrBadBody
    84 }
    85 switch {
    86 case add && b.IsMember(subject):
    87 return nil, ErrIsMember
    88 case !add && !b.IsMember(subject):
    89 return nil, ErrNotMember
    90 case !add && b.MemberCount() == 1:
    91 return nil, ErrLastMember
    92 }
    93 title := "Remove " + subject.String()
    94 if add {
    95 title = "Add " + subject.String()
    96 }
    97 return b.file(&Request{
    98 Kind: KindMember,
    99 Applicant: proposer,
    100 Beneficiary: proposer,
    101 Title: title,
    102 Body: body,
    103 Subject: subject,
    104 Add: add,
    105 CreatedAt: height,
    106 }), nil
    107}
    108
    109func (b *Board) file(r *Request) *Request {
    110 r.ID = b.nextID
    111 b.nextID++
    112 b.requests.Set(idKey(r.ID), r)
    113 return r
    114}
    115
    116// Withdraw lets an applicant pull their own request before it is decided.
    117func (b *Board) Withdraw(caller address, id int, height int64) error {
    118 r := b.Get(id)
    119 if r == nil {
    120 return ErrNoRequest
    121 }
    122 if r.Applicant != caller {
    123 return ErrNotApplicant
    124 }
    125 if r.Status != Pending {
    126 return ErrNotPending
    127 }
    128 r.Status, r.DecidedAt = Withdrawn, height
    129 return nil
    130}
    131
    132// Standing counts only the ballots of addresses that are members right now.
    133// Decisions use this, not Request.Tally: a ballot is a permanent record of
    134// what someone said, but it stops carrying weight the moment they leave the
    135// board. The two numbers differ exactly when a voter has since been removed,
    136// and a good renderer shows both.
    137func (b *Board) Standing(r *Request) (yes, no int) { return b.standing(r.Votes) }
    138
    139// Decides previews what a ballot would do without casting it, so a caller can
    140// check a precondition it cannot roll back: a realm, for instance, refusing
    141// to let a grant carry that its treasury cannot cover. It reports false for a
    142// ballot that would be refused anyway.
    143func (b *Board) Decides(id int, voter address, approve bool) (bool, Status) {
    144 r := b.Get(id)
    145 if r == nil || r.Status != Pending {
    146 return false, Pending
    147 }
    148 if !b.IsMember(voter) || r.Excludes(voter) || r.BallotOf(voter) != nil {
    149 return false, Pending
    150 }
    151 yes, no := b.Standing(r)
    152 if approve {
    153 yes++
    154 } else {
    155 no++
    156 }
    157 switch m := b.Majority(r); {
    158 case yes >= m:
    159 return true, Approved
    160 case no >= m:
    161 return true, Rejected
    162 }
    163 return false, Pending
    164}
    165
    166// ReviewDecides is Decides for a proof review: it reports whether this
    167// verdict would close the attempt under review, and how. A realm uses it to
    168// check, before recording anything, that it can actually pay a tranche it is
    169// about to release.
    170func (b *Board) ReviewDecides(id, idx int, voter address, accept bool) (bool, Outcome) {
    171 r := b.Get(id)
    172 if r == nil || r.Kind != KindGrant || r.Status != Approved {
    173 return false, UnderReview
    174 }
    175 if idx < 0 || idx >= len(r.Milestones) {
    176 return false, UnderReview
    177 }
    178 a := r.Milestones[idx].Current()
    179 if a == nil {
    180 return false, UnderReview
    181 }
    182 if !b.IsMember(voter) || r.Excludes(voter) || find(a.Reviews, voter) != nil {
    183 return false, UnderReview
    184 }
    185 yes, no := b.standing(a.Reviews)
    186 if accept {
    187 yes++
    188 } else {
    189 no++
    190 }
    191 switch m := b.Majority(r); {
    192 case yes >= m:
    193 return true, Accepted
    194 case no >= m:
    195 return true, Refused
    196 }
    197 return false, UnderReview
    198}
    199
    200// standing counts ballots from current members only. See Standing.
    201func (b *Board) standing(bs []Ballot) (yes, no int) {
    202 for _, v := range bs {
    203 if !b.IsMember(v.Voter) {
    204 continue
    205 }
    206 if v.Approve {
    207 yes++
    208 } else {
    209 no++
    210 }
    211 }
    212 return yes, no
    213}
    214
    215// Vote casts one member's ballot on a request and returns the status it left
    216// the request in. A member votes once; there is no changing your mind, which
    217// is the price of every ballot being a permanent public statement.
    218func (b *Board) Vote(voter address, id int, approve bool, reason string, height int64) (Status, error) {
    219 r := b.Get(id)
    220 if r == nil {
    221 return Pending, ErrNoRequest
    222 }
    223 if !b.IsMember(voter) {
    224 return r.Status, ErrNotMember
    225 }
    226 if r.Status != Pending {
    227 return r.Status, ErrNotPending
    228 }
    229 if r.Excludes(voter) {
    230 return r.Status, ErrConflict
    231 }
    232 if r.BallotOf(voter) != nil {
    233 return r.Status, ErrAlreadyVoted
    234 }
    235 if len(reason) > MaxReason {
    236 return r.Status, ErrBadReason
    237 }
    238
    239 r.Votes = append(r.Votes, Ballot{Voter: voter, Approve: approve, Reason: reason, Height: height})
    240
    241 yes, no := b.Standing(r)
    242 switch m := b.Majority(r); {
    243 case yes >= m:
    244 r.Status, r.DecidedAt = Approved, height
    245 if r.Kind == KindMember {
    246 b.applyMemberChange(r, height)
    247 }
    248 case no >= m:
    249 r.Status, r.DecidedAt = Rejected, height
    250 }
    251 return r.Status, nil
    252}
    253
    254// applyMemberChange is the whole execution engine for KindMember: a carried
    255// membership request takes effect at once, with nothing to claim afterwards,
    256// so it goes straight from Approved to Completed.
    257func (b *Board) applyMemberChange(r *Request, height int64) {
    258 if r.Add {
    259 b.members.Set(r.Subject.String(), height)
    260 } else {
    261 b.members.Remove(r.Subject.String())
    262 }
    263 r.Status = Completed
    264}
    265
    266// SubmitProof offers evidence for the next unreleased milestone of an
    267// approved grant. Milestones are earned in order, and only one proof is under
    268// review at a time.
    269//
    270// Either the applicant or the beneficiary may submit. On a request filed for
    271// an empty account, the payee cannot transact until the first tranche lands,
    272// so restricting this to the payee would strand the grant it was filed to
    273// unstick; restricting it to the filer would leave the payee unable to show
    274// their own work once they can.
    275func (b *Board) SubmitProof(caller address, id, idx int, p Proof) error {
    276 r := b.Get(id)
    277 if r == nil {
    278 return ErrNoRequest
    279 }
    280 if r.Kind != KindGrant || r.Status != Approved {
    281 return ErrNotApproved
    282 }
    283 if r.Applicant != caller && r.Payee() != caller {
    284 return ErrNotApplicant
    285 }
    286 if idx < 0 || idx >= len(r.Milestones) {
    287 return ErrNoMilestone
    288 }
    289 if idx != r.Next() {
    290 return ErrOutOfOrder
    291 }
    292 if err := checkProof(p); err != nil {
    293 return err
    294 }
    295 m := r.Milestones[idx]
    296 if m.Current() != nil {
    297 return ErrProofPending
    298 }
    299 m.Attempts = append(m.Attempts, &Attempt{Proof: p, Outcome: UnderReview})
    300 return nil
    301}
    302
    303// Review casts a member's verdict on the proof currently under review and
    304// returns the outcome that verdict left the attempt in. Accepting it releases
    305// the tranche, which in this package means marking it Released and nothing
    306// more; moving the coins is the caller's job, and Request.Paid tells it how
    307// much it now owes. Refusing closes the attempt and lets the applicant try
    308// again with better evidence.
    309func (b *Board) Review(voter address, id, idx int, accept bool, reason string, height int64) (Outcome, error) {
    310 r := b.Get(id)
    311 if r == nil {
    312 return UnderReview, ErrNoRequest
    313 }
    314 if !b.IsMember(voter) {
    315 return UnderReview, ErrNotMember
    316 }
    317 if r.Kind != KindGrant || r.Status != Approved {
    318 return UnderReview, ErrNotApproved
    319 }
    320 if idx < 0 || idx >= len(r.Milestones) {
    321 return UnderReview, ErrNoMilestone
    322 }
    323 if r.Excludes(voter) {
    324 return UnderReview, ErrConflict
    325 }
    326 if len(reason) > MaxReason {
    327 return UnderReview, ErrBadReason
    328 }
    329 m := r.Milestones[idx]
    330 a := m.Current()
    331 if a == nil {
    332 return UnderReview, ErrNoProof
    333 }
    334 if find(a.Reviews, voter) != nil {
    335 return UnderReview, ErrAlreadyVoted
    336 }
    337
    338 a.Reviews = append(a.Reviews, Ballot{Voter: voter, Approve: accept, Reason: reason, Height: height})
    339
    340 yes, no := b.standing(a.Reviews)
    341 switch mj := b.Majority(r); {
    342 case yes >= mj:
    343 a.Outcome, a.ClosedAt = Accepted, height
    344 m.Released, m.ReleasedAt = true, height
    345 if r.Next() == -1 {
    346 r.Status = Completed
    347 }
    348 case no >= mj:
    349 a.Outcome, a.ClosedAt = Refused, height
    350 }
    351 return a.Outcome, nil
    352}
    353
    354func checkTitle(s string) error {
    355 if len(s) == 0 || len(s) > MaxTitle {
    356 return ErrBadTitle
    357 }
    358 return nil
    359}
    360
    361func checkProof(p Proof) error {
    362 switch p.Kind {
    363 case "url", "hash", "text":
    364 default:
    365 return ErrBadProof
    366 }
    367 if len(p.Ref) == 0 || len(p.Ref) > MaxRef || len(p.Note) > MaxNote {
    368 return ErrBadProof
    369 }
    370 return nil
    371}
    372