Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

association.gno

9.24 Kb · 385 lines
  1package kourt
  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("kourtv2: only the global DAO admin sets the default association bond")
 29	}
 30	if amount < 0 {
 31		panic("kourtv2: 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("kourtv2: 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("kourtv2: no such association")
 53	}
 54	v := c.assocOut.Get(assocKey(from, to))
 55	if v == nil {
 56		panic("kourtv2: 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("kourtv2: 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("kourtv2: 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("kourtv2: no bond is held on that edge")
113	}
114	if e.author != who {
115		panic("kourtv2: only the edge's author may take its bond back")
116	}
117	if nowTime() < e.bondUntil {
118		panic("kourtv2: 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("kourtv2: 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("kourtv2: 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("kourtv2: 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("kourtv2: 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("kourtv2: that claim already carries " + strconv.Itoa(maxAssocIn) + " associations")
282	}
283	if mine >= maxAssocInPerAuthor {
284		panic("kourtv2: 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("kourtv2: no such association")
306	}
307	v := c.assocOut.Get(assocKey(from, to))
308	if v == nil {
309		panic("kourtv2: 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}