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

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}