Kind Realm (renderable)
Name v0
Namespace moul / grant
Exported functions 17
Module gno.land/r/moul/grant/v0
gno 0.9 ⧉
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 Address () string
Apply (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, title string, body string, milestones string) int
ApplyFor (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, beneficiary string, reason string, title string, body string, milestones string) int
Available () int64
Balance () int64
Committed () int64
Disbursed () int64
Fund (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
Members () [].uverse.address
ProposeMember (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, addr string, add bool, body string) int
Raised () int64
Render (path string) string
Requests () int
Retract (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Review (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, milestone int, accept bool, reason string)
SubmitProof (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, milestone int, kind string, ref string, note string)
Vote (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, approve bool, reason string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
// 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.
42 package grant
43
44 import (
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
55 const (
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.
71 const Owner = address( "g1manfred47kzduec920z88wfr64ylksmdcedlf5" )
72
73 var program *grants.Program
74
75 func 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.
80 func 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`.
84 func Address() address { return chain.PackageAddress(Path) }
85
86 // Balance is what the treasury actually holds.
87 func 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.
93 func Committed() int64 { return program.Committed() }
94 func Available() int64 { return program.Available(Balance()) }
95
96 // Raised is everything donated through Fund, Disbursed everything paid out.
97 func Raised() int64 { return program.Raised() }
98 func Disbursed() int64 { return program.Disbursed() }
99
100 // Members lists the board.
101 func Members() []address { return program.Board.Members() }
102
103 // Requests is how many requests have ever been filed.
104 func 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.
109 func 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.
121 func 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.
134 func 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
139 func 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.
151 func 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.
163 func 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.
174 func 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.
186 func 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.
199 func 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 }
215
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.