association.gno
9.24 Kb · 385 lines
1package kourtv3
2import (
3 "strconv"
4 "strings"
5 bptree "gno.land/p/nt/bptree/v0"
6)
7const (
8 assocSupports = int8(0)
9 assocContests = int8(1)
10 maxAssocOut = 32
11 maxAssocIn = 64
12 maxAssocInPerAuthor = 4
13)
14const assocBondDefaultInit = int64(1_000_000)
15var assocBondDefault = assocBondDefaultInit
16func assocBondFor(c *Court) int64 {
17 if c.assocBond > 0 {
18 return c.assocBond
19 }
20 return assocBondDefault
21}
22func SetAssociationBondDefault(cur realm, amount int64) {
23 if !cur.IsCurrent() {
24 panic(errStaleRealm)
25 }
26 d := ensureGlobalDAO()
27 if cur.Previous().Address() != d.admin {
28 panic("kourtv3: only the global DAO admin sets the default association bond")
29 }
30 if amount < 0 {
31 panic("kourtv3: an association bond cannot be negative")
32 }
33 assocBondDefault = amount
34}
35func SetCourtAssociationBond(cur realm, courtSlug string, amount int64) {
36 if !cur.IsCurrent() {
37 panic(errStaleRealm)
38 }
39 who := cur.Previous().Address()
40 c := mustCourt(courtSlug)
41 requireActiveMod(ensureMod(c), who)
42 if amount < 0 {
43 panic("kourtv3: an association bond cannot be negative")
44 }
45 c.assocBond = amount
46}
47func AssociationBond(courtSlug string) int64 { return assocBondFor(mustCourt(courtSlug)) }
48func AssociationBondDefault() int64 { return assocBondDefault }
49const assocBondWindowSecs = int64(14 * 86400)
50func mustAssocEdge(c *Court, from, to uint64) *assocEdge {
51 if c.assocOut == nil {
52 panic("kourtv3: no such association")
53 }
54 v := c.assocOut.Get(assocKey(from, to))
55 if v == nil {
56 panic("kourtv3: no such association")
57 }
58 return v.(*assocEdge)
59}
60func dropAssocEdge(c *Court, from, to uint64) {
61 c.assocOut.Remove(assocKey(from, to))
62 c.assocIn.Remove(assocKey(to, from))
63}
64func burnAssocBond(c *Court, e *assocEdge) {
65 amt := e.bond
66 e.bond = 0
67 c.coin.Burn(c.escrow, amt)
68}
69func releaseAssocBond(c *Court, e *assocEdge) {
70 if e.bond <= 0 {
71 return
72 }
73 amt := e.bond
74 e.bond = 0
75 c.coin.Transfer(c.escrow, e.author, amt)
76}
77func ApproveAssociation(cur realm, courtSlug string, from, to uint64) {
78 if !cur.IsCurrent() {
79 panic(errStaleRealm)
80 }
81 who := cur.Previous().Address()
82 c := mustCourt(courtSlug)
83 requireActiveMod(ensureMod(c), who)
84 e := mustAssocEdge(c, from, to)
85 if e.bond <= 0 {
86 panic("kourtv3: no bond is held on that edge")
87 }
88 releaseAssocBond(c, e)
89}
90func DisapproveAssociation(cur realm, courtSlug string, from, to uint64) {
91 if !cur.IsCurrent() {
92 panic(errStaleRealm)
93 }
94 who := cur.Previous().Address()
95 c := mustCourt(courtSlug)
96 requireActiveMod(ensureMod(c), who)
97 e := mustAssocEdge(c, from, to)
98 if e.bond <= 0 {
99 panic("kourtv3: no bond is held on that edge; remove it instead")
100 }
101 burnAssocBond(c, e)
102 dropAssocEdge(c, from, to)
103}
104func ClaimAssociationBond(cur realm, courtSlug string, from, to uint64) {
105 if !cur.IsCurrent() {
106 panic(errStaleRealm)
107 }
108 who := cur.Previous().Address()
109 c := mustCourt(courtSlug)
110 e := mustAssocEdge(c, from, to)
111 if e.bond <= 0 {
112 panic("kourtv3: no bond is held on that edge")
113 }
114 if e.author != who {
115 panic("kourtv3: only the edge's author may take its bond back")
116 }
117 if nowTime() < e.bondUntil {
118 panic("kourtv3: the moderation window on that bond has not closed yet")
119 }
120 releaseAssocBond(c, e)
121}
122func pendingIncident(c *Court, claimID uint64) ([]uint64, []uint64) {
123 outs, ins := []uint64{}, []uint64{}
124 if c.assocOut == nil {
125 return outs, ins
126 }
127 p := beClaimKey(claimID)
128 c.assocOut.Iterate(p, "", func(key string, v any) bool {
129 if !strings.HasPrefix(key, p) {
130 return true
131 }
132 if e := v.(*assocEdge); e.bond > 0 {
133 outs = append(outs, claimIDFromKey(key[8:]))
134 }
135 return false
136 })
137 c.assocIn.Iterate(p, "", func(key string, v any) bool {
138 if !strings.HasPrefix(key, p) {
139 return true
140 }
141 if e := v.(*assocEdge); e.bond > 0 {
142 ins = append(ins, claimIDFromKey(key[8:]))
143 }
144 return false
145 })
146 return outs, ins
147}
148func ApproveAllAssociations(cur realm, courtSlug string, claimID uint64) int {
149 if !cur.IsCurrent() {
150 panic(errStaleRealm)
151 }
152 who := cur.Previous().Address()
153 c := mustCourt(courtSlug)
154 requireActiveMod(ensureMod(c), who)
155 mustClaim(c, claimID)
156 outs, ins := pendingIncident(c, claimID)
157 n := 0
158 for _, to := range outs {
159 releaseAssocBond(c, mustAssocEdge(c, claimID, to))
160 n++
161 }
162 for _, from := range ins {
163 releaseAssocBond(c, mustAssocEdge(c, from, claimID))
164 n++
165 }
166 return n
167}
168func DisapproveAllAssociations(cur realm, courtSlug string, claimID uint64, reason string) int {
169 if !cur.IsCurrent() {
170 panic(errStaleRealm)
171 }
172 who := cur.Previous().Address()
173 c := mustCourt(courtSlug)
174 cm := ensureMod(c)
175 requireActiveMod(cm, who)
176 checkReason(reason)
177 mustClaim(c, claimID)
178 key := "assocburn:" + strconv.FormatUint(claimID, 10)
179 opened := pendingOpenedAt(cm.pending, key)
180 fire, _ := approveAction(cm.pending, key, who, reason, cm.m)
181 if !fire {
182 return 0
183 }
184 if opened == 0 {
185 opened = heightNow()
186 }
187 outs, ins := pendingIncident(c, claimID)
188 n := 0
189 for _, to := range outs {
190 n += burnIfOlder(c, claimID, to, opened)
191 }
192 for _, from := range ins {
193 n += burnIfOlder(c, from, claimID, opened)
194 }
195 return n
196}
197func burnIfOlder(c *Court, from, to uint64, opened int64) int {
198 e := mustAssocEdge(c, from, to)
199 if e.bond <= 0 || e.at > opened {
200 return 0
201 }
202 burnAssocBond(c, e)
203 dropAssocEdge(c, from, to)
204 return 1
205}
206type assocEdge struct {
207 bond int64
208 bondUntil int64
209 stance int8
210 author address
211 at int64
212}
213func ensureAssocs(c *Court) {
214 if c.assocOut == nil {
215 c.assocOut = bptree.NewBPTree32()
216 }
217 if c.assocIn == nil {
218 c.assocIn = bptree.NewBPTree32()
219 }
220}
221func assocKey(a, b uint64) string { return beClaimKey(a) + beClaimKey(b) }
222func assocTextGone(c *Court, id uint64) bool {
223 if c.mod == nil {
224 return false
225 }
226 clm := lookupClaimMod(c.mod, id)
227 return clm != nil && (clm.purged || clm.global)
228}
229func AddAssociation(cur realm, courtSlug string, from, to uint64, stance string) {
230 if !cur.IsCurrent() {
231 panic(errStaleRealm)
232 }
233 who := cur.Previous().Address()
234 c := mustCourt(courtSlug)
235 if from == to {
236 panic("kourtv3: a claim cannot bear on itself")
237 }
238 var st int8
239 switch stance {
240 case "supports":
241 st = assocSupports
242 case "contests":
243 st = assocContests
244 default:
245 panic("kourtv3: a stance is \"supports\" or \"contests\"")
246 }
247 f := mustClaim(c, from)
248 mustClaim(c, to)
249 bond := int64(0)
250 if f.author != who && !isActiveMod(ensureMod(c), who) {
251 bond = assocBondFor(c)
252 }
253 ensureAssocs(c)
254 k := assocKey(from, to)
255 if c.assocOut.Get(k) != nil {
256 panic("kourtv3: that edge already exists; remove it to change its stance")
257 }
258 outN := 0
259 c.assocOut.Iterate(beClaimKey(from), "", func(key string, _ any) bool {
260 if !strings.HasPrefix(key, beClaimKey(from)) {
261 return true
262 }
263 outN++
264 return outN > maxAssocOut
265 })
266 if outN >= maxAssocOut {
267 panic("kourtv3: this claim already asserts " + strconv.Itoa(maxAssocOut) + " associations")
268 }
269 inN, mine := 0, 0
270 c.assocIn.Iterate(beClaimKey(to), "", func(key string, v any) bool {
271 if !strings.HasPrefix(key, beClaimKey(to)) {
272 return true
273 }
274 inN++
275 if e, ok := v.(*assocEdge); ok && e.author == who {
276 mine++
277 }
278 return false
279 })
280 if inN >= maxAssocIn {
281 panic("kourtv3: that claim already carries " + strconv.Itoa(maxAssocIn) + " associations")
282 }
283 if mine >= maxAssocInPerAuthor {
284 panic("kourtv3: you already hold " + strconv.Itoa(maxAssocInPerAuthor) +
285 " associations on that claim")
286 }
287 if bond > 0 {
288 mustSpendable(c, who, bond)
289 c.coin.Transfer(who, c.escrow, bond)
290 }
291 e := &assocEdge{stance: st, author: who, at: heightNow(), bond: bond}
292 if bond > 0 {
293 e.bondUntil = nowTime() + assocBondWindowSecs
294 }
295 c.assocOut.Set(k, e)
296 c.assocIn.Set(assocKey(to, from), e)
297}
298func RemoveAssociation(cur realm, courtSlug string, from, to uint64) {
299 if !cur.IsCurrent() {
300 panic(errStaleRealm)
301 }
302 who := cur.Previous().Address()
303 c := mustCourt(courtSlug)
304 if c.assocOut == nil {
305 panic("kourtv3: no such association")
306 }
307 v := c.assocOut.Get(assocKey(from, to))
308 if v == nil {
309 panic("kourtv3: no such association")
310 }
311 e := v.(*assocEdge)
312 requireEdgeRemover(c, e.author, who)
313 if e.bond > 0 {
314 if e.author == who {
315 releaseAssocBond(c, e)
316 } else {
317 burnAssocBond(c, e)
318 }
319 }
320 dropAssocEdge(c, from, to)
321}
322func ClaimAssociations(courtSlug string, claimID uint64) string {
323 c := mustCourt(courtSlug)
324 mustClaim(c, claimID)
325 var b strings.Builder
326 b.WriteString("out:")
327 if c.assocOut != nil {
328 p := beClaimKey(claimID)
329 n := 0
330 c.assocOut.Iterate(p, "", func(key string, v any) bool {
331 if !strings.HasPrefix(key, p) {
332 return true
333 }
334 e := v.(*assocEdge)
335 other := claimIDFromKey(key[8:])
336 if assocTextGone(c, other) {
337 return false
338 }
339 if n > 0 {
340 b.WriteString(",")
341 }
342 b.WriteString(strconv.FormatUint(other, 10) + ":" + stanceCode(e.stance))
343 n++
344 return false
345 })
346 }
347 b.WriteString(";in:")
348 if c.assocIn != nil {
349 p := beClaimKey(claimID)
350 n := 0
351 c.assocIn.Iterate(p, "", func(key string, v any) bool {
352 if !strings.HasPrefix(key, p) {
353 return true
354 }
355 e := v.(*assocEdge)
356 other := claimIDFromKey(key[8:])
357 if assocTextGone(c, other) {
358 return false
359 }
360 if n > 0 {
361 b.WriteString(",")
362 }
363 b.WriteString(strconv.FormatUint(other, 10) + ":" + stanceCode(e.stance))
364 n++
365 return false
366 })
367 }
368 return b.String()
369}
370func stanceCode(s int8) string {
371 if s == assocContests {
372 return "c"
373 }
374 return "s"
375}
376func claimIDFromKey(k string) uint64 {
377 if len(k) < 8 {
378 return 0
379 }
380 var id uint64
381 for i := 0; i < 8; i++ {
382 id = id<<8 | uint64(k[i])
383 }
384 return id
385}