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

grant.gno

8.95 Kb · 214 lines
  1// Package grant is moul's personal grant board.
  2//
  3// It is small on purpose. moul funds it out of his own pocket, moul is the
  4// only member, and it exists mainly so the coding agents working on his repos
  5// and the people he already works with have a way to ask for money against
  6// work they can prove they did: a request, a milestone, a link to the merged
  7// PR, a tranche. It is open to anyone, not only to agents, and nothing here is
  8// privileged to a bot.
  9//
 10// # What this is NOT
 11//
 12// It is not a faucet, and it is not an official gno.land grant program. There
 13// is no entitlement, no queue and no service level: a request can sit here
 14// unanswered, or be refused with one line of reasoning and nothing further.
 15// The official thing, where anyone asks for tokens and a DAO decides, is a
 16// separate program that does not exist yet, at a path this realm deliberately
 17// does not name: naming one would read as a commitment nobody has made. This
 18// realm is moul's own money and a proof of concept for the library under it.
 19//
 20// A bigger, multi-member, better funded program is a separate thing and will
 21// live at its own path. This one stays personal, and its limits are on the
 22// page rather than in a comment: one member means "a majority of the board" is
 23// one signature, and the board page says so.
 24//
 25// # What is here versus in the library
 26//
 27// Almost nothing. Every rule, every tally, every markdown page comes from
 28// gno.land/p/moul/grants/v0, which is pure and has no idea coins exist. This
 29// file is the chain-facing half: it turns the caller into an address and the
 30// block into a height, holds the one Program, moves ugnot through the banker,
 31// and emits events. That is deliberate, so the next board (multi-member,
 32// differently funded, maybe a different denom) is another file this short and
 33// not a fork of this one.
 34//
 35// # Who the caller is
 36//
 37// Every actor is PreviousRealm().Address(), the immediate caller. Called
 38// through another realm that is the calling REALM, not the user behind it.
 39// An agent holding a gno.land account session scoped to this path applies and
 40// submits proofs as its own address, which is exactly what makes an agent's
 41// track record on this board its own and not moul's.
 42package grant
 43
 44import (
 45	"strconv"
 46
 47	"chain"
 48	"chain/banker"
 49	"chain/runtime"
 50	"chain/runtime/unsafe"
 51
 52	"gno.land/p/moul/grants/v0"
 53)
 54
 55const (
 56	// Denom is the only coin this board holds or pays.
 57	Denom = "ugnot"
 58	// Path is this realm's package path; its treasury is derived from it.
 59	Path = "gno.land/r/moul/grant/v0"
 60	// Link is Path as a gnoweb route.
 61	Link = "/r/moul/grant/v0"
 62)
 63
 64// Owner is the founding and, for now, only member.
 65//
 66// Hardcoded rather than taken from the deployer: inside a plain
 67// `func Test(t *testing.T)` the gno test runner reports OriginCaller() as the
 68// EMPTY address, so a board seeded from it is empty in every test and
 69// something else on chain. That divergence is exactly where an authorization
 70// bug hides, so the address is written down.
 71const Owner = address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")
 72
 73var program *grants.Program
 74
 75func init() { reset() }
 76
 77// reset installs a fresh, empty board. Also called by the tests: realm globals
 78// persist for a whole test binary and examples run after every Test, so a
 79// pinned Render has to start from a known state.
 80func reset() { program = grants.NewProgram(Denom, Owner) }
 81
 82// Address is the treasury: this realm's own package address. Fund it by
 83// sending ugnot to it, or by calling Fund with `-send`.
 84func Address() address { return chain.PackageAddress(Path) }
 85
 86// Balance is what the treasury actually holds.
 87func Balance() int64 {
 88	return banker.NewReadonlyBanker().GetCoins(Address()).AmountOf(Denom)
 89}
 90
 91// Committed is what approved grants can still claim, Available is the balance
 92// minus that, and it can go negative. See the library for why.
 93func Committed() int64 { return program.Committed() }
 94func Available() int64 { return program.Available(Balance()) }
 95
 96// Raised is everything donated through Fund, Disbursed everything paid out.
 97func Raised() int64    { return program.Raised() }
 98func Disbursed() int64 { return program.Disbursed() }
 99
