Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions 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}