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.

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}