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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}