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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

grant.gno

9.71 Kb · 245 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/nym-moultest123/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/nym-moultest123/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/nym-moultest123/grant/v0"
 60	// Link is Path as a gnoweb route.
 61	Link = "/r/nym-moultest123/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	if !cur.IsCurrent() {
111		panic("grant: spoofed realm")
112	}
113	// OriginSend is what the SIGNER attached, not what reached this realm. A
114	// realm the user called keeps those coins and could call Fund any number of
115	// times, booking each as a donation, so only a user call may read it.
116	prev := cur.Previous()
117	if !prev.IsUserCall() {
118		panic("grant: Fund must be called directly by an account, with -send")
119	}
120	from := prev.Address()
121	amount := unsafe.OriginSend().AmountOf(Denom)
122	if err := program.Fund(from, amount, runtime.ChainHeight()); err != nil {
123		panic(err)
124	}
125	chain.Emit("Fund", "from", from.String(), "amount", strconv.FormatInt(amount, 10))
126}
127
128// Apply files a grant request for the caller. milestones reads
129// "title:amount,title:amount", amounts in ugnot, paid in the order given.
130// Returns the request id.
131func Apply(cur realm, title, body, milestones string) int {
132	if !cur.IsCurrent() {
133		panic("grant: spoofed realm")
134	}
135	applicant := cur.Previous().Address()
136	return file(applicant, applicant, "", title, body, milestones)
137}
138
139// ApplyFor files a grant request whose tranches pay beneficiary rather than
140// the caller, with a reason for the detour that goes on the request page.
141//
142// This is the call that makes the board usable at all for the case it exists
143// to serve. An account with nothing in it cannot pay the gas to ask for its
144// first coins, so without someone else filing on its behalf the people who
145// most need a small grant are exactly the people who cannot ask for one.
146// Either address may then submit the proofs, and neither may vote.
147func ApplyFor(cur realm, beneficiary, reason, title, body, milestones string) int {
148	if !cur.IsCurrent() {
149		panic("grant: spoofed realm")
150	}
151	applicant := cur.Previous().Address()
152	return file(applicant, address(beneficiary), reason, title, body, milestones)
153}
154
155func file(applicant, beneficiary address, reason, title, body, milestones string) int {
156	r, err := program.ApplyFor(applicant, beneficiary, reason, title, body, milestones,
157		runtime.ChainHeight())
158	if err != nil {
159		panic(err)
160	}
161	chain.Emit("Apply", "id", strconv.Itoa(r.ID), "applicant", applicant.String(),
162		"beneficiary", r.Payee().String(), "total", strconv.FormatInt(r.Total(), 10))
163	return r.ID
164}
165
166// ProposeMember asks the board to add or remove a member. Members only.
167func ProposeMember(cur realm, addr string, add bool, body string) int {
168	if !cur.IsCurrent() {
169		panic("grant: spoofed realm")
170	}
171	proposer := cur.Previous().Address()
172	r, err := program.Board.SubmitMemberChange(proposer, address(addr), add, body, runtime.ChainHeight())
173	if err != nil {
174		panic(err)
175	}
176	chain.Emit("ProposeMember", "id", strconv.Itoa(r.ID), "subject", addr,
177		"add", strconv.FormatBool(add))
178	return r.ID
179}
180
181// Retract pulls the caller's own request before it is decided.
182func Retract(cur realm, id int) {
183	if !cur.IsCurrent() {
184		panic("grant: spoofed realm")
185	}
186	caller := cur.Previous().Address()
187	if err := program.Board.Withdraw(caller, id, runtime.ChainHeight()); err != nil {
188		panic(err)
189	}
190	chain.Emit("Retract", "id", strconv.Itoa(id))
191}
192
193// Vote records a member's ballot. The reason is stored and rendered next to
194// the vote: it is the only part of a decision that tells an applicant what to
195// do about it.
196func Vote(cur realm, id int, approve bool, reason string) {
197	if !cur.IsCurrent() {
198		panic("grant: spoofed realm")
199	}
200	voter := cur.Previous().Address()
201	st, err := program.Board.Vote(voter, id, approve, reason, runtime.ChainHeight())
202	if err != nil {
203		panic(err)
204	}
205	chain.Emit("Vote", "id", strconv.Itoa(id), "voter", voter.String(),
206		"approve", strconv.FormatBool(approve), "status", st.String())
207}
208
209// SubmitProof offers evidence for the next milestone of an approved grant.
210// kind is "url", "hash" or "text"; ref is the link, digest or statement.
211func SubmitProof(cur realm, id, milestone int, kind, ref, note string) {
212	if !cur.IsCurrent() {
213		panic("grant: spoofed realm")
214	}
215	caller := cur.Previous().Address()
216	p := grants.Proof{Kind: kind, Ref: ref, Note: note, Height: runtime.ChainHeight()}
217	if err := program.Board.SubmitProof(caller, id, milestone, p); err != nil {
218		panic(err)
219	}
220	chain.Emit("SubmitProof", "id", strconv.Itoa(id),
221		"milestone", strconv.Itoa(milestone), "kind", kind)
222}
223
224// Review records a member's verdict on the proof under review. The verdict
225// that carries also pays the tranche, in this same transaction. A verdict the
226// treasury cannot cover is refused outright and changes nothing.
227func Review(cur realm, id, milestone int, accept bool, reason string) {
228	if !cur.IsCurrent() {
229		panic("grant: spoofed realm")
230	}
231	voter := cur.Previous().Address()
232	out, pay, err := program.Review(voter, id, milestone, accept, reason, runtime.ChainHeight(), Balance())
233	if err != nil {
234		panic(err)
235	}
236	chain.Emit("Review", "id", strconv.Itoa(id), "milestone", strconv.Itoa(milestone),
237		"voter", voter.String(), "accept", strconv.FormatBool(accept), "outcome", out.String())
238	if pay == nil {
239		return
240	}
241	banker.NewBanker(banker.BankerTypeRealmSend, cur).
242		SendCoins(Address(), pay.To, chain.NewCoins(chain.NewCoin(Denom, pay.Amount)))
243	chain.Emit("Release", "id", strconv.Itoa(id), "milestone", strconv.Itoa(milestone),
244		"to", pay.To.String(), "amount", strconv.FormatInt(pay.Amount, 10))
245}