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

6.45 Kb · 217 lines
  1package campaigns
  2
  3import (
  4	"chain"
  5	"net/url"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/guildhall/campaign/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	apiMaxLimit = 50
 17	apiMaxScan  = 2000
 18)
 19
 20// renderAPI serves JSON for the web app, under Render("api/..."):
 21//
 22//	api/stats                                       counts, escrow totals, council, paused
 23//	api/campaigns?status=&tag=&page=&limit=         newest first; status "all" or empty for every campaign
 24//	api/campaign/<id>                               one campaign
 25//	api/campaign/<id>/claims?status=&page=&limit=   its claims, newest first
 26//	api/campaign/<id>/claimant/<address>            {"claim": an address's latest claim, or null}
 27//	api/user/<address>?page=&limit=                 campaigns an address is involved in, with roles and their claim
 28func renderAPI(route string, q url.Values) string {
 29	parts := strings.Split(route, "/")
 30	switch {
 31	case route == "api/stats":
 32		return apiStats()
 33	case route == "api/campaigns":
 34		status := q.Get("status")
 35		if _, ok := campaign.ParseStatus(status); status != "" && status != "all" && !ok {
 36			return apiError("unknown status")
 37		}
 38		limit := limitOf(q)
 39		list, more := scan(status, strings.ToLower(q.Get("tag")), (pageOf(q)-1)*limit, limit)
 40		items := make([]string, len(list))
 41		for i, c := range list {
 42			items[i] = campaignJSON(c, false)
 43		}
 44		return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(more))
 45	case len(parts) >= 3 && len(parts) <= 5 && parts[1] == "campaign":
 46		id, err := strconv.ParseUint(parts[2], 10, 64)
 47		if err != nil {
 48			return apiError("invalid id")
 49		}
 50		c := getCampaign(id)
 51		if c == nil {
 52			return apiError("not found")
 53		}
 54		if len(parts) == 3 {
 55			return campaignJSON(c, true)
 56		}
 57		if len(parts) == 4 && parts[3] == "claims" {
 58			return apiClaims(c, q)
 59		}
 60		if len(parts) == 5 && parts[3] == "claimant" {
 61			addr := address(parts[4])
 62			if !addr.IsValid() {
 63				return apiError("invalid address")
 64			}
 65			mine := jsonw.Null
 66			if cl := c.ClaimOf(addr); cl != nil {
 67				mine = claimJSON(cl)
 68			}
 69			return jsonw.Obj("claim", mine)
 70		}
 71		return apiError("unknown endpoint")
 72	case len(parts) == 3 && parts[1] == "user":
 73		return apiUser(address(parts[2]), q)
 74	}
 75	return apiError("unknown endpoint")
 76}
 77
 78func apiStats() string {
 79	var escrow []string
 80	escrowed.Iterate("", "", func(k string, v any) bool {
 81		if amt := v.(int64); amt > 0 {
 82			escrow = append(escrow, jsonw.Obj(append([]string{"amount", jsonw.Int(amt)}, denomFields(k, grc20reg.Get(k) != nil)...)...))
 83		}
 84		return false
 85	})
 86	return jsonw.Obj(
 87		"total", jsonw.Int(int64(campaigns.Size())),
 88		"open", jsonw.Int(int64(statusCount[campaign.StatusOpen])),
 89		"closed", jsonw.Int(int64(statusCount[campaign.StatusClosed])),
 90		"escrow", jsonw.Arr(escrow),
 91		"council", jsonw.String(council.Owner().String()),
 92		"paused", jsonw.Bool(paused),
 93		"realmAddress", jsonw.String(chain.PackageAddress(realmPath).String()),
 94		"claimShareBps", jsonw.Int(ClaimShareBps),
 95		"maxSlots", jsonw.Int(campaign.MaxSlots),
 96	)
 97}
 98
 99func apiClaims(c *campaign.Campaign, q url.Values) string {
100	var st *campaign.ClaimStatus
101	if s := q.Get("status"); s != "" && s != "all" {
102		parsed, ok := campaign.ParseClaimStatus(s)
103		if !ok {
104			return apiError("unknown status")
105		}
106		st = &parsed
107	}
108	limit := limitOf(q)
109	list, more := c.Claims(st, (pageOf(q)-1)*limit, limit)
110	items := make([]string, len(list))
111	for i, cl := range list {
112		items[i] = claimJSON(cl)
113	}
114	return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(more))
115}
116
117func apiUser(addr address, q url.Values) string {
118	if !addr.IsValid() {
119		return apiError("invalid address")
120	}
121	limit := limitOf(q)
122	var items []string
123	more := false
124	if v := byUser.Get(addr.String()); v != nil {
125		ids := v.(*avl.Tree)
126		ids.ReverseIterateByOffset((pageOf(q)-1)*limit, limit, func(k string, _ any) bool {
127			if cv := campaigns.Get(k); cv != nil {
128				c := cv.(*campaign.Campaign)
129				mine := jsonw.Null
130				if cl := c.ClaimOf(addr); cl != nil {
131					mine = claimJSON(cl)
132				}
133				items = append(items, jsonw.Obj("campaign", campaignJSON(c, false), "roles", jsonw.Strings(roles(c, addr)), "claim", mine))
134			}
135			return false
136		})
137		more = ids.Size() > pageOf(q)*limit
138	}
139	return jsonw.Obj("items", jsonw.Arr(items), "more", jsonw.Bool(more))
140}
141
142func campaignJSON(c *campaign.Campaign, full bool) string {
143	fields := []string{
144		"id", jsonw.Uint(c.ID),
145		"title", jsonw.String(c.Title),
146		"tags", jsonw.Strings(c.Tags),
147		"status", jsonw.String(c.Status.String()),
148		"poster", jsonw.String(c.Poster.String()),
149		"reward", jsonw.Int(c.Reward),
150		"slots", jsonw.Int(int64(c.Slots)),
151		"paid", jsonw.Int(int64(c.Paid)),
152		"pending", jsonw.Int(int64(c.Pending)),
153		"rejected", jsonw.Int(int64(c.Rejected)),
154		"refunded", jsonw.Int(int64(c.Refunded)),
155		"freeSlots", jsonw.Int(int64(c.FreeSlots())),
156		"claims", jsonw.Int(int64(c.ClaimCount())),
157		"total", jsonw.Int(c.Total()),
158		"escrowed", jsonw.Int(c.Escrowed()),
159		"paidOut", jsonw.Int(c.PaidOut()),
160		"deadline", jsonw.Time(c.Deadline),
161		"createdAt", jsonw.Time(c.CreatedAt),
162	}
163	fields = append(fields, denomFields(c.Denom, c.IsGRC20)...)
164	if full {
165		fields = append(fields,
166			"description", jsonw.String(c.Description),
167			"link", jsonw.String(c.Link),
168			"reviewers", jsonw.Addresses(c.Reviewers),
169			"closedAt", jsonw.Time(c.ClosedAt),
170			"claimShareBps", jsonw.Int(ClaimShareBps),
171		)
172	}
173	return jsonw.Obj(fields...)
174}
175
176func claimJSON(cl *campaign.Claim) string {
177	reviewer := ""
178	if cl.ReviewedBy != "" {
179		reviewer = cl.ReviewedBy.String()
180	}
181	return jsonw.Obj(
182		"number", jsonw.Int(int64(cl.Number)),
183		"claimant", jsonw.String(cl.Claimant.String()),
184		"proof", jsonw.String(cl.Proof),
185		"status", jsonw.String(cl.Status.String()),
186		"at", jsonw.Time(cl.At),
187		"reviewedBy", jsonw.String(reviewer),
188		"reviewedAt", jsonw.Time(cl.ReviewedAt),
189		"reason", jsonw.String(cl.Reason),
190	)
191}
192
193// denomFields describes how to display amounts of denom.
194func denomFields(denom string, isGRC20 bool) []string {
195	symbol, decimals := symbolOf(denom, isGRC20)
196	return []string{
197		"denom", jsonw.String(denom),
198		"isGRC20", jsonw.Bool(isGRC20),
199		"symbol", jsonw.String(symbol),
200		"decimals", jsonw.Int(int64(decimals)),
201	}
202}
203
204func limitOf(q url.Values) int {
205	n, err := strconv.Atoi(q.Get("limit"))
206	if err != nil || n < 1 {
207		return 20
208	}
209	if n > apiMaxLimit {
210		return apiMaxLimit
211	}
212	return n
213}
214
215func apiError(msg string) string {
216	return jsonw.Obj("error", jsonw.String(msg))
217}