100// Members lists the board.
101func Members() []address { return program.Board.Members() }
102
103// Requests is how many requests have ever been filed.
104func Requests() int { return program.Board.Size() }
105
106// Fund credits the ugnot sent with the call to the treasury and puts the donor
107// on the record. Coins sent to the realm address directly still land in the
108// treasury; they just do not get a name next to them.
109func Fund(cur realm) {
110	from := unsafe.PreviousRealm().Address()
111	amount := unsafe.OriginSend().AmountOf(Denom)
112	if err := program.Fund(from, amount, runtime.ChainHeight()); err != nil {
113		panic(err)
114	}
115	chain.Emit("Fund", "from", from.String(), "amount", strconv.FormatInt(amount, 10))
116}
117
118// Apply files a grant request for the caller. milestones reads
119// "title:amount,title:amount", amounts in ugnot, paid in the order given.
120// Returns the request id.
121func Apply(cur realm, title, body, milestones string) int {
122	applicant := unsafe.PreviousRealm().Address()
123	return file(applicant, applicant, "", title, body, milestones)
124}
125
126// ApplyFor files a grant request whose tranches pay beneficiary rather than
127// the caller, with a reason for the detour that goes on the request page.
128//
129// This is the call that makes the board usable at all for the case it exists
130// to serve. An account with nothing in it cannot pay the gas to ask for its
131// first coins, so without someone else filing on its behalf the people who
132// most need a small grant are exactly the people who cannot ask for one.
133// Either address may then submit the proofs, and neither may vote.
134func ApplyFor(cur realm, beneficiary, reason, title, body, milestones string) int {
135	applicant := unsafe.PreviousRealm().Address()
136	return file(applicant, address(beneficiary), reason, title, body, milestones)
137}
138
139func file(applicant, beneficiary address, reason, title, body, milestones string) int {
140	r, err := program.ApplyFor(applicant, beneficiary, reason, title, body, milestones,
141		runtime.ChainHeight())
142	if err != nil {
143		panic(err)
144	}
145	chain.Emit("Apply", "id", strconv.Itoa(r.ID), "applicant", applicant.String(),
146		"beneficiary", r.Payee().String(), "total", strconv.FormatInt(r.Total(), 10))
147	return r.ID
148}
149
150// ProposeMember asks the board to add or remove a member. Members only.
151func ProposeMember(cur realm, addr string, add bool, body string) int {
152	proposer := unsafe.PreviousRealm().Address()
153	r, err := program.Board.SubmitMemberChange(proposer, address(addr), add, body, runtime.ChainHeight())
154	if err != nil {
155		panic(err)
156	}
157	chain.Emit("ProposeMember", "id", strconv.Itoa(r.ID), "subject", addr,
158		"add", strconv.FormatBool(add))
159	return r.ID
160}
161
162// Retract pulls the caller's own request before it is decided.
163func Retract(cur realm, id int) {
164	caller := unsafe.PreviousRealm().Address()
165	if err := program.Board.Withdraw(caller, id, runtime.ChainHeight()); err != nil {
166		panic(err)
167	}
168	chain.Emit("Retract", "id", strconv.Itoa(id))
169}
170
171// Vote records a member's ballot. The reason is stored and rendered next to
172// the vote: it is the only part of a decision that tells an applicant what to
173// do about it.
174func Vote(cur realm, id int, approve bool, reason string) {
175	voter := unsafe.PreviousRealm().Address()
176	st, err := program.Board.Vote(voter, id, approve, reason, runtime.ChainHeight())
177	if err != nil {
178		panic(err)
179	}
180	chain.Emit("Vote", "id", strconv.Itoa(id), "voter", voter.String(),
181		"approve", strconv.FormatBool(approve), "status", st.String())
182}
183
184// SubmitProof offers evidence for the next milestone of an approved grant.
185// kind is "url", "hash" or "text"; ref is the link, digest or statement.
186func SubmitProof(cur realm, id, milestone int, kind, ref, note string) {
187	caller := unsafe.PreviousRealm().Address()
188	p := grants.Proof{Kind: kind, Ref: ref, Note: note, Height: runtime.ChainHeight()}
189	if err := program.Board.SubmitProof(caller, id, milestone, p); err != nil {
190		panic(err)
191	}
192	chain.Emit("SubmitProof", "id", strconv.Itoa(id),
193		"milestone", strconv.Itoa(milestone), "kind", kind)
194}
195
196// Review records a member's verdict on the proof under review. The verdict
197// that carries also pays the tranche, in this same transaction. A verdict the
198// treasury cannot cover is refused outright and changes nothing.
199func Review(cur realm, id, milestone int, accept bool, reason string) {
200	voter := unsafe.PreviousRealm().Address()
201	out, pay, err := program.Review(voter, id, milestone, accept, reason, runtime.ChainHeight(), Balance())
202	if err != nil {
203		panic(err)
204	}
205	chain.Emit("Review", "id", strconv.Itoa(id), "milestone", strconv.Itoa(milestone),
206		"voter", voter.String(), "accept", strconv.FormatBool(accept), "outcome", out.String())
207	if pay == nil {
208		return
209	}
210	banker.NewBanker(banker.BankerTypeRealmSend, cur).
211		SendCoins(Address(), pay.To, chain.NewCoins(chain.NewCoin(Denom, pay.Amount)))
212	chain.Emit("Release", "id", strconv.Itoa(id), "milestone", strconv.Itoa(milestone),
213		"to", pay.To.String(), "amount", strconv.FormatInt(pay.Amount, 10))
214}