api.gno
7.96 Kb · 289 lines
1package bounties
2
3import (
4 "chain"
5 "net/url"
6 "strconv"
7 "strings"
8
9 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/bounty/v0"
10 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/jsonw/v0"
11 "gno.land/p/nt/avl/v0"
12 "gno.land/r/nt/grc20reg/v0"
13)
14
15const (
16 realmPath = "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/bounties"
17 apiMaxLimit = 50
18 apiMaxScan = 2000
19 apiMaxToken = 200
20)
21
22// renderAPI serves JSON for the web app, under Render("api/..."):
23//
24// api/stats counts, escrow totals, council, paused
25// api/bounties?status=&tag=&page=&limit= newest first; status "all" or empty for every bounty
26// api/bounty/<id> a bounty with milestones, disputes and applications
27// api/user/<address>?page=&limit= bounties an address is involved in, with roles
28// api/tokens registered GRC20 tokens usable for funding
29func renderAPI(route string, q url.Values) string {
30 parts := strings.Split(route, "/")
31 switch {
32 case route == "api/stats":
33 return apiStats()
34 case route == "api/bounties":
35 return apiList(q)
36 case len(parts) == 3 && parts[1] == "bounty":
37 id, err := strconv.ParseUint(parts[2], 10, 64)
38 if err != nil {
39 return apiError("invalid id")
40 }
41 bn := getBounty(id)
42 if bn == nil {
43 return apiError("not found")
44 }
45 return bountyJSON(bn, true)
46 case len(parts) == 3 && parts[1] == "user":
47 return apiUser(address(parts[2]), q)
48 case route == "api/tokens":
49 return apiTokens()
50 }
51 return apiError("unknown endpoint")
52}
53
54func apiStats() string {
55 var escrow []string
56 escrowed.Iterate("", "", func(k string, v any) bool {
57 if amt := v.(int64); amt > 0 {
58 escrow = append(escrow, jsonw.Obj(append([]string{"amount", jsonw.Int(amt)}, denomFields(k, grc20reg.Get(k) != nil)...)...))
59 }
60 return false
61 })
62 return jsonw.Obj(
63 "total", jsonw.Int(int64(bounties.Size())),
64 "open", jsonw.Int(int64(statusCount[bounty.StatusOpen])),
65 "active", jsonw.Int(int64(statusCount[bounty.StatusActive])),
66 "completed", jsonw.Int(int64(statusCount[bounty.StatusCompleted])),
67 "cancelled", jsonw.Int(int64(statusCount[bounty.StatusCancelled])),
68 "escrow", jsonw.Arr(escrow),
69 "council", jsonw.String(council.Owner().String()),
70 "paused", jsonw.Bool(paused),
71 "realmAddress", jsonw.String(chain.PackageAddress(realmPath).String()),
72 )
73}
74
75func apiList(q url.Values) string {
76 status := q.Get("status")
77 want, filtered := bounty.ParseStatus(status)
78 if status != "" && status != "all" && !filtered {
79 return apiError("unknown status")
80 }
81 tag := strings.ToLower(q.Get("tag"))
82 limit := limitOf(q)
83 skip := (pageOf(q) - 1) * limit
84
85 var items []string
86 more := false
87 scanned := 0
88 bounties.ReverseIterate("", "", func(_ string, v any) bool {
89 scanned++
90 if scanned > apiMaxScan {
91 more = true
92 return true
93 }
94 bn := v.(*bounty.Bounty)
95 if filtered && bn.Status != want {
96 return false
97 }
98 if tag != "" && !hasTag(bn, tag) {
99 return false
100 }
101 if skip > 0 {
102 skip--
103 return false
104 }
105 if len(items) == limit {
106 more = true
107 return true
108 }
109 items = append(items, bountyJSON(bn, false))
110 return false
111 })
112 return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(more))
113}
114
115func apiUser(addr address, q url.Values) string {
116 if !addr.IsValid() {
117 return apiError("invalid address")
118 }
119 limit := limitOf(q)
120 var items []string
121 more := false
122 if v := byUser.Get(addr.String()); v != nil {
123 ids := v.(*avl.Tree)
124 ids.ReverseIterateByOffset((pageOf(q)-1)*limit, limit, func(k string, _ any) bool {
125 if bv := bounties.Get(k); bv != nil {
126 bn := bv.(*bounty.Bounty)
127 items = append(items, jsonw.Obj("bounty", bountyJSON(bn, false), "roles", jsonw.Strings(roles(bn, addr))))
128 }
129 return false
130 })
131 more = ids.Size() > pageOf(q)*limit
132 }
133 return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(more))
134}
135
136func apiTokens() string {
137 var items []string
138 grc20reg.GetRegistry().Iterate("", "", func(k string, _ any) bool {
139 tok := grc20reg.Get(k)
140 if tok == nil {
141 return false
142 }
143 items = append(items, jsonw.Obj(
144 "key", jsonw.String(k),
145 "name", jsonw.String(tok.GetName()),
146 "symbol", jsonw.String(tok.GetSymbol()),
147 "decimals", jsonw.Int(int64(tok.GetDecimals())),
148 ))
149 return len(items) >= apiMaxToken
150 })
151 return jsonw.Obj("items", jsonw.Arr(items))
152}
153
154func bountyJSON(bn *bounty.Bounty, full bool) string {
155 ms := make([]string, len(bn.Milestones))
156 for i, m := range bn.Milestones {
157 fields := []string{
158 "title", jsonw.String(m.Title),
159 "amount", jsonw.Int(m.Amount),
160 "status", jsonw.String(m.Status.String()),
161 }
162 if full {
163 dispute := jsonw.Null
164 if d := m.Dispute; d != nil {
165 dispute = jsonw.Obj(
166 "openedBy", jsonw.String(d.OpenedBy.String()),
167 "reason", jsonw.String(d.Reason),
168 "openedAt", jsonw.Time(d.OpenedAt),
169 "resolved", jsonw.Bool(d.Resolved),
170 "resolvedAt", jsonw.Time(d.ResolvedAt),
171 "workerBps", jsonw.Int(d.WorkerBps),
172 "ruling", jsonw.String(d.Ruling),
173 )
174 }
175 fields = append(fields,
176 "submission", jsonw.String(m.Submission),
177 "submittedAt", jsonw.Time(m.SubmittedAt),
178 "approvals", jsonw.Addresses(m.Approvals),
179 "feedback", jsonw.String(m.Feedback),
180 "worker", jsonw.String(m.Worker.String()),
181 "paidOut", jsonw.Int(m.PaidOut),
182 "refunded", jsonw.Int(m.Refunded),
183 "settledAt", jsonw.Time(m.SettleAt),
184 "dispute", dispute,
185 )
186 }
187 ms[i] = jsonw.Obj(fields...)
188 }
189
190 fields := []string{
191 "id", jsonw.Uint(bn.ID),
192 "title", jsonw.String(bn.Title),
193 "tags", jsonw.Strings(bn.Tags),
194 "status", jsonw.String(bn.Status.String()),
195 "poster", jsonw.String(bn.Poster.String()),
196 "assignee", jsonw.String(bn.Assignee.String()),
197 "total", jsonw.Int(bn.Total()),
198 "escrowed", jsonw.Int(bn.Escrowed()),
199 "paidOut", jsonw.Int(bn.PaidOut()),
200 "applications", jsonw.Int(int64(len(bn.Applications))),
201 "createdAt", jsonw.Time(bn.CreatedAt),
202 "milestones", jsonw.Arr(ms),
203 }
204 fields = append(fields, denomFields(bn.Denom, bn.IsGRC20)...)
205 if full {
206 var apps []string
207 for _, a := range bn.Applications {
208 apps = append(apps, jsonw.Obj(
209 "applicant", jsonw.String(a.Applicant.String()),
210 "pitch", jsonw.String(a.Pitch),
211 "at", jsonw.Time(a.At),
212 ))
213 }
214 fields = append(fields,
215 "description", jsonw.String(bn.Description),
216 "link", jsonw.String(bn.Link),
217 "reviewers", jsonw.Addresses(bn.Reviewers),
218 "quorum", jsonw.Int(int64(bn.Quorum)),
219 "arbiter", jsonw.String(bn.Arbiter.String()),
220 "councilArbitrated", jsonw.Bool(councilArbitrated.Has(idKey(bn.ID))),
221 "council", jsonw.String(council.Owner().String()),
222 "workWindowDays", jsonw.Int(int64(bn.WorkWindow.Hours()/24)),
223 "assignedAt", jsonw.Time(bn.AssignedAt),
224 "closedAt", jsonw.Time(bn.ClosedAt),
225 "applicationList", jsonw.Arr(apps),
226 )
227 }
228 return jsonw.Obj(fields...)
229}
230
231// denomFields describes how to display amounts of denom.
232func denomFields(denom string, isGRC20 bool) []string {
233 symbol, decimals := denom, 0
234 switch {
235 case isGRC20:
236 if tok := grc20reg.Get(denom); tok != nil {
237 symbol, decimals = tok.GetSymbol(), tok.GetDecimals()
238 }
239 case denom == "ugnot":
240 symbol, decimals = "GNOT", 6
241 }
242 return []string{
243 "denom", jsonw.String(denom),
244 "isGRC20", jsonw.Bool(isGRC20),
245 "symbol", jsonw.String(symbol),
246 "decimals", jsonw.Int(int64(decimals)),
247 }
248}
249
250func roles(bn *bounty.Bounty, addr address) []string {
251 var out []string
252 if bn.Poster == addr {
253 out = append(out, "poster")
254 }
255 if bn.Assignee == addr {
256 out = append(out, "assignee")
257 }
258 if bn.IsReviewer(addr) {
259 out = append(out, "reviewer")
260 }
261 if bn.Arbiter == addr {
262 out = append(out, "arbiter")
263 }
264 if bn.HasApplied(addr) && bn.Assignee != addr {
265 out = append(out, "applicant")
266 }
267 for _, m := range bn.Milestones {
268 if m.Worker == addr && bn.Assignee != addr {
269 out = append(out, "contributor")
270 break
271 }
272 }
273 return out
274}
275
276func limitOf(q url.Values) int {
277 n, err := strconv.Atoi(q.Get("limit"))
278 if err != nil || n < 1 {
279 return 20
280 }
281 if n > apiMaxLimit {
282 return apiMaxLimit
283 }
284 return n
285}
286
287func apiError(msg string) string {
288 return jsonw.Obj("error", jsonw.String(msg))
289}