lists.gno
7.13 Kb · 212 lines
1package bolo
2
3import "gno.land/p/nt/avl/v0"
4
5// Lists (API §4). A size is clamped to 1–50 (INV-9). The campaign lists page by
6// cursor: each reaches its place in its index by key, in O(log n), never by counting
7// entries. The per-campaign lists page by number, by offset in the campaign's own tree.
8
9const maxPageSize = 50
10
11// idKeyLen is the length of a campaign's key, seqid.ID.Binary().
12const idKeyLen = 8
13
14// pageSize clamps a requested size to 1–50.
15func pageSize(size int) int {
16 if size < 1 {
17 return 1
18 }
19 if size > maxPageSize {
20 return maxPageSize
21 }
22 return size
23}
24
25// ListCampaigns returns the open, listed campaigns, newest first, from below after (""
26// for the newest), and the cursor to pass next, "" at the end; category may be "".
27// A campaign past a deadline nobody recorded is left out (Q6). One call reads at most
28// four times size entries, so a page can be short while the cursor isn't "".
29func ListCampaigns(after string, size int, category string) ([]CampaignView, string) {
30 size = pageSize(size)
31 index, prefix := &active, ""
32 if category != "" {
33 index, prefix = &activeByCategory, category+"/"
34 }
35 return campaignPage(index, prefix, after, size, 4*size, func(c *campaign) bool {
36 status, _, _ := effective(c)
37 return status == statusOpen
38 })
39}
40
41// ListByOrganizer returns the campaigns addr organizes, delisted included, newest
42// first, from below after, and the cursor to pass next. An address is compared as
43// given (C12).
44func ListByOrganizer(addr address, after string, size int) ([]CampaignView, string) {
45 size = pageSize(size)
46 return campaignPage(&byOrganizer, addr.String()+"/", after, size, size, everyCampaign)
47}
48
49// ListByBeneficiary returns the campaigns naming addr as beneficiary, delisted included
50// — the beneficiary must keep seeing them (INV-4) — newest first, from below after,
51// and the cursor to pass next.
52func ListByBeneficiary(addr address, after string, size int) ([]CampaignView, string) {
53 size = pageSize(size)
54 return campaignPage(&byBeneficiary, addr.String()+"/", after, size, size, everyCampaign)
55}
56
57// ListByDonor returns the campaigns addr gave to at least once without anonymity,
58// newest first, from below after, and the cursor to pass next: a campaign where all of
59// addr's gifts were anonymous is in no index under addr.
60func ListByDonor(addr address, after string, size int) ([]CampaignView, string) {
61 size = pageSize(size)
62 return campaignPage(&byDonor, addr.String()+"/", after, size, size, everyCampaign)
63}
64
65func everyCampaign(*campaign) bool { return true }
66
67// campaignPage reads index newest first: the keys under prefix — a category's or an
68// address's range, which ends at the "/" after it, or the whole index for "" — below
69// after's key. It reads at most budget entries, keeps size views of those keep
70// accepts, and returns them with the ID of the last entry read when more remain, ""
71// when the range is done. A non-canonical after is ErrNotFound (C9).
72func campaignPage(index *avl.Tree, prefix, after string, size, budget int, keep func(*campaign) bool) ([]CampaignView, string) {
73 start, end := prefix, ""
74 if prefix != "" {
75 end = prefix[:len(prefix)-1] + "0" // "0" sorts right after "/"
76 }
77 cursor := ""
78 if after != "" {
79 id, ok := parseID(after)
80 if !ok {
81 panic(ErrNotFound)
82 }
83 cursor = prefix + id.Binary()
84 end = cursor
85 }
86 views := []CampaignView{}
87 last, read, more := "", 0, false
88 // A reverse traversal includes its end: the cursor's own entry, read last time, is
89 // skipped. A key whose ID part isn't a campaign key's eight bytes stops the reading:
90 // only a prefix holding "/" — an address or a category crafted to end in a key's
91 // first bytes — meets one, and every key in its range is such a key, so walking on
92 // would read them all, past the budget, to find nothing.
93 index.ReverseIterate(start, end, func(key string, _ any) bool {
94 if key == cursor {
95 return false
96 }
97 if len(key) != len(prefix)+idKeyLen {
98 return true
99 }
100 if len(views) == size || read == budget {
101 more = true
102 return true
103 }
104 read++
105 c := campaigns.Get(key[len(prefix):]).(*campaign)
106 last = idString(c.id)
107 if keep(c) {
108 views = append(views, campaignView(c))
109 }
110 return false
111 })
112 if !more {
113 return views, ""
114 }
115 return views, last
116}
117
118// pageOffset returns where page starts, newest first, in a tree of n entries paged by
119// size, and false past the end. A page counts from 1; below 1 reads as 1.
120func pageOffset(page, size, n int) (int, bool) {
121 if page < 1 {
122 page = 1
123 }
124 if page-1 > n/size {
125 return 0, false
126 }
127 return (page - 1) * size, true
128}
129
130// ListReceipts returns a page of the campaign's gifts, newest first.
131func ListReceipts(id string, page, size int) []ReceiptView {
132 c := campaignOf(id)
133 size = pageSize(size)
134 views := []ReceiptView{}
135 if offset, ok := pageOffset(page, size, c.receipts.Size()); ok {
136 c.receipts.ReverseIterateByOffset(offset, size, func(_ string, v any) bool {
137 views = append(views, receiptView(c, v.(*receipt)))
138 return false
139 })
140 }
141 return views
142}
143
144// WithdrawalView is one payout to the beneficiary (API §2.4): its tracked amount and
145// the coins sent outside Bolo it carried, apart.
146type WithdrawalView struct {
147 Seq, Amount, Untracked, At int64
148 To address
149}
150
151// ListWithdrawals returns a page of the campaign's withdrawals, newest first.
152func ListWithdrawals(id string, page, size int) []WithdrawalView {
153 c := campaignOf(id)
154 size = pageSize(size)
155 views := []WithdrawalView{}
156 if offset, ok := pageOffset(page, size, c.withdrawals.Size()); ok {
157 c.withdrawals.ReverseIterateByOffset(offset, size, func(_ string, v any) bool {
158 w := v.(*withdrawal)
159 views = append(views, WithdrawalView{Seq: int64(w.seq), Amount: w.amount, Untracked: w.untracked, At: w.at, To: w.to})
160 return false
161 })
162 }
163 return views
164}
165
166// RefundView is one payout to a donor (API §2.4). Donor is empty when all of that
167// donor's gifts to the campaign were anonymous; Kind is "open" or "exit".
168type RefundView struct {
169 Seq, Amount, At int64
170 Donor address
171 Kind string
172}
173
174// ListRefunds returns a page of the campaign's refunds, newest first.
175func ListRefunds(id string, page, size int) []RefundView {
176 c := campaignOf(id)
177 size = pageSize(size)
178 views := []RefundView{}
179 if offset, ok := pageOffset(page, size, c.refunds.Size()); ok {
180 c.refunds.ReverseIterateByOffset(offset, size, func(_ string, v any) bool {
181 r := v.(*refund)
182 view := RefundView{Seq: int64(r.seq), Amount: r.amount, At: r.at, Kind: r.kind}
183 if k := contributionOf(c, r.donor); k != nil && k.public {
184 view.Donor = r.donor
185 }
186 views = append(views, view)
187 return false
188 })
189 }
190 return views
191}
192
193// UpdateView is one organizer update (API §2.4).
194type UpdateView struct {
195 Seq, At int64
196 Body string
197}
198
199// ListUpdates returns a page of the campaign's updates, newest first.
200func ListUpdates(id string, page, size int) []UpdateView {
201 c := campaignOf(id)
202 size = pageSize(size)
203 views := []UpdateView{}
204 if offset, ok := pageOffset(page, size, c.updates.Size()); ok {
205 c.updates.ReverseIterateByOffset(offset, size, func(_ string, v any) bool {
206 u := v.(*update)
207 views = append(views, UpdateView{Seq: int64(u.seq), At: u.at, Body: u.body})
208 return false
209 })
210 }
211 return views
212}