Transaction
FF1676798375A0…BA956AE7D59F
Block 9,676 · index 0 · indexed
Summary
- Hash
- FF1676798375A0B312AFDBA92638C392A18E55EA6903FF6F460DBA956AE7D59F
- Block
- 9,676
- Size
- 303679 bytes
- Gas used
- 388,602,281 / 900,000,000
- Fee
- 1125000 ugnot
- Status
- success
Messages
- Attached funds
- 200000000 ugnot
Arguments · 24
- #1kourt
- #2adminparams.gno
- #3package kourt import ( "strconv" "strings" ) func adminAddr() address { if globalDAO != nil { return globalDAO.admin } return directoryAdmin } func purgeThreshold() int { if globalDAO != nil { return globalDAO.purgeM } return daoBootstrapM } func daoSeats() int { if globalDAO != nil { return globalDAO.n } return daoBootstrapN } func AdminParams() string { l := ladderDefault r := creditRatesDefault hosts := MediaHosts() exact, suffix := hosts, "" if i := strings.Index(hosts, "|"); i >= 0 { exact, suffix = hosts[:i], hosts[i+1:] } n := func(v int64) string { return strconv.FormatInt(v, 10) } return "admin:" + adminAddr().String() + ";creationBurn:" + n(courtCreationBurn) + ";assocBond:" + n(assocBondDefault) + ";purgeM:" + strconv.Itoa(purgeThreshold()) + ";t1:" + n(l.t1Bps) + ";t2:" + n(l.t2Bps) + ";t3:" + n(l.t3Bps) + ";entry:" + n(l.postsEntry) + ";l2:" + n(l.postsT2) + ";l3:" + n(l.postsT3()) + ";flag:" + n(r.flag) + ";dispute:" + n(r.dispute) + ";authorHigh:" + n(r.authorHigh) + ";conviction:" + n(r.conviction) + ";unadjudicated:" + n(r.unadjudicated) + ";domain:" + siteDomain + ";mediaExact:" + exact + ";mediaSuffix:" + suffix } const adminParamsPath = "admin-params" func renderAdminParams() string { l := ladderDefault r := creditRatesDefault n := func(v int64) string { return strconv.FormatInt(v, 10) } var b strings.Builder b.WriteString("# Realm parameters\n\n") b.WriteString("Everything the global DAO admin can set, and what it is set " + "to now. A court's moderators set their own court's bond, ladder and " + "standing rates; those are on the court's page, not here.\n\n") b.WriteString("Admin: `" + adminAddr().String() + "`\n\n") b.WriteString("| Setting | Now | What it does | Set with |\n") b.WriteString("|---|---|---|---|\n") b.WriteString("| Court creation burn | " + ugnotAmt(courtCreationBurn) + " GNOT | The minimum a new court costs to open. The whole payment is " + "burned. 0 turns it off, and off is the shipped default. | " + "`SetCourtCreationBurn` |\n") b.WriteString("| Association bond | " + ccAmt(assocBondDefault) + " CC | What a stranger posts to attach an edge, in courts that set no " + "override. | `SetAssociationBondDefault` |\n") b.WriteString("| Purge threshold | " + strconv.Itoa(purgeThreshold()) + " of " + strconv.Itoa(daoSeats()) + " | How many global moderators must agree to purge. | " + "`SetPurgeThreshold` |\n") b.WriteString("| Posting ladder | " + n(l.t1Bps) + "/" + n(l.t2Bps) + "/" + n(l.t3Bps) + " bps at " + n(l.postsEntry) + "/" + n(l.postsT2) + "/" + n(l.postsT3()) + " posts | The default rungs a court inherits: the share " + "earned at each tier and the post counts that reach them. | " + "`SetLadderDefault` |\n") b.WriteString("| Standing rates | flag " + n(r.flag) + " (inert), dispute " + n(r.dispute) + ", author " + n(r.authorHigh) + ", conviction " + n(r.conviction) + ", unadjudicated " + n(r.unadjudicated) + " bps | The default credit each act earns toward standing. Policing " + "ranks above authorship, which ranks above staking. The flag rate is " + "still settable and still bounds what authorship may be set to, but no " + "path credits it since the quality lane was removed. | " + "`SetCreditRatesDefault` |\n") dom := siteDomain if dom == "" { dom = "_none_" } b.WriteString("| Site domain | " + dom + " | The website this realm belongs " + "to. Also allow-lists that domain for images without listing it below. | " + "`SetSiteDomain` |\n") hosts := MediaHosts() if hosts == "|" { hosts = "_none_" } b.WriteString("| Media hosts | " + hosts + " | Where images may be loaded " + "from: exact hosts before the bar, suffixes after it. | " + "`SetMediaHosts` |\n") b.WriteString("\nBurned to date, across every court and every creation: **" + ugnotAmt(BurnedGNOT()) + " GNOT**, held at `" + BurnSink().String() + "` — an address derived from a sub-realm identity that is never created, " + "so no key to it exists and nothing can ever be spent from it.\n") return b.String() }
- #4answer.gno
- #5package kourt const answerWindow = 3 * epochBlocks func PostAnswer(cur realm, courtSlug string, claimID uint64, verdict int) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) cs := mustClaim(c, claimID) mustSide(verdict) if cs.closed { panic("kourtv2: this claim is closed") } if cs.frozenAt != 0 { panic("kourtv2: this claim already has an answer") } now := heightNow() if passed, known := pastDeadline(cs.openedAtTime, deadClaimSecs); (known && passed) || (!known && now >= cs.openedAt+deadClaimTimeout) { panic("kourtv2: this claim is past the dead-claim timeout; it closes, it is not answered") } xbar, mature := cs.oi.Average(now, answerWindow) if !mature { panic("kourtv2: not enough stake history to answer yet") } if xbar < effMinAnswerX(c) { panic("kourtv2: trailing stake is below the minimum to answer") } inPriority := false if passed, known := pastDeadline(cs.openedAtTime, blocksToSecs(c.params.stakeOpenDelayBlocks+answerWindow)+priorityWindowSecs); priorityGateActive(c) && ((known && !passed) || (!known && now < cs.openedAt+c.params.stakeOpenDelayBlocks+answerWindow+priorityWindowBlocks)) { if !mayAnswerInPriority(c, who) { panic("kourtv2: the priority window is open — a qualified answerer may act first (wait ~24h)") } inPriority = true } touch(c) advancePools(c, cs) if life := lifeAvgStake(cs, now); life > xbar { xbar = life } bond := xbar * answerBondBps / 10000 if c.params.answerBondCapCC > 0 && bond > c.params.answerBondCapCC { bond = c.params.answerBondCapCC } convHi, convLo := cs.sideConv(verdict) floor := bondFloorAt(xbar, convHi, convLo) oppHi, oppLo := cs.sideConv(oppositeSide(verdict)) if opp := bondFloorAt(xbar, oppHi, oppLo); opp > floor { floor = opp } if floor > bond { bond = floor } if bond < 1 { bond = 1 } mustSpendable(c, who, bond) c.coin.Transfer(who, c.escrow, bond) cs.frozenAt = now cs.rateAccAtFreeze = c.rateAccFP cs.xBarFrozen = xbar cs.tierRef = tierRefAt(c) observeClaimSize(c, xbar) cs.yesStakeAtFreeze, cs.noStakeAtFreeze = cs.yesStake, cs.noStake cs.answer = int8(verdict) cs.answerer = who cs.answerBond = bond cs.answerBond0 = bond cs.answerHeight = now cs.answeredAtTime = nowTime() if inPriority { holdPriority(c, who, claimID) } pendingExit(c, cs) stripEnter(c, cs) onClaimAnswered(c, cs) } func HasAnswer(courtSlug string, claimID uint64) bool { return mustClaim(mustCourt(courtSlug), claimID).frozenAt != 0 } func AnswerVerdict(courtSlug string, claimID uint64) int { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.frozenAt == 0 { panic("kourtv2: no answer yet") } return int(cs.answer) } func AnswerBond(courtSlug string, claimID uint64) int64 { return mustClaim(mustCourt(courtSlug), claimID).answerBond } func Answerer(courtSlug string, claimID uint64) address { return mustClaim(mustCourt(courtSlug), claimID).answerer } func XBarFrozen(courtSlug string, claimID uint64) int64 { return mustClaim(mustCourt(courtSlug), claimID).xBarFrozen }
- #6answerbond.gno
- #7package kourt func settleAnswerBond(c *Court, cs *claimState) { c.coin.Transfer(c.escrow, cs.answerer, cs.answerBond) cs.answerBond = 0 } func (cs *claimState) sideConv(side int) (hi, lo uint64) { if side == sideYES { return cs.yesConvHi, cs.yesConvLo } return cs.noConvHi, cs.noConvLo } func bondFloorAt(xbar int64, convHi, convLo uint64) int64 { flat := xbar * bondFloorFlatBps / 10000 if draw := mulDiv128(convToCC(convHi, convLo), bondFloorConvBps, 10000); draw > flat { return draw } return flat } func bondFloorFor(cs *claimState) int64 { hi, lo := cs.sideConv(int(cs.answer)) return bondFloorAt(cs.xBarFrozen, hi, lo) }
- #8association.gno
- #9package kourt import ( "strconv" "strings" bptree "gno.land/p/nt/bptree/v0" ) const ( assocSupports = int8(0) assocContests = int8(1) maxAssocOut = 32 maxAssocIn = 64 maxAssocInPerAuthor = 4 ) const assocBondDefaultInit = int64(1_000_000) var assocBondDefault = assocBondDefaultInit func assocBondFor(c *Court) int64 { if c.assocBond > 0 { return c.assocBond } return assocBondDefault } func SetAssociationBondDefault(cur realm, amount int64) { if !cur.IsCurrent() { panic(errStaleRealm) } d := ensureGlobalDAO() if cur.Previous().Address() != d.admin { panic("kourtv2: only the global DAO admin sets the default association bond") } if amount < 0 { panic("kourtv2: an association bond cannot be negative") } assocBondDefault = amount } func SetCourtAssociationBond(cur realm, courtSlug string, amount int64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) requireActiveMod(ensureMod(c), who) if amount < 0 { panic("kourtv2: an association bond cannot be negative") } c.assocBond = amount } func AssociationBond(courtSlug string) int64 { return assocBondFor(mustCourt(courtSlug)) } func AssociationBondDefault() int64 { return assocBondDefault } const assocBondWindowSecs = int64(14 * 86400) func mustAssocEdge(c *Court, from, to uint64) *assocEdge { if c.assocOut == nil { panic("kourtv2: no such association") } v := c.assocOut.Get(assocKey(from, to)) if v == nil { panic("kourtv2: no such association") } return v.(*assocEdge) } func dropAssocEdge(c *Court, from, to uint64) { c.assocOut.Remove(assocKey(from, to)) c.assocIn.Remove(assocKey(to, from)) } func burnAssocBond(c *Court, e *assocEdge) { amt := e.bond e.bond = 0 c.coin.Burn(c.escrow, amt) } func releaseAssocBond(c *Court, e *assocEdge) { if e.bond <= 0 { return } amt := e.bond e.bond = 0 c.coin.Transfer(c.escrow, e.author, amt) } func ApproveAssociation(cur realm, courtSlug string, from, to uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) requireActiveMod(ensureMod(c), who) e := mustAssocEdge(c, from, to) if e.bond <= 0 { panic("kourtv2: no bond is held on that edge") } releaseAssocBond(c, e) } func DisapproveAssociation(cur realm, courtSlug string, from, to uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) requireActiveMod(ensureMod(c), who) e := mustAssocEdge(c, from, to) if e.bond <= 0 { panic("kourtv2: no bond is held on that edge; remove it instead") } burnAssocBond(c, e) dropAssocEdge(c, from, to) } func ClaimAssociationBond(cur realm, courtSlug string, from, to uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) e := mustAssocEdge(c, from, to) if e.bond <= 0 { panic("kourtv2: no bond is held on that edge") } if e.author != who { panic("kourtv2: only the edge's author may take its bond back") } if nowTime() < e.bondUntil { panic("kourtv2: the moderation window on that bond has not closed yet") } releaseAssocBond(c, e) } func pendingIncident(c *Court, claimID uint64) ([]uint64, []uint64) { outs, ins := []uint64{}, []uint64{} if c.assocOut == nil { return outs, ins } p := beClaimKey(claimID) c.assocOut.Iterate(p, "", func(key string, v any) bool { if !strings.HasPrefix(key, p) { return true } if e := v.(*assocEdge); e.bond > 0 { outs = append(outs, claimIDFromKey(key[8:])) } return false }) c.assocIn.Iterate(p, "", func(key string, v any) bool { if !strings.HasPrefix(key, p) { return true } if e := v.(*assocEdge); e.bond > 0 { ins = append(ins, claimIDFromKey(key[8:])) } return false }) return outs, ins } func ApproveAllAssociations(cur realm, courtSlug string, claimID uint64) int { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) requireActiveMod(ensureMod(c), who) mustClaim(c, claimID) outs, ins := pendingIncident(c, claimID) n := 0 for _, to := range outs { releaseAssocBond(c, mustAssocEdge(c, claimID, to)) n++ } for _, from := range ins { releaseAssocBond(c, mustAssocEdge(c, from, claimID)) n++ } return n } func DisapproveAllAssociations(cur realm, courtSlug string, claimID uint64, reason string) int { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireActiveMod(cm, who) checkReason(reason) mustClaim(c, claimID) key := "assocburn:" + strconv.FormatUint(claimID, 10) opened := pendingOpenedAt(cm.pending, key) fire, _ := approveAction(cm.pending, key, who, reason, cm.m) if !fire { return 0 } if opened == 0 { opened = heightNow() } outs, ins := pendingIncident(c, claimID) n := 0 for _, to := range outs { n += burnIfOlder(c, claimID, to, opened) } for _, from := range ins { n += burnIfOlder(c, from, claimID, opened) } return n } func burnIfOlder(c *Court, from, to uint64, opened int64) int { e := mustAssocEdge(c, from, to) if e.bond <= 0 || e.at > opened { return 0 } burnAssocBond(c, e) dropAssocEdge(c, from, to) return 1 } type assocEdge struct { bond int64 bondUntil int64 stance int8 author address at int64 } func ensureAssocs(c *Court) { if c.assocOut == nil { c.assocOut = bptree.NewBPTree32() } if c.assocIn == nil { c.assocIn = bptree.NewBPTree32() } } func assocKey(a, b uint64) string { return beClaimKey(a) + beClaimKey(b) } func assocTextGone(c *Court, id uint64) bool { if c.mod == nil { return false } clm := lookupClaimMod(c.mod, id) return clm != nil && (clm.purged || clm.global) } func AddAssociation(cur realm, courtSlug string, from, to uint64, stance string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) if from == to { panic("kourtv2: a claim cannot bear on itself") } var st int8 switch stance { case "supports": st = assocSupports case "contests": st = assocContests default: panic("kourtv2: a stance is \"supports\" or \"contests\"") } f := mustClaim(c, from) mustClaim(c, to) bond := int64(0) if f.author != who && !isActiveMod(ensureMod(c), who) { bond = assocBondFor(c) } ensureAssocs(c) k := assocKey(from, to) if c.assocOut.Get(k) != nil { panic("kourtv2: that edge already exists; remove it to change its stance") } outN := 0 c.assocOut.Iterate(beClaimKey(from), "", func(key string, _ any) bool { if !strings.HasPrefix(key, beClaimKey(from)) { return true } outN++ return outN > maxAssocOut }) if outN >= maxAssocOut { panic("kourtv2: this claim already asserts " + strconv.Itoa(maxAssocOut) + " associations") } inN, mine := 0, 0 c.assocIn.Iterate(beClaimKey(to), "", func(key string, v any) bool { if !strings.HasPrefix(key, beClaimKey(to)) { return true } inN++ if e, ok := v.(*assocEdge); ok && e.author == who { mine++ } return false }) if inN >= maxAssocIn { panic("kourtv2: that claim already carries " + strconv.Itoa(maxAssocIn) + " associations") } if mine >= maxAssocInPerAuthor { panic("kourtv2: you already hold " + strconv.Itoa(maxAssocInPerAuthor) + " associations on that claim") } if bond > 0 { mustSpendable(c, who, bond) c.coin.Transfer(who, c.escrow, bond) } e := &assocEdge{stance: st, author: who, at: heightNow(), bond: bond} if bond > 0 { e.bondUntil = nowTime() + assocBondWindowSecs } c.assocOut.Set(k, e) c.assocIn.Set(assocKey(to, from), e) } func RemoveAssociation(cur realm, courtSlug string, from, to uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) if c.assocOut == nil { panic("kourtv2: no such association") } v := c.assocOut.Get(assocKey(from, to)) if v == nil { panic("kourtv2: no such association") } e := v.(*assocEdge) requireEdgeRemover(c, e.author, who) if e.bond > 0 { if e.author == who { releaseAssocBond(c, e) } else { burnAssocBond(c, e) } } dropAssocEdge(c, from, to) } func ClaimAssociations(courtSlug string, claimID uint64) string { c := mustCourt(courtSlug) mustClaim(c, claimID) var b strings.Builder b.WriteString("out:") if c.assocOut != nil { p := beClaimKey(claimID) n := 0 c.assocOut.Iterate(p, "", func(key string, v any) bool { if !strings.HasPrefix(key, p) { return true } e := v.(*assocEdge) other := claimIDFromKey(key[8:]) if assocTextGone(c, other) { return false } if n > 0 { b.WriteString(",") } b.WriteString(strconv.FormatUint(other, 10) + ":" + stanceCode(e.stance)) n++ return false }) } b.WriteString(";in:") if c.assocIn != nil { p := beClaimKey(claimID) n := 0 c.assocIn.Iterate(p, "", func(key string, v any) bool { if !strings.HasPrefix(key, p) { return true } e := v.(*assocEdge) other := claimIDFromKey(key[8:]) if assocTextGone(c, other) { return false } if n > 0 { b.WriteString(",") } b.WriteString(strconv.FormatUint(other, 10) + ":" + stanceCode(e.stance)) n++ return false }) } return b.String() } func stanceCode(s int8) string { if s == assocContests { return "c" } return "s" } func claimIDFromKey(k string) uint64 { if len(k) < 8 { return 0 } var id uint64 for i := 0; i < 8; i++ { id = id<<8 | uint64(k[i]) } return id }
- #10board.gno
- #11package kourt import ( "strconv" "strings" bptree "gno.land/p/nt/bptree/v0" sanitize "gno.land/p/nt/markdown/sanitize/v0" ) const ( maxBoardTextLen = 2000 maxBoardRowsPerClaim = 512 maxBoardRowsPerAuthor = 8 maxBoardRepliesShown = 8 ) type boardRow struct { id uint64 parent uint64 author address text string score int64 scoreKey string at int64 hiddenByAuthor bool hiddenByMod bool global bool purged bool globalClearedAt int64 globalClearedAtTime int64 } func boardMark(r *boardRow) string { switch { case r.purged: return "x" case r.global: return "g" case r.hiddenByAuthor || r.hiddenByMod: return "h" } return "." } func ensureBoard(cs *claimState) { if cs.board == nil { cs.board = bptree.NewBPTree32() cs.boardTop = bptree.NewBPTree32() cs.boardWrote = bptree.NewBPTree32() } } func ensureKids(cs *claimState) *bptree.BPTree { if cs.boardKids == nil { cs.boardKids = bptree.NewBPTree32() } return cs.boardKids } func ensureScoreIdx(cs *claimState) *bptree.BPTree { if cs.boardScore == nil { cs.boardScore = bptree.NewBPTree32() } return cs.boardScore } func ensureVoted(cs *claimState) *bptree.BPTree { if cs.boardVoted == nil { cs.boardVoted = bptree.NewBPTree32() } return cs.boardVoted } func boardOpen(cs *claimState) bool { return cs.verdictAt == 0 && !cs.closed } func boardWroteBy(cs *claimState, who address) int64 { if cs.boardWrote == nil { return 0 } if v := cs.boardWrote.Get(string(who)); v != nil { return v.(int64) } return 0 } func PostComment(cur realm, courtSlug string, claimID uint64, parentRow uint64, text string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; it takes no new content") } cs := mustClaim(c, claimID) if !boardOpen(cs) { panic("kourtv2: this claim is settled; its board is closed to new comments") } mustBoardWritable(c, cs, who) text = strings.TrimSpace(text) if text == "" { panic("kourtv2: a comment needs text") } if runeLen(text) > maxBoardTextLen { panic("kourtv2: a comment is at most " + strconv.Itoa(maxBoardTextLen) + " characters") } if parentRow != 0 { p := mustBoardRow(cs, parentRow) if p.parent != 0 { panic("kourtv2: replies are one level deep; reply to the comment, not to a reply") } } if boardWroteBy(cs, who) >= maxBoardRowsPerAuthor { panic("kourtv2: you already have " + strconv.Itoa(maxBoardRowsPerAuthor) + " comments on this claim") } if cs.board != nil && cs.board.Size() >= maxBoardRowsPerClaim { panic("kourtv2: this claim's board is full") } mustSpendPost(c, who) ensureBoard(cs) cs.boardNextID++ r := &boardRow{ id: cs.boardNextID, parent: parentRow, author: who, text: text, at: heightNow(), } k := beClaimKey(r.id) cs.board.Set(k, r) if parentRow == 0 { cs.boardTop.Set(k, r) scoreIndexPut(cs, r) } else { ensureKids(cs).Set(beClaimKey(parentRow)+k, r) } cs.boardWrote.Set(string(who), boardWroteBy(cs, who)+1) return r.id } func mustBoardRow(cs *claimState, rowID uint64) *boardRow { if cs.board == nil { panic("kourtv2: no such comment") } v := cs.board.Get(beClaimKey(rowID)) if v == nil { panic("kourtv2: no such comment") } return v.(*boardRow) } func HideOwnComment(cur realm, courtSlug string, claimID uint64, rowID uint64, hide bool) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() cs := mustClaim(mustCourt(courtSlug), claimID) r := mustBoardRow(cs, rowID) if r.author != who { panic("kourtv2: only a comment's author may hide their own comment") } r.hiddenByAuthor = hide if hide { scoreIndexDrop(cs, r) } else { scoreIndexPut(cs, r) } } func BoardSize(courtSlug string, claimID uint64) int { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.board == nil { return 0 } return cs.board.Size() } func BoardCounts(courtSlug string, ids string) string { c := mustCourt(courtSlug) out := strings.Builder{} sent := 0 for _, part := range strings.Split(ids, ",") { if sent >= seriesPageMax { break } part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.ParseUint(part, 10, 64) if err != nil { continue } v := c.claims.Get(beClaimKey(id)) if v == nil { continue } cs := v.(*claimState) rows, threads := 0, 0 if cs.board != nil { rows = cs.board.Size() } if cs.boardTop != nil { threads = cs.boardTop.Size() } if sent > 0 { out.WriteString(";") } out.WriteString(strconv.FormatUint(id, 10)) out.WriteString(":" + strconv.Itoa(rows)) out.WriteString(":" + strconv.Itoa(threads)) sent++ } return out.String() } func BoardHiddenCount(courtSlug string, claimID uint64) int { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardTop == nil { return 0 } n := 0 cs.boardTop.Iterate("", "", func(_ string, v any) bool { if boardMark(v.(*boardRow)) != "." { n++ } return false }) return n } func BoardOpen(courtSlug string, claimID uint64) bool { return boardOpen(mustClaim(mustCourt(courtSlug), claimID)) } func CommentsWrittenBy(courtSlug string, claimID uint64, who address) int64 { return boardWroteBy(mustClaim(mustCourt(courtSlug), claimID), who) } func BoardNewest(courtSlug string, claimID uint64, offset, count int) string { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardTop == nil || count <= 0 { return "" } if count > maxBoardRepliesShown*8 { count = maxBoardRepliesShown * 8 } var b strings.Builder cs.boardTop.ReverseIterateByOffset(offset, count, func(_ string, v any) bool { r := v.(*boardRow) b.WriteString(strconv.FormatUint(r.id, 10) + "|" + r.author.String() + "|" + strconv.Itoa(boardReplyCount(cs, r.id)) + "|") if m := boardMark(r); m != "." { b.WriteString(m + "|" + strconv.FormatInt(r.at, 10) + "|\n") return false } b.WriteString(".|" + strconv.FormatInt(r.at, 10) + "|" + boardTextFor(r) + "\n") return false }) return b.String() } func boardReplyCount(cs *claimState, parent uint64) int { if cs.boardKids == nil { return 0 } pre := beClaimKey(parent) n := 0 cs.boardKids.Iterate(pre, "", func(k string, _ any) bool { if !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) { return true } n++ return false }) return n } func boardKeyMine(pre, k string) bool { return len(k) == len(pre)+8 } func BoardReplies(courtSlug string, claimID uint64, parent uint64) string { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardKids == nil { return "" } pre := beClaimKey(parent) var b strings.Builder n := 0 cs.boardKids.Iterate(pre, "", func(k string, v any) bool { if !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) { return true } r := v.(*boardRow) b.WriteString(strconv.FormatUint(r.id, 10) + "|" + r.author.String() + "|") if m := boardMark(r); m != "." { b.WriteString(m + "|" + strconv.FormatInt(r.at, 10) + "|\n") } else { b.WriteString(".|" + strconv.FormatInt(r.at, 10) + "|" + boardTextFor(r) + "\n") } n++ return n >= maxBoardRepliesShown }) return b.String() } func BoardPartyRows(courtSlug string, claimID uint64) string { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardTop == nil { return "" } var b strings.Builder for _, r := range boardPartyRows(cs) { role := "answerer" if r.author == cs.author { role = "author" } b.WriteString(strconv.FormatUint(r.id, 10) + "|" + r.author.String() + "|" + role + "|") if m := boardMark(r); m != "." { b.WriteString(m + "|" + strconv.FormatInt(r.at, 10) + "|\n") continue } b.WriteString(".|" + strconv.FormatInt(r.at, 10) + "|" + boardTextFor(r) + "\n") } return b.String() } func boardTextFor(r *boardRow) string { if boardMark(r) != "." { return "" } return escapeWireText(r.text) } func escapeWireText(s string) string { if !strings.ContainsAny(s, "\\\n\r") { return s } var b strings.Builder for i := 0; i < len(s); i++ { switch s[i] { case '\\': b.WriteString("\\\\") case '\n': b.WriteString("\\n") case '\r': b.WriteString("\\r") default: b.WriteByte(s[i]) } } return b.String() } func renderBoardPage(slug string, claimID uint64) string { return renderBoardIn(slug, claimID, false) } func renderBoardTop(slug string, claimID uint64) string { return renderBoardIn(slug, claimID, true) } func renderBoardHidden(slug string, claimID uint64) string { c := mustCourt(slug) cs := mustClaim(c, claimID) base := "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) var b strings.Builder b.WriteString("# Hidden comments on claim " + strconv.FormatUint(claimID, 10) + "\n\n") b.WriteString("[← the comments](" + base + "/board)\n\n") if cs.boardTop == nil { b.WriteString("_Nothing has been hidden here._\n") return b.String() } n := 0 cs.boardTop.Iterate("", "", func(_ string, v any) bool { r := v.(*boardRow) m := boardMark(r) if m == "." { return false } n++ b.WriteString("---\n\n**" + sanitize.InlineText(r.author.String()) + "**") b.WriteString(" · block " + strconv.FormatInt(r.at, 10)) if m != "x" { b.WriteString(" · [#" + strconv.FormatUint(r.id, 10) + "](" + base + "/board/" + strconv.FormatUint(r.id, 10) + ")") } else { b.WriteString(" · #" + strconv.FormatUint(r.id, 10)) } b.WriteString("\n\n") switch { case r.purged: b.WriteString("> _Removed on legal grounds. The text is gone from the record; " + "the row remains._\n\n") case r.global: b.WriteString("> _Withheld pending a legal determination._\n\n") case r.hiddenByMod: b.WriteString("> _Removed from the listing by this court's moderators._\n\n") b.WriteString(boardTextVisible(r) + "\n\n") default: b.WriteString("> _Withdrawn from the listing by its author._\n\n") b.WriteString(boardTextVisible(r) + "\n\n") } return false }) if n == 0 { b.WriteString("_Nothing has been hidden here._\n") } return b.String() } func boardPostLink(slug string, claimID, parentRow uint64) string { return "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt$help&func=PostComment" + "&courtSlug=" + slug + "&claimID=" + strconv.FormatUint(claimID, 10) + "&parentRow=" + strconv.FormatUint(parentRow, 10) } func renderBoardIn(slug string, claimID uint64, ranked bool) string { c := mustCourt(slug) cs := mustClaim(c, claimID) var b strings.Builder b.WriteString("# Comments on claim " + strconv.FormatUint(claimID, 10) + "\n\n") base := "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) b.WriteString("[← the claim](" + base + ")") if ranked { b.WriteString(" · **Top** · [Newest](" + base + "/board)") } else { b.WriteString(" · [Top](" + base + "/board/top) · **Newest**") } b.WriteString("\n\n") b.WriteString("[Write a comment](" + boardPostLink(slug, claimID, 0) + ") · " + "[Where you stand](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/me) · " + helpLink + "\n\n") if !boardOpen(cs) { b.WriteString("> This claim is settled. Its board is closed to new comments;\n" + "> what is here stays.\n\n") } if TextRedacted(c.id, claimID) { b.WriteString("> **The claim itself is withheld or removed on legal grounds.**\n" + "> These comments are a separate record and are not covered by that act.\n\n") } if at, until := claimBoardFrozenAt(c, claimID); until != 0 { when := "block " + strconv.FormatInt(until, 10) if at != 0 { when = strconv.FormatInt(at, 10) + " (" + when + ")" } b.WriteString("> **Comments are paused on this claim by this court's moderators**\n" + "> until " + when + ". The claim itself is\n" + "> unaffected: answers, stakes and the verdict continue as normal.\n\n") } if cs.boardTop == nil || cs.boardTop.Size() == 0 { b.WriteString("_No comments yet._\n") return b.String() } seen := map[uint64]bool{} if parties := boardPartyRows(cs); len(parties) > 0 { b.WriteString("## The parties\n\n") for _, r := range parties { seen[r.id] = true if boardMark(r) != "." { continue } b.WriteString(renderBoardOne(slug, claimID, cs, r, true)) } if ranked { b.WriteString("\n## Ranked\n\n") } else { b.WriteString("\n## Newest\n\n") } } shown := 0 if ranked { if cs.boardScore != nil { cs.boardScore.IterateByOffset(0, maxBoardRepliesShown*3, func(_ string, v any) bool { r := v.(*boardRow) if seen[r.id] { return false } b.WriteString(renderBoardOne(slug, claimID, cs, r, true)) shown++ return false }) } if shown == 0 { b.WriteString("_Nothing else upvoted yet — [Newest](" + base + "/board) is the fuller view._\n") } writeHiddenLink(&b, slug, claimID, base) return b.String() } cs.boardTop.ReverseIterateByOffset(0, maxBoardRepliesShown*3, func(_ string, v any) bool { r := v.(*boardRow) if seen[r.id] || boardMark(r) != "." { return false } b.WriteString(renderBoardOne(slug, claimID, cs, r, true)) shown++ return false }) if rest := cs.boardTop.Size() - shown - len(seen); rest > 0 { b.WriteString("\n_" + strconv.Itoa(rest) + " older comments._\n") } writeHiddenLink(&b, slug, claimID, base) return b.String() } func writeHiddenLink(b *strings.Builder, slug string, claimID uint64, base string) { if k := BoardHiddenCount(slug, claimID); k > 0 { b.WriteString("\n[" + strconv.Itoa(k) + " hidden comment" + plural2(k) + "](" + base + "/board/hidden)\n") } } func boardPartyRows(cs *claimState) []*boardRow { want := []address{cs.author} if cs.frozenAt != 0 && cs.answerer != "" && cs.answerer != cs.author { want = append(want, cs.answerer) } var out []*boardRow for _, who := range want { var newest *boardRow cs.boardTop.ReverseIterate("", "", func(_ string, v any) bool { r := v.(*boardRow) if r.author == who { newest = r return true } return false }) if newest != nil { out = append(out, newest) } } return out } func renderBoardRow(slug string, claimID uint64, rowArg string) string { id, err := strconv.ParseUint(rowArg, 10, 64) if err != nil { return "## Not found\nA comment id must be a number." } c := mustCourt(slug) cs := mustClaim(c, claimID) if cs.board == nil || cs.board.Get(beClaimKey(id)) == nil { return "## Not found\nNo comment by that id on this claim." } r := mustBoardRow(cs, id) var b strings.Builder b.WriteString("# " + claimTitleFor(c, cs) + "\n\n") base := "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) b.WriteString("Comment " + strconv.FormatUint(id, 10) + " on this claim in " + "[" + courtNameFor(c) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + ")\n\n") b.WriteString("[← the claim](" + base + ") · [all " + strconv.Itoa(cs.board.Size()) + " comments](" + base + "/board)\n\n") b.WriteString(renderBoardOne(slug, claimID, cs, r, false)) return b.String() } func renderBoardOne(slug string, claimID uint64, cs *claimState, r *boardRow, listing bool) string { var b strings.Builder b.WriteString("---\n\n**" + sanitize.InlineText(r.author.String()) + "**") for _, badge := range boardBadges(cs, r.author) { b.WriteString(" · " + badge) } b.WriteString(" · [#" + strconv.FormatUint(r.id, 10) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) + "/board/" + strconv.FormatUint(r.id, 10) + ")" + " · block " + strconv.FormatInt(r.at, 10) + "\n\n") if (r.hiddenByAuthor || r.hiddenByMod) && !r.global && !r.purged && listing { if r.hiddenByMod { b.WriteString("> _Removed from this listing by this court's moderators. " + "It still reads at its own link._\n\n") } else { b.WriteString("> _Withdrawn from this listing. It still reads at its own link._\n\n") } return b.String() } b.WriteString(boardTextVisible(r) + "\n\n") if r.parent == 0 { writeBoardReplies(&b, slug, claimID, cs, r.id) if boardOpen(cs) { b.WriteString("[Reply](" + boardPostLink(slug, claimID, r.id) + ")\n\n") } } return b.String() } func writeBoardReplies(b *strings.Builder, slug string, claimID uint64, cs *claimState, parent uint64) { pre := beClaimKey(parent) n, total := 0, boardReplyCount(cs, parent) if total == 0 { return } cs.boardKids.Iterate(pre, "", func(k string, v any) bool { if !strings.HasPrefix(k, pre) || !boardKeyMine(pre, k) { return true } r := v.(*boardRow) if boardMark(r) != "." { return false } b.WriteString("> **" + sanitize.InlineText(r.author.String()) + "**") for _, badge := range boardBadges(cs, r.author) { b.WriteString(" · " + badge) } b.WriteString(" · [#" + strconv.FormatUint(r.id, 10) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) + "/board/" + strconv.FormatUint(r.id, 10) + ")\n>\n") { for _, line := range strings.Split(boardTextVisible(r), "\n") { b.WriteString("> " + line + "\n") } b.WriteString("\n") } n++ return n >= maxBoardRepliesShown }) if total > n { b.WriteString("_" + strconv.Itoa(total-n) + " more repl" + plural(total-n) + " at [#" + strconv.FormatUint(parent, 10) + "](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug + "/" + strconv.FormatUint(claimID, 10) + "/board/" + strconv.FormatUint(parent, 10) + ")._\n\n") } } func plural(n int) string { if n == 1 { return "y" } return "ies" } func boardBadges(cs *claimState, who address) []string { var out []string if who == cs.author { out = append(out, "author") } if cs.frozenAt != 0 && who == cs.answerer { out = append(out, "answerer") } for _, side := range []int{sideYES, sideNO} { if v := cs.stakers.Get(posKey(who, side)); v != nil { if p := v.(*stakePos); p.stake > 0 { name := "YES" if side == sideNO { name = "NO" } out = append(out, name+" "+strconv.FormatInt(p.stake, 10)) } } } return out } func boardTextVisible(r *boardRow) string { if r.purged { return "> _Removed. The text is gone from the record; the row remains._" } if r.global { return "> _Withheld pending a legal determination._" } if r.text == "" { return "_(empty)_" } return sanitize.Block(r.text) } func boardScoreKey(score int64, rowID uint64) string { return beInv(score) + beInv(int64(rowID)) } func scoreIndexPut(cs *claimState, r *boardRow) { if r.parent != 0 || boardMark(r) != "." { return } r.scoreKey = boardScoreKey(r.score, r.id) ensureScoreIdx(cs).Set(r.scoreKey, r) } func scoreIndexDrop(cs *claimState, r *boardRow) { if cs.boardScore == nil || r.scoreKey == "" { return } cs.boardScore.Remove(r.scoreKey) r.scoreKey = "" } func UpvoteComment(cur realm, courtSlug string, claimID uint64, rowID uint64) int64 { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; it takes no new content") } cs := mustClaim(c, claimID) if !boardOpen(cs) { panic("kourtv2: this claim is settled; its board is closed") } mustBoardWritable(c, cs, who) r := mustBoardRow(cs, rowID) if r.parent != 0 { panic("kourtv2: only top-level comments are ranked; a reply is read under its parent") } if r.author == who { panic("kourtv2: you cannot upvote your own comment") } if r.purged { panic("kourtv2: that comment was destroyed on legal grounds; it takes no votes") } vk := beClaimKey(rowID) + string(who) if cs.boardVoted != nil && cs.boardVoted.Has(vk) { panic("kourtv2: you have already upvoted that comment") } var w int64 if sr := lookupStanding(c, who); sr != nil { w = sr.score } if w <= 0 { panic("kourtv2: an upvote is weighted by standing, and you have none here yet") } mustSpendPost(c, who) ensureBoard(cs) ensureVoted(cs).Set(vk, true) scoreIndexDrop(cs, r) r.score = satAdd(r.score, w) scoreIndexPut(cs, r) return r.score } func HasUpvoted(courtSlug string, claimID uint64, rowID uint64, who address) bool { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardVoted == nil { return false } return cs.boardVoted.Has(beClaimKey(rowID) + string(who)) } func CommentScore(courtSlug string, claimID uint64, rowID uint64) int64 { return mustBoardRow(mustClaim(mustCourt(courtSlug), claimID), rowID).score } func BoardTop(courtSlug string, claimID uint64, offset, count int) string { cs := mustClaim(mustCourt(courtSlug), claimID) if cs.boardScore == nil || count <= 0 { return "" } if count > maxBoardRepliesShown*8 { count = maxBoardRepliesShown * 8 } var b strings.Builder cs.boardScore.IterateByOffset(offset, count, func(_ string, v any) bool { r := v.(*boardRow) b.WriteString(strconv.FormatUint(r.id, 10) + "|" + r.author.String() + "|" + strconv.FormatInt(r.score, 10) + "|.|" + strconv.FormatInt(r.at, 10) + "|" + boardTextFor(r) + "\n") return false }) return b.String() } func writeBoardLink(b *strings.Builder, c *Court, cs *claimState, claimID uint64) { n := 0 if cs.board != nil { n = cs.board.Size() } link := "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + c.id + "/" + strconv.FormatUint(claimID, 10) + "/board" switch { case n == 0 && !boardOpen(cs): return case n == 0: b.WriteString("## Comments\n\n[Write the first one](" + boardPostLink(c.id, claimID, 0) + ")\n\n") return } b.WriteString("## Comments\n\n") b.WriteString("[Write a comment](" + boardPostLink(c.id, claimID, 0) + ")\n\n") shown, drawn := 0, 0 if cs.boardTop != nil { cs.boardTop.ReverseIterate("", "", func(_ string, v any) bool { r := v.(*boardRow) if boardMark(r) != "." { return false } b.WriteString(renderBoardOne(c.id, claimID, cs, r, true)) shown += 1 + boardReplyCount(cs, r.id) drawn++ return drawn >= claimBoardPreview }) } all := "" if shown < n { all = "all " } b.WriteString("\n[" + all + strconv.Itoa(n) + " comment" + plural2(n) + "](" + link + ") · [Top](" + link + "/top)\n\n") writeHiddenLink(b, c.id, claimID, "/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:"+c.id+"/"+strconv.FormatUint(claimID, 10)) } const claimBoardPreview = 3 func plural2(n int) string { if n == 1 { return "" } return "s" }
- #12boardlegal.gno
- #13package kourt import "strconv" func boardActCode(verb string, rowID uint64, categoryCode string) string { code := verb + "#" + strconv.FormatUint(rowID, 10) if categoryCode != "" { code += ":" + categoryCode } return code } const ( boardCodeSpam = "spam" boardCodeOffTopic = "off-topic" boardCodeAbuse = "abuse" boardCodeImpersonation = "impersonation" boardCodeIllegal = "illegal-referred" ) func mustBoardCode(code string) { switch code { case boardCodeSpam, boardCodeOffTopic, boardCodeAbuse, boardCodeImpersonation, boardCodeIllegal: return } panic("kourtv2: a board moderation code is one of: spam, off-topic, abuse, " + "impersonation, illegal-referred") } func BoardCodes() string { return boardCodeSpam + "," + boardCodeOffTopic + "," + boardCodeAbuse + "," + boardCodeImpersonation + "," + boardCodeIllegal } func GlobalHideRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() if !isGlobalMod(who) { panic("kourtv2: only a global DAO member may globally hide a comment") } checkReason(categoryCode) c := mustCourt(courtSlug) cs := mustClaim(c, claimID) r := mustBoardRow(cs, rowID) if r.purged { panic("kourtv2: that comment was already destroyed; there is nothing to withhold") } now := heightNow() if !r.global && r.globalClearedAt != 0 && reSetWindowOpen(r.globalClearedAtTime, r.globalClearedAt, now) { d := ensureGlobalDAO() fire, _ := approveAction(d.pending, "boardreset:"+c.id+":"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10), who, categoryCode, d.purgeM) if !fire { return } } r.global = true scoreIndexDrop(cs, r) ensureClaimMod(c, ensureMod(c), claimID). appendLog(who, boardActCode("board-global-hide", rowID, categoryCode), "") emitGlobalAct(c.id, claimID, "board-global-hide", who) } func GlobalClearRow(cur realm, courtSlug string, claimID, rowID uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() if !isGlobalMod(who) { panic("kourtv2: only a global DAO member may clear a global comment hide") } c := mustCourt(courtSlug) cs := mustClaim(c, claimID) r := mustBoardRow(cs, rowID) if !r.global { panic("kourtv2: that comment is not globally hidden") } r.global = false r.globalClearedAt = heightNow() r.globalClearedAtTime = nowTime() scoreIndexPut(cs, r) ensureClaimMod(c, ensureMod(c), claimID). appendLog(who, boardActCode("board-global-clear", rowID, ""), "") emitGlobalAct(c.id, claimID, "board-global-clear", who) } func PurgeBoardRow(cur realm, courtSlug string, claimID, rowID uint64, categoryCode string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() d := ensureGlobalDAO() if !d.members.Has(who.String()) { panic("kourtv2: only a global DAO member may purge") } mustCategoryCode(categoryCode) c := mustCourt(courtSlug) cs := mustClaim(c, claimID) r := mustBoardRow(cs, rowID) if r.purged { return } fire, votedCode := approveAction(d.pending, "boardpurge:"+c.id+":"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10), who, categoryCode, d.purgeM) if !fire { return } r.purged = true r.text = "" scoreIndexDrop(cs, r) ensureClaimMod(c, ensureMod(c), claimID). appendLog(who, boardActCode("board-purge", rowID, votedCode), "") emitPurge(c.id, claimID, "board-row", who) } func BoardRowState(courtSlug string, claimID uint64, rowID uint64) string { return boardMark(mustBoardRow(mustClaim(mustCourt(courtSlug), claimID), rowID)) } const maxBatchRows = 64 func PurgeBoardRange(cur realm, courtSlug string, claimID, fromRowID uint64, categoryCode string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() d := ensureGlobalDAO() if !d.members.Has(who.String()) { panic("kourtv2: only a global DAO member may purge") } mustCategoryCode(categoryCode) c := mustCourt(courtSlug) cs := mustClaim(c, claimID) cm := ensureMod(c) clm := lookupClaimMod(cm, claimID) if clm == nil || !clm.boardPurgeAll { fire, _ := approveAction(d.pending, "boardpurgeall:"+c.id+":"+strconv.FormatUint(claimID, 10), who, categoryCode, d.purgeM) if !fire { return fromRowID } clm = ensureClaimMod(c, cm, claimID) clm.boardPurgeAll = true clm.appendLog(who, boardActCode("board-purge-all", 0, categoryCode), "") emitPurge(c.id, claimID, "board-all", who) } if cs.board == nil { return 0 } n, next := 0, uint64(0) cs.board.Iterate(beClaimKey(fromRowID), "", func(_ string, v any) bool { r := v.(*boardRow) if r.purged { return false } if n >= maxBatchRows { next = r.id return true } r.purged = true r.text = "" scoreIndexDrop(cs, r) n++ return false }) return next } func BoardPurgeOrdered(courtSlug string, claimID uint64) bool { c := mustCourt(courtSlug) if c.mod == nil { return false } clm := lookupClaimMod(c.mod, claimID) return clm != nil && clm.boardPurgeAll }
- #14boardmod.gno
- #15package kourt import "strconv" const ( freezeMaxBlocks = int64(17_280) / 2 freezeGapBlocks = freezeMaxBlocks ) func mustBoardModInvariants() { if freezeMaxBlocks >= 17_280 { panic("kourtv2: a board freeze must expire inside the nomination window") } if freezeGapBlocks < freezeMaxBlocks { panic("kourtv2: the gap between freezes must be at least one freeze long, " + "or the ceiling is a formality") } } func init() { mustBoardModInvariants() } func speechM(cm *courtMod) int { m := cm.m if m < 2 { m = 2 } if m > cm.n { m = cm.n } return m } func SpeechThreshold(courtSlug string) int { c := mustCourt(courtSlug) if c.mod == nil { return 0 } return speechM(c.mod) } func boardSanctionEpochOf(c *Court) int64 { if c.mod == nil { return 0 } return c.mod.boardSanctionEpoch } func boardFreezeActive(r *standingRow) bool { passed, known := pastDeadline(r.frozenUntilTime, 0) return (known && !passed) || (!known && heightNow() < r.frozenUntil) } func boardFreezeGapOpen(r *standingRow) bool { passed, known := pastDeadline(r.frozenGapUntilTime, 0) return (known && !passed) || (!known && heightNow() < r.frozenGapUntil) } func boardFrozen(c *Court, who address) bool { r := lookupStanding(c, who) if r == nil || r.frozenUntil == 0 { return false } if r.frozenEpoch != boardSanctionEpochOf(c) { return false } return boardFreezeActive(r) } func boardFrozenAt(c *Court, who address) (at, atHeight int64) { if !boardFrozen(c, who) { return 0, 0 } r := lookupStanding(c, who) return r.frozenUntilTime, r.frozenUntil } func BoardFrozenUntil(courtSlug string, who address) (at, atHeight int64) { return boardFrozenAt(mustCourt(courtSlug), who) } func claimFreezeActive(clm *claimMod) bool { passed, known := pastDeadline(clm.boardFrozenUntilTime, 0) return (known && !passed) || (!known && heightNow() < clm.boardFrozenUntil) } func claimFreezeGapOpen(clm *claimMod) bool { passed, known := pastDeadline(clm.boardFrozenGapUntilTime, 0) return (known && !passed) || (!known && heightNow() < clm.boardFrozenGapUntil) } func claimBoardFrozen(c *Court, claimID uint64) bool { if c.mod == nil { return false } clm := lookupClaimMod(c.mod, claimID) if clm == nil || clm.boardFrozenUntil == 0 { return false } if clm.boardFrozenEpoch != c.mod.boardSanctionEpoch { return false } return claimFreezeActive(clm) } func claimBoardFrozenAt(c *Court, claimID uint64) (at, atHeight int64) { if !claimBoardFrozen(c, claimID) { return 0, 0 } clm := lookupClaimMod(c.mod, claimID) return clm.boardFrozenUntilTime, clm.boardFrozenUntil } func ClaimBoardFrozenUntil(courtSlug string, claimID uint64) (at, atHeight int64) { return claimBoardFrozenAt(mustCourt(courtSlug), claimID) } func HideBoardRow(cur realm, courtSlug string, claimID, rowID uint64, code string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireActiveMod(cm, who) mustBoardCode(code) cs := mustClaim(c, claimID) r := mustBoardRow(cs, rowID) if r.hiddenByMod { panic("kourtv2: that comment is already hidden") } fire, _ := approveAction(cm.pending, "boardhide:"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10), who, code, speechM(cm)) if !fire { return } r.hiddenByMod = true scoreIndexDrop(cs, r) ensureClaimMod(c, cm, claimID). appendLog(who, boardActCode("board-hide", rowID, code), "") emitModAct(c.id, claimID, "board-hide", who) } func UnhideBoardRow(cur realm, courtSlug string, claimID, rowID uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireMod(cm, who) cs := mustClaim(c, claimID) r := mustBoardRow(cs, rowID) if !r.hiddenByMod { panic("kourtv2: that comment is not hidden") } fire, _ := approveAction(cm.pending, "boardunhide:"+strconv.FormatUint(claimID, 10)+":"+strconv.FormatUint(rowID, 10), who, "", speechM(cm)) if !fire { return } r.hiddenByMod = false scoreIndexPut(cs, r) ensureClaimMod(c, cm, claimID). appendLog(who, boardActCode("board-unhide", rowID, ""), "") emitModAct(c.id, claimID, "board-unhide", who) } func SlashStanding(cur realm, courtSlug string, who address, levels int64, code string) { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() if levels < 1 || levels > 3 { panic("kourtv2: a slash is 1, 2 or 3 whole levels") } c := mustCourt(courtSlug) cm := ensureMod(c) requireActiveMod(cm, actor) mustBoardCode(code) if who == "" { panic("kourtv2: no such address") } fire, voted := approveAction(cm.pending, "slash:"+who.String()+":"+strconv.FormatInt(levels, 10), actor, code, speechM(cm)) if !fire { return } r := getStanding(c, who) target := postLevel(c, who) - levels if target < 0 { target = 0 } if want := levelFloor(c, target); r.score > want { r.score = want } if target == 0 { r.passHeld = false } cm.appendCourtLog(actor, "slash:"+strconv.FormatInt(levels, 10), voted) emitModAct(c.id, 0, "slash", actor) } func levelFloor(c *Court, lv int64) int64 { if lv <= 0 { return 0 } l := ladderFor(c) sup, _ := boardSupply(c) switch lv { case 1: return sup * l.t1Bps / 10_000 case 2: return sup * l.t2Bps / 10_000 } return sup * l.t3Bps / 10_000 } func RestoreStanding(cur realm, courtSlug string, who address) int64 { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireMod(cm, actor) r := lookupStanding(c, who) if r == nil || r.score >= r.highWater { panic("kourtv2: that address has nothing to restore") } fire, _ := approveAction(cm.pending, "restore:"+who.String(), actor, "", speechM(cm)) if !fire { return 0 } r.score = r.highWater cm.appendCourtLog(actor, "restore", "") emitModAct(c.id, 0, "restore", actor) return r.score } func FreezeBoard(cur realm, courtSlug string, who address, blocks int64, code string) { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() if blocks < 1 || blocks > freezeMaxBlocks { panic("kourtv2: a board freeze is at most freezeMaxBlocks long") } c := mustCourt(courtSlug) cm := ensureMod(c) requireActiveMod(cm, actor) mustBoardCode(code) if who == "" { panic("kourtv2: no such address") } now := heightNow() if r := lookupStanding(c, who); r != nil && boardFreezeGapOpen(r) { panic("kourtv2: this address was frozen too recently; the ceiling is not a formality") } fire, voted := approveAction(cm.pending, "freeze:"+who.String(), actor, code, speechM(cm)) if !fire { return } r := getStanding(c, who) r.frozenUntil = now + blocks r.frozenUntilTime = nowTime() + blocksToSecs(blocks) r.frozenEpoch = boardSanctionEpochOf(c) r.frozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks r.frozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks) cm.appendCourtLog(actor, "freeze:"+strconv.FormatInt(blocks, 10), voted) emitModAct(c.id, 0, "freeze", actor) } func UnfreezeBoard(cur realm, courtSlug string, who address) { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireMod(cm, actor) if !boardFrozen(c, who) { panic("kourtv2: that address is not frozen") } fire, _ := approveAction(cm.pending, "unfreeze:"+who.String(), actor, "", speechM(cm)) if !fire { return } lifted := lookupStanding(c, who) lifted.frozenUntil = 0 lifted.frozenUntilTime = 0 cm.appendCourtLog(actor, "unfreeze", "") emitModAct(c.id, 0, "unfreeze", actor) } func FreezeClaimBoard(cur realm, courtSlug string, claimID uint64, blocks int64, code string) { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() if blocks < 1 || blocks > freezeMaxBlocks { panic("kourtv2: a board freeze is at most freezeMaxBlocks long") } c := mustCourt(courtSlug) cm := ensureMod(c) requireActiveMod(cm, actor) mustBoardCode(code) _ = mustClaim(c, claimID) now := heightNow() if prev := lookupClaimMod(cm, claimID); prev != nil && claimFreezeGapOpen(prev) { panic("kourtv2: this board was frozen too recently; the ceiling is not a formality") } fire, _ := approveAction(cm.pending, "freezeboard:"+strconv.FormatUint(claimID, 10), actor, code, speechM(cm)) if !fire { return } clm := ensureClaimMod(c, cm, claimID) clm.boardFrozenUntil = now + blocks clm.boardFrozenUntilTime = nowTime() + blocksToSecs(blocks) clm.boardFrozenEpoch = cm.boardSanctionEpoch clm.boardFrozenGapUntil = now + freezeMaxBlocks + freezeGapBlocks clm.boardFrozenGapUntilTime = nowTime() + blocksToSecs(freezeMaxBlocks+freezeGapBlocks) clm.appendLog(actor, "freezeboard:"+strconv.FormatInt(blocks, 10)+":"+code, "") emitModAct(c.id, claimID, "freezeboard", actor) } func ThawClaimBoard(cur realm, courtSlug string, claimID uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() c := mustCourt(courtSlug) cm := ensureMod(c) requireMod(cm, actor) if !claimBoardFrozen(c, claimID) { panic("kourtv2: that board is not frozen") } fire, _ := approveAction(cm.pending, "thawboard:"+strconv.FormatUint(claimID, 10), actor, "", speechM(cm)) if !fire { return } thawed := lookupClaimMod(cm, claimID) thawed.boardFrozenUntil = 0 thawed.boardFrozenUntilTime = 0 ensureClaimMod(c, cm, claimID).appendLog(actor, "thawboard", "") emitModAct(c.id, claimID, "thawboard", actor) } func GlobalClearBoardBits(cur realm, courtSlug string) int64 { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() if !isGlobalMod(who) { panic("kourtv2: only a global DAO member may clear board bits (recovery)") } c := mustCourt(courtSlug) cm := ensureMod(c) cm.boardSanctionEpoch++ cm.appendCourtLog(who, "global-clear-board-bits", "") emitGlobalAct(c.id, 0, "clear-board-bits", who) return cm.boardSanctionEpoch } func GlobalRestoreStanding(cur realm, courtSlug string, who address) int64 { if !cur.IsCurrent() { panic(errStaleRealm) } actor := cur.Previous().Address() if !isGlobalMod(actor) { panic("kourtv2: only a global DAO member may restore standing (recovery)") } c := mustCourt(courtSlug) r := lookupStanding(c, who) if r == nil || r.score >= r.highWater { panic("kourtv2: that address has nothing to restore") } r.score = r.highWater ensureMod(c).appendCourtLog(actor, "global-restore-standing", "") emitGlobalAct(c.id, 0, "restore-standing", actor) return r.score } func mustBoardWritable(c *Court, cs *claimState, who address) { if claimBoardFrozen(c, cs.id) { panic("kourtv2: this claim's board is frozen by moderators") } if boardFrozen(c, who) { panic("kourtv2: you are frozen out of this court's boards") } } func PurgeCourtLogRow(cur realm, courtSlug string, rowID uint64, categoryCode string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() d := ensureGlobalDAO() if !d.members.Has(who.String()) { panic("kourtv2: only a global DAO member may purge") } mustCategoryCode(categoryCode) c := mustCourt(courtSlug) cm := ensureMod(c) idx := -1 for i := range cm.log { if cm.log[i].rowID == rowID { idx = i break } } if idx < 0 { panic("kourtv2: no such court log row") } if cm.log[idx].purged { return } fire, code := approveAction(d.pending, "purgecourtrow:"+c.id+":"+strconv.FormatUint(rowID, 10), who, categoryCode, d.purgeM) if !fire { return } cm.log[idx].purged = true cm.log[idx].reason = "" cm.appendCourtLog(who, "purgecourtrow:"+code, "") emitPurge(c.id, 0, code, who) } func GlobalClearCourtParams(cur realm, courtSlug string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() if !isGlobalMod(who) { panic("kourtv2: only a global DAO member may clear court parameters (recovery)") } c := mustCourt(courtSlug) if c.ladder == nil && c.creditRates == nil { panic("kourtv2: this court has no parameter overrides") } c.ladder, c.creditRates = nil, nil ensureMod(c).appendCourtLog(who, "global-clear-params", "") emitGlobalAct(c.id, 0, "clear-court-params", who) }
- #16buy.gno
- #17package kourt import ( "chain" "chain/banker" "chain/runtime/unsafe" ) const gnotDenom = "ugnot" const burnSinkPath = "gno.land/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:burned" func Buy(cur realm, slug string) int64 { if !cur.IsCurrent() { panic(errStaleRealm) } prev := cur.Previous() if !prev.IsUserCall() { panic("kourtv2: a buy must be a direct user call") } buyer := prev.Address() c := mustCourt(slug) if buyer == cur.Address() { panic("kourtv2: the court's own address may not buy") } touch(c) sent := unsafe.OriginSend().AmountOf(gnotDenom) if sent <= 0 { panic("kourtv2: send GNOT to buy the coin") } delta, spent := c.crv.Minted(c.minted, sent) if delta <= 0 { panic("kourtv2: payment too small to mint a coin") } c.minted += delta recordPrice(c) c.coin.Mint(buyer, delta) reindexBurn(c, spent) accrueFranchise(c, buyer, spent) b := banker.NewBanker(banker.BankerTypeRealmSend, cur) b.SendCoins(cur.Address(), chain.PackageAddress(burnSinkPath), chain.Coins{chain.NewCoin(gnotDenom, spent)}) if r := sent - spent; r > 0 { b.SendCoins(cur.Address(), buyer, chain.Coins{chain.NewCoin(gnotDenom, r)}) } return delta } func CurvePosition(slug string) int64 { return mustCourt(slug).minted } func BurnSink() address { return chain.PackageAddress(burnSinkPath) } func BurnedGNOT() int64 { b := banker.NewReadonlyBanker() return b.GetCoins(chain.PackageAddress(burnSinkPath)).AmountOf(gnotDenom) } func CoinBalanceOf(slug string, who address) int64 { return mustCourt(slug).coin.BalanceOf(who) }
- #18claim.gno
- #19package kourt import ( "chain" "strconv" "strings" checkpoint "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/checkpoint/v0" twap "gno.land/p/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/twap/v0" bptree "gno.land/p/nt/bptree/v0" ) const ( sideYES = 0 sideNO = 1 ) const twapBuckets = 168 const maxClaimBodyLen = 2000 func runeLen(s string) int { n := 0 for range s { n++ } return n } type claimState struct { id uint64 title string body string media []mediaItem author address deposit int64 fee int64 seeded bool noAccuracy bool openedAt int64 openedAtTime int64 answeredAtTime int64 verdictAtTime int64 escrowUntilAt int64 closed bool board *bptree.BPTree boardTop *bptree.BPTree boardKids *bptree.BPTree boardWrote *bptree.BPTree boardScore *bptree.BPTree boardVoted *bptree.BPTree boardNextID uint64 inStrip bool inPending bool yesStake, noStake int64 stakers *bptree.BPTree yesConvHi, yesConvLo uint64 noConvHi, noConvLo uint64 yesRawHi, yesRawLo uint64 noRawHi, noRawLo uint64 yesLastAcc, noLastAcc int64 yesLastRawAt, noLastRawAt int64 oi twap.Ring yes twap.Ring yesH, noH checkpoint.Series yesD, noD checkpoint.Series histTrimmedTo uint32 frozenAt int64 rateAccAtFreeze int64 xBarFrozen int64 yesStakeAtFreeze, noStakeAtFreeze int64 answer int8 answerer address answerBond int64 answerBond0 int64 answerHeight int64 verdictAt int64 route string tierRef int64 disputeOpen bool disputeOpenedTime int64 round int64 failedRounds int64 decidedRounds int64 disputer address disputeBond int64 overturnBy address overturnBond int64 proposalID int64 provisional int8 escrowUntil int64 provClose bool credEligible bool voted *bptree.BPTree rewardsOpened bool drawWinners int64 drawAuthor int64 drawAnswerer int64 winPoolConvCC int64 openBlocks int64 carrotTotal int64 carrotPool int64 carrotChoice string carrotDenom int64 decidedPID int64 spamW int64 spamTotalW int64 spamClosed bool authorPaid bool answererPaid bool } func OpenClaim(cur realm, courtSlug, title string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; no new claims may be opened") } return openClaim(c, cur.Previous().Address(), title, "", false) } func OpenClaimIn(cur realm, courtSlug string, folderID uint64, title, body string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; no new claims may be opened") } cm := ensureMod(c) f := cm.mustFolder(folderID) if f.retired || f.purged { panic("kourtv2: that set is retired; a claim filed there would be unreachable") } who := cur.Previous().Address() id := openClaim(c, who, title, body, false) fileInto(f, id) emitModAct(c.id, id, "folder-add", who) return id } func OpenClaimP(cur realm, courtSlug, title, body string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; no new claims may be opened") } return openClaim(c, cur.Previous().Address(), title, body, false) } func OpenClaimSeeded(cur realm, courtSlug, title string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; no new claims may be opened") } cm := ensureMod(c) requireActiveMod(cm, who) if cm.installedByMeta { panic("kourtv2: a meta-installed moderator set may not seed claims; seed after a local election") } return openClaim(c, who, title, "", true) } func OpenClaimPM(cur realm, courtSlug, title, body, media string) uint64 { if !cur.IsCurrent() { panic(errStaleRealm) } c := mustCourt(courtSlug) if courtIsPurged(c) { panic("kourtv2: this court is purged; no new claims may be opened") } items := parseMediaArg(media) id := openClaim(c, cur.Previous().Address(), title, body, false) if len(items) > 0 { mustClaim(c, id).media = items } return id } func openClaim(c *Court, who address, title, body string, seeded bool) uint64 { if runeLen(title) == 0 || runeLen(title) > 200 { panic("kourtv2: a claim title is 1..200 characters") } if runeLen(body) > maxClaimBodyLen { panic("kourtv2: a claim body is at most " + strconv.Itoa(maxClaimBodyLen) + " characters") } var dep, fee int64 if !seeded { dep = effMinDeposit(c) fee = dep * depositFeeBps / 10000 mustSpendable(c, who, dep+fee) c.coin.Transfer(who, c.escrow, dep+fee) c.deposit += dep + fee } c.nextID++ id := c.nextID cs := &claimState{ id: id, title: title, body: body, author: who, deposit: dep, fee: fee, seeded: seeded, openedAt: heightNow(), openedAtTime: nowTime(), stakers: bptree.NewBPTree32(), oi: twap.New(epochBlocks, twapBuckets, 8), yes: twap.New(epochBlocks, twapBuckets, 8), provisional: -1, } c.claims.Set(beClaimKey(id), cs) if seeded { pendingEnter(c, cs) } onClaimOpened(c, cs) chain.Emit("ClaimOpened", "court", c.id, "claim", strconv.FormatUint(id, 10), "author", who.String(), "seeded", strconv.FormatBool(seeded), ) return id } func EditClaimTitle(cur realm, courtSlug string, claimID uint64, newTitle string) { if !cur.IsCurrent() { panic(errStaleRealm) } who := cur.Previous().Address() c := mustCourt(courtSlug) cs := mustClaim(c, claimID) if who != cs.author { panic("kourtv2: only the author may edit the title") } if len(newTitle) == 0 || len(newTitle) > 200 { panic("kourtv2: a claim title is 1..200 characters") } if cs.closed || cs.frozenAt != 0 { panic("kourtv2: the claim is no longer editable") } if passed, known := pastDeadline(cs.openedAtTime, blocksToSecs(c.params.stakeOpenDelayBlocks)); (known && passed) || (!known && heightNow() >= cs.openedAt+c.params.stakeOpenDelayBlocks) { panic("kourtv2: the polish window has closed; the title is frozen") } if cs.yesStake != 0 || cs.noStake != 0 { panic("kourtv2: staking has begun; the title is frozen") } cs.title = newTitle onTitleEdited(c, cs) chain.Emit("ClaimTitleEdited", "court", c.id, "claim", strconv.FormatUint(claimID, 10)) } func CloseDeadClaim(cur realm, courtSlug string, claimID uint64) { if !cur.IsCurrent() { panic(errStaleRealm) } c := mustCourt(courtSlug) cs := mustClaim(c, claimID) if cs.closed { panic("kourtv2: already closed") } if cs.frozenAt != 0 { panic("kourtv2: this claim has an answer; it settles, it does not die") } if passed, known := pastDeadline(cs.openedAtTime, deadClaimSecs); (known && !passed) || (!known && heightNow() < cs.openedAt+deadClaimTimeout) { panic("kourtv2: the dead-claim timeout has not passed") } cs.closed = true pendingExit(c, cs) stripExit(c, cs) releaseMetaLatchIfMeta(c, cs) if cs.deposit > 0 { c.coin.Transfer(c.escrow, cs.author, cs.deposit) c.deposit -= cs.deposit cs.deposit = 0 } if cs.fee > 0 { c.coin.Burn(c.escrow, cs.fee) c.deposit -= cs.fee cs.fee = 0 } } func ClaimCount(courtSlug string) uint64 { return mustCourt(courtSlug).nextID } func ClaimTitle(courtSlug string, claimID uint64) string { c := mustCourt(courtSlug) return claimTitleFor(c, mustClaim(c, claimID)) } func ClaimBody(courtSlug string, claimID uint64) string { c := mustCourt(courtSlug) return claimBodyFor(c, mustClaim(c, claimID)) } func ClaimMedia(courtSlug string, claimID uint64) string { c := mustCourt(courtSlug) return encodeMedia(claimMediaVisible(c, mustClaim(c, claimID))) } func ClaimAuthor(courtSlug string, claimID uint64) address { return mustClaim(mustCourt(courtSlug), claimID).author } func ClaimClosed(courtSlug string, claimID uint64) bool { return mustClaim(mustCourt(courtSlug), claimID).closed } func ClaimSeeded(courtSlug string, claimID uint64) bool { return mustClaim(mustCourt(courtSlug), claimID).seeded } func StakePools(courtSlug string, claimID uint64) (yes, no int64) { cs := mustClaim(mustCourt(courtSlug), claimID) return cs.yesStake, cs.noStake } func TrailingOI(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) { cs := mustClaim(mustCourt(courtSlug), claimID) return cs.oi.Average(heightNow(), window) } func PoolConviction(courtSlug string, claimID uint64) (yes, no int64) { cs := mustClaim(mustCourt(courtSlug), claimID) return convToCC(cs.yesConvHi, cs.yesConvLo), convToCC(cs.noConvHi, cs.noConvLo) } func TrailingYes(courtSlug string, claimID uint64, window int64) (avg int64, mature bool) { cs := mustClaim(mustCourt(courtSlug), claimID) return cs.yes.Average(heightNow(), window) } func DocketSeries(courtSlug string, ids string, window int64) string { c := mustCourt(courtSlug) now := heightNow() out := strings.Builder{} sent := 0 for _, part := range strings.Split(ids, ",") { if sent >= seriesPageMax { break } part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.ParseUint(part, 10, 64) if err != nil { continue } v := c.claims.Get(beClaimKey(id)) if v == nil { continue } cs := v.(*claimState) oi, oiMature := cs.oi.Average(now, window) yes, yesMature := cs.yes.Average(now, window) if sent > 0 { out.WriteString(";") } out.WriteString(strconv.FormatUint(id, 10)) out.WriteString(":" + strconv.FormatInt(cs.yesStake, 10)) out.WriteString(":" + strconv.FormatInt(cs.noStake, 10)) out.WriteString(":" + strconv.FormatInt(convToCC(cs.yesConvHi, cs.yesConvLo), 10)) out.WriteString(":" + strconv.FormatInt(convToCC(cs.noConvHi, cs.noConvLo), 10)) out.WriteString(":" + strconv.FormatInt(oi, 10)) out.WriteString(":" + boolDigit(oiMature)) out.WriteString(":" + strconv.FormatInt(yes, 10)) out.WriteString(":" + boolDigit(yesMature)) sent++ } return out.String() } const seriesPageMax = 60 func boolDigit(b bool) string { if b { return "1" } return "0" } func mustClaim(c *Court, id uint64) *claimState { v := c.claims.Get(beClaimKey(id)) if v == nil { panic("kourtv2: no such claim") } return v.(*claimState) } func beClaimKey(id uint64) string { b := [8]byte{ byte(id >> 56), byte(id >> 48), byte(id >> 40), byte(id >> 32), byte(id >> 24), byte(id >> 16), byte(id >> 8), byte(id), } return string(b[:]) }
- #20claimhead.gno
- #21package kourt import ( "strconv" "strings" ) func ClaimHead(courtSlug string, claimID uint64, window int64) string { c := mustCourt(courtSlug) mustClaim(c, claimID) b := strings.Builder{} n := func(v int64) { b.WriteString(strconv.FormatInt(v, 10) + "\t") } i := func(v int) { b.WriteString(strconv.Itoa(v) + "\t") } f := func(v bool) { b.WriteString(boolDigit(v) + "\t") } t := func(v string) { b.WriteString(v + "\t") } yes, no := StakePools(courtSlug, claimID) cy, cn := PoolConviction(courtSlug, claimID) oi, oiM := TrailingOI(courtSlug, claimID, window) ty, tyM := TrailingYes(courtSlug, claimID, window) n(yes) n(no) n(cy) n(cn) n(oi) f(oiM) n(ty) f(tyM) answered := HasAnswer(courtSlug, claimID) settled := Settled(courtSlug, claimID) disputeOpen := DisputeOpen(courtSlug, claimID) provClose := ProvClose(courtSlug, claimID) f(answered) f(disputeOpen) f(settled) f(provClose) i(Provisional(courtSlug, claimID)) f(ClaimClosed(courtSlug, claimID)) if answered { who := Answerer(courtSlug, claimID) i(AnswerVerdict(courtSlug, claimID)) n(AnswerBond(courtSlug, claimID)) n(SettleDeadline(courtSlug, claimID)) t(who.String()) i(AnswerRecord(courtSlug, who)) } else { i(-1) n(0) n(0) t("") i(0) } if answered && !settled { n(FailedRounds(courtSlug, claimID)) n(DisputeBondNext(courtSlug, claimID)) n(EscrowUntil(courtSlug, claimID)) if disputeOpen { n(QuorumFloorOf(courtSlug, claimID)) n(DisputeVoteCloses(courtSlug, claimID)) } else { n(0) n(0) } } else { n(0) n(0) n(0) n(0) n(0) } if settled && !provClose { i(Verdict(courtSlug, claimID)) t(VerdictRoute(courtSlug, claimID)) } else { i(-1) t("") } cryst := RewardsOpened(courtSlug, claimID) f(cryst) if cryst { w, au, an, ca := DrawSlices(courtSlug, claimID) n(w) n(au) n(an) n(ca) } else { n(0) n(0) n(0) n(0) } t(ClaimAuthor(courtSlug, claimID).String()) f(HiddenFromListing(courtSlug, claimID)) f(TextRedacted(courtSlug, claimID)) f(ClaimPurged(courtSlug, claimID)) f(ClaimSeeded(courtSlug, claimID)) n(CourtStakeOpenDelay(courtSlug)) i(BoardSize(courtSlug, claimID)) f(BoardOpen(courtSlug, claimID)) return strings.TrimSuffix(b.String(), "\t") } const claimHeadFields = 39
- #22clock.gno
- #23package kourt import ( "chain/runtime" "strconv" ) const ( settleSecs = int64(72 * 3600) deadClaimSecs = int64(12 * 7 * 86400) finalizeGraceSecs = int64(7 * 86400) priorityWindowSecs = int64(24 * 3600) ) func nowTime() int64 { return tcNow() } func deadlineTime(stamp, secs int64) int64 { if stamp == 0 { return 0 } return stamp + secs } func pastDeadline(stamp, secs int64) (passed, known bool) { if stamp == 0 { return false, false } return nowTime() >= stamp+secs, true } func blocksToSecs(b int64) int64 { return b * 5 } func heightNow() int64 { var h int64 if tcArmed { h = tcHeightBase + tcHeightSkew } else { h = runtime.ChainHeight() if h < tcHeightFloor { h = tcHeightFloor } } return h } func itoa(v int64) string { return strconv.FormatInt(v, 10) } func ClaimTimeline(courtSlug string, claimID uint64) string { c := mustCourt(courtSlug) cs := mustClaim(c, claimID) out := "opened:" + itoa(cs.openedAtTime) + ":" + itoa(cs.openedAt) if cs.answerHeight != 0 { out += ";answered:" + itoa(cs.answeredAtTime) + ":" + itoa(cs.answerHeight) if cs.verdictAt == 0 && !cs.closed { out += ";settle:" + itoa(deadlineTime(cs.answeredAtTime, settleSecs)) + ":" + itoa(cs.answerHeight+settleDelay) } } if cs.verdictAt != 0 { out += ";verdict:" + itoa(cs.verdictAtTime) + ":" + itoa(cs.verdictAt) } if cs.escrowUntil != 0 { out += ";reopen:" + itoa(cs.escrowUntilAt) + ":" + itoa(cs.escrowUntil) } if cs.disputeOpen && cs.proposalID != 0 { opened, closes, _, _ := c.gov.Timings(cs.proposalID) out += ";dispute:" + itoa(cs.disputeOpenedTime) + ":" + itoa(opened) _, closesTime := c.gov.TimingsAt(cs.proposalID) out += ";voteclose:" + itoa(closesTime) + ":" + itoa(closes) } out += ";now:" + itoa(nowTime()) + ":" + itoa(heightNow()) if TestClockFabricated() { out += ";testclock:" + itoa(TestClockPeakSkew()) + ":" + itoa(TestHeightPeakSkew()) } return out } type realmClock struct{} func (realmClock) Height() int64 { return heightNow() } func (realmClock) Now() int64 { return nowTime() } func eventHeight() string { h := strconv.FormatInt(heightNow(), 10) if TestClockFabricated() { return h + "~test" } return h }
- #24court.gno
Result log
msg:0,success:true,log:,events:[]