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}