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
  • render.gnogno
    1package grants23import (4	"strconv"5	"strings"67	"gno.land/p/moul/kit/ui/v0"8

    Functions

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

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

    "gno.land/p/nt/markdown/sanitize/v0"
    9)
    10
    11// Renderer turns a Program into the three markdown pages a grant program
    12// needs: the board, one request, and the ledger. Everything that differs
    13// between one program and the next is a field, so a realm's whole Render is
    14// building this literal and calling it.
    15//
    16// Build it INSIDE Render rather than storing it in a realm global: Note is a
    17// func, and Balance has to be read fresh on every call anyway.
    18//
    19// # Escaping
    20//
    21// Everything a caller typed (titles, bodies, reasons, proofs, notes) is
    22// escaped here, once, through p/moul/kit/ui and p/nt/markdown/sanitize. A
    23// realm does not repeat it and must not pre-escape: escaping twice shows the
    24// backslashes. The realm's OWN fields below (Title, Intro, Notes, Footer) are
    25// chrome, are written by whoever deployed the realm, and are emitted as-is so
    26// they can carry markdown.
    27type Renderer struct {
    28 Program *Program
    29 Title string // page heading
    30 Intro string // markdown under the heading, already wrapped
    31 Notes []string // extra paragraphs on the board page: house rules, scope
    32 Path string // package path, for the gnokey examples
    33 Link string // gnoweb route prefix, e.g. "/r/moul/grant/v0"
    34 Treasury address // where the money sits
    35 Balance int64 // what it holds right now
    36 Footer string // closing paragraph on the board page
    37
    38 // Note returns an extra callout for one request page, or "". Use it for
    39 // anything the library cannot know, such as a seeded request whose
    40 // applicant address has no key behind it.
    41 Note func(*Request) string
    42}
    43
    44// Render serves the board at "", one request at "request/<id>", and the money
    45// trail at "ledger".
    46func (rr Renderer) Render(path string) string {
    47 path = strings.TrimSpace(path)
    48 switch {
    49 case path == "" || path == "/":
    50 return rr.index()
    51 case path == "ledger":
    52 return rr.ledger()
    53 case strings.HasPrefix(path, "request/"):
    54 return rr.request(strings.TrimPrefix(path, "request/"))
    55 }
    56 return "# " + rr.Title + "\n\nNo page `" + path + "`. Try [the board](" + rr.Link +
    57 ") or [the ledger](" + rr.Link + ":ledger).\n"
    58}
    59
    60func (rr Renderer) index() string {
    61 p := rr.Program
    62 var b strings.Builder
    63 b.WriteString("# " + rr.Title + "\n\n")
    64 if rr.Intro != "" {
    65 b.WriteString(rr.Intro + "\n\n")
    66 }
    67 for _, n := range rr.Notes {
    68 b.WriteString(n + "\n\n")
    69 }
    70
    71 b.WriteString("## Treasury\n\n")
    72 b.WriteString("| Field | Value |\n|---|---|\n")
    73 b.WriteString("| Address | " + ui.AddrFull(rr.Treasury) + " |\n")
    74 b.WriteString("| Balance | " + rr.amount(rr.Balance) + " |\n")
    75 b.WriteString("| Promised | " + rr.amount(p.Committed()) + " |\n")
    76 b.WriteString("| Unpromised | " + rr.amount(p.Available(rr.Balance)) + " |\n")
    77 b.WriteString("| Donated | " + rr.amount(p.Raised()) + " |\n")
    78 b.WriteString("| Paid out | " + rr.amount(p.Disbursed()) + " |\n\n")
    79 if short := -p.Available(rr.Balance); short > 0 {
    80 b.WriteString("The board has promised " + rr.amount(short) + " more than it holds. Approving a\n")
    81 b.WriteString("grant does not escrow anything, so a tranche can come due against an empty\n")
    82 b.WriteString("treasury; a review that would release one is refused until the money is there.\n")
    83 b.WriteString("Top it up with `Fund`, or by sending coins straight to the address above.\n\n")
    84 }
    85
    86 b.WriteString("## Board\n\n")
    87 b.WriteString("A decision needs a majority of the members eligible to cast a ballot on it,\n")
    88 b.WriteString("recomputed every time: the party a decision is about never votes on it, so an\n")
    89 b.WriteString("applicant who sits on the board shrinks the room rather than packing it.\n\n")
    90 b.WriteString("| Member | Joined at height |\n|---|---|\n")
    91 for _, m := range p.Board.Members() {
    92 b.WriteString("| " + ui.AddrFull(m) + " | " + i64(p.Board.JoinedAt(m)) + " |\n")
    93 }
    94 b.WriteString("\n")
    95
    96 b.WriteString("## Requests\n\n")
    97 all := p.Board.List()
    98 if len(all) == 0 {
    99 b.WriteString("Nothing has been asked of this board yet. `Apply` is open to anyone.\n\n")
    100 } else {
    101 b.WriteString("| # | Kind | Title | For | Asking | Status | Ballots |\n|---|---|---|---|---|---|---|\n")
    102 for _, r := range all {
    103 yes, no := p.Board.Standing(r)
    104 b.WriteString("| [" + strconv.Itoa(r.ID) + "](" + rr.Link + ":request/" + strconv.Itoa(r.ID) + ")" +
    105 " | " + r.Kind.String() +
    106 " | " + ui.Cell(r.Title) +
    107 " | " + rr.who(r) +
    108 " | " + rr.ask(r) +
    109 " | " + r.Status.String() +
    110 " | " + strconv.Itoa(yes) + " for, " + strconv.Itoa(no) + " against" +
    111 " |\n")
    112 }
    113 b.WriteString("\n")
    114 }
    115
    116 b.WriteString("## Where the money went\n\n")
    117 b.WriteString("[The ledger](" + rr.Link + ":ledger) lists every donation in and every tranche\n")
    118 b.WriteString("out, with the height, the payee and the milestone it paid for.\n\n")
    119
    120 b.WriteString("## Calling it\n\n")
    121 b.WriteString("```sh\n")
    122 b.WriteString("# put money in\n")
    123 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func Fund -send 5000000" + p.Denom + " ...\n")
    124 b.WriteString("# ask for some\n")
    125 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func Apply \\\n")
    126 b.WriteString(" -args 'Port the thing' -args 'why it matters' -args 'design:100,ship:400' ...\n")
    127 b.WriteString("# ask on behalf of someone who cannot pay the gas to ask\n")
    128 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func ApplyFor \\\n")
    129 b.WriteString(" -args g1... -args 'their account is empty' \\\n")
    130 b.WriteString(" -args 'Port the thing' -args 'why it matters' -args 'design:100,ship:400' ...\n")
    131 b.WriteString("# decide (members only)\n")
    132 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func Vote -args 1 -args true -args 'reason' ...\n")
    133 b.WriteString("# show your work, then get paid for it\n")
    134 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func SubmitProof \\\n")
    135 b.WriteString(" -args 1 -args 0 -args url -args 'https://...' -args 'what it is' ...\n")
    136 b.WriteString("gnokey maketx call -pkgpath " + rr.Path + " -func Review -args 1 -args 0 -args true -args 'looks done' ...\n")
    137 b.WriteString("```\n")
    138 if rr.Footer != "" {
    139 b.WriteString("\n" + rr.Footer + "\n")
    140 }
    141 return b.String()
    142}
    143
    144func (rr Renderer) request(raw string) string {
    145 id, err := strconv.Atoi(raw)
    146 if err != nil {
    147 return "# " + rr.Title + "\n\n`" + raw + "` is not a request number.\n"
    148 }
    149 r := rr.Program.Board.Get(id)
    150 if r == nil {
    151 return "# " + rr.Title + "\n\nThere is no request " + raw + ".\n"
    152 }
    153 board := rr.Program.Board
    154
    155 var b strings.Builder
    156 b.WriteString("# Request " + strconv.Itoa(r.ID) + ": " + ui.Inline(r.Title) + "\n\n")
    157 b.WriteString("| Field | Value |\n|---|---|\n")
    158 b.WriteString("| Kind | " + r.Kind.String() + " |\n")
    159 b.WriteString("| Applicant | " + ui.AddrFull(r.Applicant) + " |\n")
    160 if r.OnBehalf() {
    161 b.WriteString("| Beneficiary | " + ui.AddrFull(r.Beneficiary) + " |\n")
    162 }
    163 if r.Kind == KindMember {
    164 b.WriteString("| Subject | " + ui.AddrFull(r.Subject) + " |\n")
    165 b.WriteString("| Effect | " + addRemove(r.Add) + " |\n")
    166 }
    167 b.WriteString("| Status | " + r.Status.String() + " |\n")
    168 b.WriteString("| Filed at height | " + i64(r.CreatedAt) + " |\n")
    169 if r.DecidedAt != 0 {
    170 b.WriteString("| Decided at height | " + i64(r.DecidedAt) + " |\n")
    171 }
    172 if r.Kind == KindGrant {
    173 b.WriteString("| Asking | " + rr.amount(r.Total()) + " |\n")
    174 b.WriteString("| Paid so far | " + rr.amount(r.Paid()) + " |\n")
    175 b.WriteString("| Still owed | " + rr.amount(r.Outstanding()) + " |\n")
    176 }
    177 b.WriteString("\n")
    178
    179 if r.OnBehalf() {
    180 b.WriteString("## Filed for someone else\n\n")
    181 b.WriteString("The applicant is not the payee: every tranche of this request pays\n")
    182 b.WriteString(ui.AddrFull(r.Beneficiary) + ". Both addresses are barred from voting on it, and\n")
    183 b.WriteString("either may submit a proof. The reason given:\n\n")
    184 b.WriteString("> " + ui.Inline(r.Reason) + "\n\n")
    185 b.WriteString("Nothing checks that reason. It is a claim by the applicant, and the point of\n")
    186 b.WriteString("printing it here is that a member can check it before voting.\n\n")
    187 }
    188
    189 if rr.Note != nil {
    190 if note := rr.Note(r); note != "" {
    191 b.WriteString(note + "\n\n")
    192 }
    193 }
    194
    195 if r.Body != "" {
    196 b.WriteString("## The ask\n\n" + block(r.Body) + "\n\n")
    197 }
    198
    199 b.WriteString("## Ballots\n\n")
    200 yes, no := board.Standing(r)
    201 recorded, against := r.Tally()
    202 b.WriteString("Needs " + strconv.Itoa(board.Majority(r)) + " of " + strconv.Itoa(board.Eligible(r)) +
    203 " eligible members. Standing: " + strconv.Itoa(yes) + " for, " + strconv.Itoa(no) + " against.\n")
    204 if recorded != yes || against != no {
    205 b.WriteString("On the record: " + strconv.Itoa(recorded) + " for, " + strconv.Itoa(against) +
    206 " against. The difference is ballots cast by people who have since left the board;\n")
    207 b.WriteString("they stay on the record and stop counting.\n")
    208 }
    209 b.WriteString("\n")
    210 if len(r.Votes) == 0 {
    211 b.WriteString("Nobody has voted yet.\n\n")
    212 } else {
    213 b.WriteString(rr.ballots(r.Votes, "for", "against"))
    214 }
    215
    216 if r.Kind != KindGrant {
    217 return b.String()
    218 }
    219
    220 b.WriteString("## Milestones\n\n")
    221 b.WriteString("Earned in order. The applicant submits a proof, the board reviews that proof,\n")
    222 b.WriteString("and the verdict that carries also moves the coins.\n\n")
    223 for i, m := range r.Milestones {
    224 b.WriteString("### " + strconv.Itoa(i+1) + ". " + ui.Inline(m.Title) + ": " + rr.amount(m.Amount) + "\n\n")
    225 b.WriteString(milestoneState(r, i, m) + "\n\n")
    226 for j, a := range m.Attempts {
    227 b.WriteString("**Proof " + strconv.Itoa(j+1) + "** (" + a.Proof.Kind + ", height " +
    228 i64(a.Proof.Height) + ", " + a.Outcome.String() + ")\n\n")
    229 b.WriteString(fence(a.Proof.Ref) + "\n\n")
    230 if a.Proof.Note != "" {
    231 b.WriteString(block(a.Proof.Note) + "\n\n")
    232 }
    233 if len(a.Reviews) == 0 {
    234 b.WriteString("No review yet.\n\n")
    235 continue
    236 }
    237 b.WriteString(rr.ballots(a.Reviews, "accept", "refuse"))
    238 }
    239 }
    240 return b.String()
    241}
    242
    243func (rr Renderer) ledger() string {
    244 p := rr.Program
    245 var b strings.Builder
    246 b.WriteString("# " + rr.Title + ": the ledger\n\n")
    247 b.WriteString("Every coin in and every coin out, in the order it happened. Coins sent\n")
    248 b.WriteString("straight to " + ui.AddrFull(rr.Treasury) + " land in the treasury\n")
    249 b.WriteString("without appearing here, which is why `Fund` exists: it is the same transfer\n")
    250 b.WriteString("with a name attached.\n\n")
    251
    252 b.WriteString("## In\n\n")
    253 if len(p.Donations) == 0 {
    254 b.WriteString("Nothing donated through `Fund` yet.\n\n")
    255 } else {
    256 b.WriteString("| Height | From | Amount |\n|---|---|---|\n")
    257 for _, d := range p.Donations {
    258 b.WriteString("| " + i64(d.Height) + " | " + ui.AddrFull(d.From) + " | " + rr.amount(d.Amount) + " |\n")
    259 }
    260 b.WriteString("\nTotal in: " + rr.amount(p.Raised()) + "\n\n")
    261 }
    262
    263 b.WriteString("## Out\n\n")
    264 if len(p.Payments) == 0 {
    265 b.WriteString("No tranche has been released yet.\n\n")
    266 } else {
    267 b.WriteString("| Height | Request | Milestone | To | Amount |\n|---|---|---|---|---|\n")
    268 for _, pay := range p.Payments {
    269 b.WriteString("| " + i64(pay.Height) +
    270 " | [" + strconv.Itoa(pay.Request) + "](" + rr.Link + ":request/" + strconv.Itoa(pay.Request) + ")" +
    271 " | " + strconv.Itoa(pay.Milestone+1) +
    272 " | " + ui.AddrFull(pay.To) +
    273 " | " + rr.amount(pay.Amount) + " |\n")
    274 }
    275 b.WriteString("\nTotal out: " + rr.amount(p.Disbursed()) + "\n\n")
    276 }
    277
    278 b.WriteString("Balance now: " + rr.amount(rr.Balance) + ". [Back to the board](" + rr.Link + ").\n")
    279 return b.String()
    280}
    281
    282// ballots renders one decision's ballots, marking anyone who has since left
    283// the board: their vote stays on the record and stops counting.
    284func (rr Renderer) ballots(bs []Ballot, forLabel, againstLabel string) string {
    285 var b strings.Builder
    286 b.WriteString("| Member | Vote | Height | Reason |\n|---|---|---|---|\n")
    287 for _, v := range bs {
    288 who := ui.AddrFull(v.Voter)
    289 if !rr.Program.Board.IsMember(v.Voter) {
    290 who += " (left the board)"
    291 }
    292 vote := againstLabel
    293 if v.Approve {
    294 vote = forLabel
    295 }
    296 b.WriteString("| " + who + " | " + vote + " | " + i64(v.Height) + " | " + orDash(ui.Cell(v.Reason)) + " |\n")
    297 }
    298 b.WriteString("\n")
    299 return b.String()
    300}
    301
    302func (rr Renderer) amount(n int64) string { return strconv.FormatInt(n, 10) + " " + rr.Program.Denom }
    303
    304// who is the board index's "For" column: who this request would pay, marked
    305// when that is not the address that filed it.
    306func (rr Renderer) who(r *Request) string {
    307 if r.Kind == KindMember {
    308 return "-"
    309 }
    310 if r.OnBehalf() {
    311 return ui.Addr(r.Beneficiary) + " (filed by " + ui.AddrText(r.Applicant) + ")"
    312 }
    313 return ui.Addr(r.Applicant)
    314}
    315
    316func (rr Renderer) ask(r *Request) string {
    317 if r.Kind == KindMember {
    318 return "nothing"
    319 }
    320 return rr.amount(r.Total())
    321}
    322
    323func milestoneState(r *Request, i int, m *Milestone) string {
    324 switch {
    325 case m.Released:
    326 return "Released at height " + i64(m.ReleasedAt) + ", paid to " + ui.AddrFull(r.Payee()) + "."
    327 case r.Status != Approved:
    328 return "Locked: the request is " + r.Status.String() + "."
    329 case m.Current() != nil:
    330 return "A proof is under review."
    331 case i == r.Next():
    332 return "Waiting on a proof from the applicant."
    333 }
    334 return "Locked until milestone " + strconv.Itoa(r.Next()+1) + " is released."
    335}
    336
    337func addRemove(add bool) string {
    338 if add {
    339 return "add to the board"
    340 }
    341 return "remove from the board"
    342}
    343
    344// block and fence escape a multi-line, user-written string for its slot and
    345// then trim the sanitizer's own trailing newlines.
    346//
    347// The trim is not cosmetic. gno collapses two consecutive blank lines in an
    348// Example's output the way Go does, so any page that emits one can never be
    349// pinned by an ExampleRender, and the only test that proves a whole page is
    350// the one that stops working.
    351func block(s string) string { return strings.TrimSpace(sanitize.Block(s)) }
    352
    353func fence(s string) string { return strings.TrimSpace(sanitize.CodeBlock(s)) }
    354
    355func i64(n int64) string { return strconv.FormatInt(n, 10) }
    356
    357func orDash(s string) string {
    358 if s == "" {
    359 return "-"
    360 }
    361 return s
    362}
    363