program.gno
5.19 Kb · 156 lines
1package grants
2
3import (
4 "strconv"
5 "strings"
6)
7
8// Program is a Board plus the money that moves around it: what came in, what
9// went out, and the denomination it is all counted in.
10//
11// It still holds no coins and calls no banker. A realm hands it the current
12// treasury balance whenever a decision depends on one, and executes the
13// Payment it hands back. That split is the whole point: everything a grant
14// program decides is testable without a node, and the realm on top is thin
15// enough to read in one screen.
16type Program struct {
17 Board *Board
18 Denom string
19 Donations []Donation
20 Payments []Payment
21}
22
23// Donation is one top-up of the treasury, credited to a name.
24type Donation struct {
25 Height int64
26 From address
27 Amount int64
28}
29
30// Payment is one released tranche. The ledger of these is a program's answer
31// to "where did the money go".
32type Payment struct {
33 Height int64
34 Request int
35 Milestone int
36 To address
37 Amount int64
38}
39
40// NewProgram returns a program whose board is seated with founders. denom is
41// the coin everything is counted in ("ugnot" on gno.land); it is carried so a
42// renderer can label amounts without the realm telling it twice.
43func NewProgram(denom string, founders ...address) *Program {
44 return &Program{Board: New(founders...), Denom: denom}
45}
46
47// Fund records a top-up. The coins have already moved by the time this is
48// called: a realm calls it to put a name next to a transfer that would
49// otherwise be anonymous.
50func (p *Program) Fund(from address, amount, height int64) error {
51 if amount <= 0 {
52 return ErrNothingSent
53 }
54 if !from.IsValid() {
55 return ErrBadAddress
56 }
57 p.Donations = append(p.Donations, Donation{Height: height, From: from, Amount: amount})
58 return nil
59}
60
61// Apply files a grant request from a milestone spec, "design:100,ship:400".
62// See ParseMilestones for the grammar.
63func (p *Program) Apply(applicant address, title, body, spec string, height int64) (*Request, error) {
64 return p.ApplyFor(applicant, applicant, "", title, body, spec, height)
65}
66
67// ApplyFor is Apply for a request whose money goes to someone other than the
68// address filing it. See Board.SubmitFor for why the reason is required.
69func (p *Program) ApplyFor(applicant, beneficiary address, reason, title, body, spec string, height int64) (*Request, error) {
70 ms, err := ParseMilestones(spec)
71 if err != nil {
72 return nil, err
73 }
74 return p.Board.SubmitFor(applicant, beneficiary, reason, title, body, ms, height)
75}
76
77// Review casts a member's verdict on the proof under review and, when that
78// verdict releases the tranche, records the payment and hands it back for the
79// caller to execute.
80//
81// balance is what the treasury holds right now. A verdict that would release
82// more than that is refused with ErrUnderfunded and changes NOTHING: no
83// ballot, no release, no payment. That ordering is the reason Board.Review is
84// not called directly by a realm. A refusal is always free to record, since it
85// costs the treasury nothing.
86func (p *Program) Review(voter address, id, idx int, accept bool, reason string, height, balance int64) (Outcome, *Payment, error) {
87 if releases, out := p.Board.ReviewDecides(id, idx, voter, accept); releases && out == Accepted {
88 if amount := p.Board.Get(id).Milestones[idx].Amount; balance < amount {
89 return UnderReview, nil, ErrUnderfunded
90 }
91 }
92
93 out, err := p.Board.Review(voter, id, idx, accept, reason, height)
94 if err != nil || out != Accepted {
95 return out, nil, err
96 }
97
98 r := p.Board.Get(id)
99 pay := Payment{
100 Height: height,
101 Request: id,
102 Milestone: idx,
103 To: r.Payee(),
104 Amount: r.Milestones[idx].Amount,
105 }
106 p.Payments = append(p.Payments, pay)
107 return out, &pay, nil
108}
109
110// Raised is everything ever donated through Fund.
111func (p *Program) Raised() int64 {
112 var n int64
113 for _, d := range p.Donations {
114 n += d.Amount
115 }
116 return n
117}
118
119// Disbursed is everything ever released.
120func (p *Program) Disbursed() int64 { return p.Board.Disbursed() }
121
122// Committed is what approved grants can still claim.
123func (p *Program) Committed() int64 { return p.Board.Committed() }
124
125// Available is balance minus what is already promised. It goes NEGATIVE when
126// the board has approved more than the treasury holds, which is allowed on
127// purpose: a board that can only approve what it already has cannot approve
128// anything before a donor shows up. Renderers say so out loud.
129func (p *Program) Available(balance int64) int64 { return balance - p.Committed() }
130
131// ParseMilestones reads "design:100,ship:400" into milestones, amounts in the
132// program's denomination, paid in the order written. A title may not contain a
133// comma, and the amount is whatever follows the LAST colon, so
134// "port gno:land tooling:250" parses the way it reads.
135func ParseMilestones(spec string) ([]*Milestone, error) {
136 out := []*Milestone{}
137 for _, part := range strings.Split(spec, ",") {
138 part = strings.TrimSpace(part)
139 if part == "" {
140 continue
141 }
142 i := strings.LastIndex(part, ":")
143 if i < 0 {
144 return nil, ErrBadSpec
145 }
146 amount, err := strconv.ParseInt(strings.TrimSpace(part[i+1:]), 10, 64)
147 if err != nil {
148 return nil, ErrBadSpec
149 }
150 out = append(out, NewMilestone(strings.TrimSpace(part[:i]), amount))
151 }
152 if len(out) == 0 {
153 return nil, ErrBadSpec
154 }
155 return out, nil
156}