claim.gno
10.28 Kb · 376 lines
1package kourt
2import (
3 "chain"
4 "strconv"
5 "strings"
6 checkpoint "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/checkpoint/v0"
7 twap "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/twap/v0"
8 bptree "gno.land/p/nt/bptree/v0"
9)
10const (
11 sideYES = 0
12 sideNO = 1
13)
14const twapBuckets = 168
15const maxClaimBodyLen = 2000
16func runeLen(s string) int {
17 n := 0
18 for range s {
19 n++
20 }
21 return n
22}
23type claimState struct {
24 id uint64
25 title string
26 body string
27 media []mediaItem
28 author address
29 deposit int64
30 fee int64
31 seeded bool
32 noAccuracy bool
33 openedAt int64
34 openedAtTime int64
35 answeredAtTime int64
36 verdictAtTime int64
37 escrowUntilAt int64
38 closed bool
39 board *bptree.BPTree
40 boardTop *bptree.BPTree
41 boardKids *bptree.BPTree
42 boardWrote *bptree.BPTree
43 boardScore *bptree.BPTree
44 boardVoted *bptree.BPTree
45 boardNextID uint64
46 inStrip bool
47 inPending bool
48 yesStake, noStake int64
49 stakers *bptree.BPTree
50 yesConvHi, yesConvLo uint64
51 noConvHi, noConvLo uint64
52 yesRawHi, yesRawLo uint64
53 noRawHi, noRawLo uint64
54 yesLastAcc, noLastAcc int64
55 yesLastRawAt, noLastRawAt int64
56 oi twap.Ring
57 yes twap.Ring
58 yesH, noH checkpoint.Series
59 yesD, noD checkpoint.Series
60 histTrimmedTo uint32
61 frozenAt int64
62 rateAccAtFreeze int64
63 xBarFrozen int64
64 yesStakeAtFreeze, noStakeAtFreeze int64
65 answer int8
66 answerer address
67 answerBond int64
68 answerBond0 int64
69 answerHeight int64
70 verdictAt int64
71 route string
72 tierRef int64
73 disputeOpen bool
74 disputeOpenedTime int64
75 round int64
76 failedRounds int64
77 decidedRounds int64
78 disputer address
79 disputeBond int64
80 overturnBy address
81 overturnBond int64
82 proposalID int64
83 provisional int8
84 escrowUntil int64
85 provClose bool
86 credEligible bool
87 voted *bptree.BPTree
88 rewardsOpened bool
89 drawWinners int64
90 drawAuthor int64
91 drawAnswerer int64
92 winPoolConvCC int64
93 openBlocks int64
94 carrotTotal int64
95 carrotPool int64
96 carrotChoice string
97 carrotDenom int64
98 decidedPID int64
99 spamW int64
100 spamTotalW int64
101 spamClosed bool
102 authorPaid bool
103 answererPaid bool
104}
105func OpenClaim(cur realm, courtSlug, title string) uint64 {
106 if !cur.IsCurrent() {
107 panic(errStaleRealm)
108 }
109 c := mustCourt(courtSlug)
110 if courtIsPurged(c) {
111 panic("kourtv2: this court is purged; no new claims may be opened")
112 }
113 return openClaim(c, cur.Previous().Address(), title, "", false)
114}
115func OpenClaimIn(cur realm, courtSlug string, folderID uint64, title, body string) uint64 {
116 if !cur.IsCurrent() {
117 panic(errStaleRealm)
118 }
119 c := mustCourt(courtSlug)
120 if courtIsPurged(c) {
121 panic("kourtv2: this court is purged; no new claims may be opened")
122 }
123 cm := ensureMod(c)
124 f := cm.mustFolder(folderID)
125 if f.retired || f.purged {
126 panic("kourtv2: that set is retired; a claim filed there would be unreachable")
127 }
128 who := cur.Previous().Address()
129 id := openClaim(c, who, title, body, false)
130 fileInto(f, id)
131 emitModAct(c.id, id, "folder-add", who)
132 return id
133}
134func OpenClaimP(cur realm, courtSlug, title, body string) uint64 {
135 if !cur.IsCurrent() {
136 panic(errStaleRealm)
137 }
138 c := mustCourt(courtSlug)
139 if courtIsPurged(c) {
140 panic("kourtv2: this court is purged; no new claims may be opened")
141 }
142 return openClaim(c, cur.Previous().Address(), title, body, false)
143}
144func OpenClaimSeeded(cur realm, courtSlug, title string) uint64 {
145 if !cur.IsCurrent() {
146 panic(errStaleRealm)
147 }
148 who := cur.Previous().Address()
149 c := mustCourt(courtSlug)
150 if courtIsPurged(c) {
151 panic("kourtv2: this court is purged; no new claims may be opened")
152 }
153 cm := ensureMod(c)
154 requireActiveMod(cm, who)
155 if cm.installedByMeta {
156 panic("kourtv2: a meta-installed moderator set may not seed claims; seed after a local election")
157 }
158 return openClaim(c, who, title, "", true)
159}
160func OpenClaimPM(cur realm, courtSlug, title, body, media string) uint64 {
161 if !cur.IsCurrent() {
162 panic(errStaleRealm)
163 }
164 c := mustCourt(courtSlug)
165 if courtIsPurged(c) {
166 panic("kourtv2: this court is purged; no new claims may be opened")
167 }
168 items := parseMediaArg(media)
169 id := openClaim(c, cur.Previous().Address(), title, body, false)
170 if len(items) > 0 {
171 mustClaim(c, id).media = items
172 }
173 return id
174}
175func openClaim(c *Court, who address, title, body string, seeded bool) uint64 {
176 if runeLen(title) == 0 || runeLen(title) > 200 {
177 panic("kourtv2: a claim title is 1..200 characters")
178 }
179 if runeLen(body) > maxClaimBodyLen {
180 panic("kourtv2: a claim body is at most " + strconv.Itoa(maxClaimBodyLen) + " characters")
181 }
182 var dep, fee int64
183 if !seeded {
184 dep = effMinDeposit(c)
185 fee = dep * depositFeeBps / 10000
186 mustSpendable(c, who, dep+fee)
187 c.coin.Transfer(who, c.escrow, dep+fee)
188 c.deposit += dep + fee
189 }
190 c.nextID++
191 id := c.nextID
192 cs := &claimState{
193 id: id,
194 title: title,
195 body: body,
196 author: who,
197 deposit: dep,
198 fee: fee,
199 seeded: seeded,
200 openedAt: heightNow(),
201 openedAtTime: nowTime(),
202 stakers: bptree.NewBPTree32(),
203 oi: twap.New(epochBlocks, twapBuckets, 8),
204 yes: twap.New(epochBlocks, twapBuckets, 8),
205 provisional: -1,
206 }
207 c.claims.Set(beClaimKey(id), cs)
208 if seeded {
209 pendingEnter(c, cs)
210 }
211 onClaimOpened(c, cs)
212 chain.Emit("ClaimOpened",
213 "court", c.id,
214 "claim", strconv.FormatUint(id, 10),
215 "author", who.String(),
216 "seeded", strconv.FormatBool(seeded),
217 )
218 return id
219}
220func EditClaimTitle(cur realm, courtSlug string, claimID uint64, newTitle string) {
221 if !cur.IsCurrent() {
222 panic(errStaleRealm)
223 }
224 who := cur.Previous().Address()
225 c := mustCourt(courtSlug)
226 cs := mustClaim(c, claimID)
227 if who != cs.author {
228 panic("kourtv2: only the author may edit the title")
229 }
230 if len(newTitle) == 0 || len(newTitle) > 200 {
231 panic("kourtv2: a claim title is 1..200 characters")
232 }
233 if cs.closed || cs.frozenAt != 0 {
234 panic("kourtv2: the claim is no longer editable")
235 }
236 if passed, known := pastDeadline(cs.openedAtTime, blocksToSecs(c.params.stakeOpenDelayBlocks)); (known && passed) ||
237 (!known && heightNow() >= cs.openedAt+c.params.stakeOpenDelayBlocks) {
238 panic("kourtv2: the polish window has closed; the title is frozen")
239 }
240 if cs.yesStake != 0 || cs.noStake != 0 {
241 panic("kourtv2: staking has begun; the title is frozen")
242 }
243 cs.title = newTitle
244 onTitleEdited(c, cs)
245 chain.Emit("ClaimTitleEdited", "court", c.id, "claim", strconv.FormatUint(claimID, 10))
246}
247func CloseDeadClaim(cur realm, courtSlug string, claimID uint64) {
248 if !cur.IsCurrent() {
249 panic(errStaleRealm)
250 }
251 c := mustCourt(courtSlug)
252 cs := mustClaim(c, claimID)
253 if cs.closed {
254 panic("kourtv2: already closed")
255 }
256 if cs.frozenAt != 0 {
257 panic("kourtv2: this claim has an answer; it settles, it does not die")
258 }
259 if passed, known := pastDeadline(cs.openedAtTime, deadClaimSecs); (known && !passed) ||
260 (!known && heightNow() < cs.openedAt+deadClaimTimeout) {
261 panic("kourtv2: the dead-claim timeout has not passed")
262 }
263 cs.closed = true
264 pendingExit(c, cs)
265 stripExit(c, cs)
266 releaseMetaLatchIfMeta(c, cs)
267 if cs.deposit > 0 {
268 c.coin.Transfer(c.escrow, cs.author, cs.deposit)
269 c.deposit -= cs.deposit
270 cs.deposit = 0
271 }
272 if cs.fee > 0 {
273 c.coin.Burn(c.escrow, cs.fee)
274 c.deposit -= cs.fee
275 cs.fee = 0
276 }
277}
278func ClaimCount(courtSlug string) uint64 { return mustCourt(courtSlug).nextID }
279func ClaimTitle(courtSlug string, claimID uint64) string {
280 c := mustCourt(courtSlug)
281 return claimTitleFor(c, mustClaim(c, claimID))
282}
283func ClaimBody(courtSlug string, claimID uint64) string {
284 c := mustCourt(courtSlug)
285 return claimBodyFor(c, mustClaim(c, claimID))
286}
287func ClaimMedia(courtSlug string, claimID uint64) string {
288 c := mustCourt(courtSlug)
289 return encodeMedia(claimMediaVisible(c, mustClaim(c, claimID)))
290}
291func ClaimAuthor(courtSlug string, claimID uint64) address {
292 return mustClaim(mustCourt(courtSlug), claimID).author
293}
294func ClaimClosed(courtSlug string, claimID uint64) bool {
295 return mustClaim(mustCourt(courtSlug), claimID).closed
296}
297func ClaimSeeded(courtSlug string, claimID uint64) bool {
298 return mustClaim(mustCourt(courtSlug), claimID).seeded
299}
300func StakePools(courtSlug string, claimID uint64) (yes, no int64) {
301 cs := mustClaim(mustCourt(courtSlug), claimID)
302 return cs.yesStake, cs.noStake
303}
304func TrailingOI(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) {
305 cs := mustClaim(mustCourt(courtSlug), claimID)
306 return cs.oi.Average(heightNow(), window)
307}
308func PoolConviction(courtSlug string, claimID uint64) (yes, no int64) {
309 cs := mustClaim(mustCourt(courtSlug), claimID)
310 return convToCC(cs.yesConvHi, cs.yesConvLo), convToCC(cs.noConvHi, cs.noConvLo)
311}
312func TrailingYes(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) {
313 cs := mustClaim(mustCourt(courtSlug), claimID)
314 return cs.yes.Average(heightNow(), window)
315}
316func DocketSeries(courtSlug string, ids string, window int64) string {
317 c := mustCourt(courtSlug)
318 now := heightNow()
319 out := strings.Builder{}
320 sent := 0
321 for _, part := range strings.Split(ids, ",") {
322 if sent >= seriesPageMax {
323 break
324 }
325 part = strings.TrimSpace(part)
326 if part == "" {
327 continue
328 }
329 id, err := strconv.ParseUint(part, 10, 64)
330 if err != nil {
331 continue
332 }
333 v := c.claims.Get(beClaimKey(id))
334 if v == nil {
335 continue
336 }
337 cs := v.(*claimState)
338 oi, oiMature := cs.oi.Average(now, window)
339 yes, yesMature := cs.yes.Average(now, window)
340 if sent > 0 {
341 out.WriteString(";")
342 }
343 out.WriteString(strconv.FormatUint(id, 10))
344 out.WriteString(":" + strconv.FormatInt(cs.yesStake, 10))
345 out.WriteString(":" + strconv.FormatInt(cs.noStake, 10))
346 out.WriteString(":" + strconv.FormatInt(convToCC(cs.yesConvHi, cs.yesConvLo), 10))
347 out.WriteString(":" + strconv.FormatInt(convToCC(cs.noConvHi, cs.noConvLo), 10))
348 out.WriteString(":" + strconv.FormatInt(oi, 10))
349 out.WriteString(":" + boolDigit(oiMature))
350 out.WriteString(":" + strconv.FormatInt(yes, 10))
351 out.WriteString(":" + boolDigit(yesMature))
352 sent++
353 }
354 return out.String()
355}
356const seriesPageMax = 60
357func boolDigit(b bool) string {
358 if b {
359 return "1"
360 }
361 return "0"
362}
363func mustClaim(c *Court, id uint64) *claimState {
364 v := c.claims.Get(beClaimKey(id))
365 if v == nil {
366 panic("kourtv2: no such claim")
367 }
368 return v.(*claimState)
369}
370func beClaimKey(id uint64) string {
371 b := [8]byte{
372 byte(id >> 56), byte(id >> 48), byte(id >> 40), byte(id >> 32),
373 byte(id >> 24), byte(id >> 16), byte(id >> 8), byte(id),
374 }
375 return string(b[:])
376}