1// Package threads is a discussion block any realm can embed, keyed on the page2// it appears on rather than on the realm that stores it.3//4// It is the text half of gno.land/r/moul/reactions: the same embed shape, the5// same page keys, two lines in the host realm.6//7// import "gno.land/r/moul/x/social/threads/v0"8//9// func Render(path string) string {10// return body + threads.RenderBlock()11// }12//13// [RenderBlock] takes no argument because it does not need one: a plain read14// exported by a realm and called by another opens no realm frame, so15// unsafe.CurrentRealm() inside it reports the CALLER. The host realm's own16// path is the page key, and nothing has to be configured or registered.17// [RenderBlockFor] is the explicit form, for a realm that renders more than18// one page or already holds its path as a constant.19//20// # The token, and the three things it has to answer21//22// THREAD exists because placement is the one thing a discussion has that23// people want and cannot all have at once.24//25// mint 1 THREAD to a thread's author, the first time each distinct26// address replies to it. People, not messages, and never yourself.27// sink [Boost] burns PinCost THREAD to pin a thread to the top of its28// page for PinBlocks blocks. That is the only thing that removes29// supply, and the only thing the token does.30// buyer anyone who wants placement and has not earned it. They have to31// acquire THREAD from someone who did, with [Transfer], which is32// what makes attention a market rather than a scoreboard.33//34// It is farmable, and saying so is cheaper than pretending otherwise: two35// addresses replying to each other mint one THREAD each per thread. The fix is36// a gate on who counts as a replier, which belongs in a vouching realm and not37// here; until then the cost of farming is one transaction per point and the38// benefit is capped by what a pin is worth.39package threads4041import (42 "chain"43 "chain/runtime"44 "chain/runtime/unsafe"45 "strconv"4647 "gno.land/p/moul/kit/store/v0"48 sc "gno.land/p/moul/x/social/coin/v0"49 th "gno.land/p/moul/x/social/threads/v0"50 "gno.land/r/nt/grc20reg/v0"51)5253// realmPath is this realm's own path, the one its gnomod.toml module line54// declares. It is written out rather than read from unsafe.CurrentRealm(),55// which reports the CALLER in every plain read this realm exports and would56// therefore build every link against whichever realm embedded the block.57const realmPath = "gno.land/r/moul/x/social/threads/v0"5859const (60 // EarnPerReplier is minted to a thread's author the first time each61 // distinct address replies to it.62 EarnPerReplier = int64(1)6364 // PinCost is burned by Boost, and PinBlocks is how long the pin lasts.65 // A pin extends from whichever is later, now or the current pin, so66 // buying one never shortens somebody else's.67 PinCost = int64(10)68 PinBlocks = int64(1000)6970 // BlockThreads is how many threads an embedded block shows before it71 // turns into a link to the full page.72 BlockThreads = 573)7475var (76 // board holds every thread. A redeploy would wipe it, which is why this77 // realm is not private (see gnomod.toml).78 board = th.NewBoard()7980 // points is the THREAD ledger. The Coin is unexported on purpose: it81 // carries unrestricted mint and burn, and an exported pointer to it82 // would hand both to any importer.83 points *sc.Coin8485 // tokenKey is the r/nt/grc20reg lookup key, which is how a realm that86 // does not import this one refers to THREAD.87 tokenKey string88)8990// tokenPolicy is the three declarations THREAD is issued under. It is a var91// and not three literals inside init so that the tests, which rebuild the92// ledger between cases, issue the same token the chain holds.93var tokenPolicy = sc.Policy{94 Mint: "1 to a thread's author per distinct address that replies to it",95 Sink: "burned by Boost to pin a thread to the top of its page",96 Buyer: "anyone who wants placement and has not earned it",97}9899func init(cur realm) {100 points = sc.New("Thread Points", "THREAD", 0, 0, tokenPolicy, 0, cur)101 tokenKey = grc20reg.Register(cross(cur), points.Token(), "thread")102}103104// Post opens a thread under page.105//106// page is a package path, the full one: "gno.land/r/moul/home". It does not107// have to be a realm that embeds the block, and nothing checks that it exists.108// A page is just a name, which is what lets a realm key discussion on109// something finer than itself.110func Post(cur realm, page, body string) int64 {111 who := caller(cur)112 id, err := board.Post(page, who, body, runtime.ChainHeight())113 if err != nil {114 panic(err.Error())115 }116 chain.Emit("Post", "id", id.String(), "page", page, "author", who.String())117 return int64(id)118}119120// Reply appends to a thread.121//122// The first time each distinct address replies to a given thread, its author123// earns EarnPerReplier. Replying again earns nothing, and replying to your own124// thread never earns anything.125func Reply(cur realm, threadID int64, body string) {126 who := caller(cur)127 id := store.ID(threadID)128 isNew, err := board.Reply(id, who, body, runtime.ChainHeight())129 if err != nil {130 panic(err.Error())131 }132 chain.Emit("Reply", "id", id.String(), "author", who.String())133134 if isNew {135 t, _ := board.Get(id)136 points.Earn(t.Author, EarnPerReplier)137 chain.Emit("Earn", "id", id.String(), "author", t.Author.String())138 }139}140141// Boost burns PinCost THREAD from the caller and pins the thread to the top of142// its page for PinBlocks blocks.143//144// Anyone may boost anyone's thread. The pin is on the thread and not on the145// buyer, so the thing being sold is placement and not authorship.146func Boost(cur realm, threadID int64) {147 who := caller(cur)148 id := store.ID(threadID)149 t, ok := board.Get(id)150 if !ok {151 panic(th.ErrNoThread.Error())152 }153154 now := runtime.ChainHeight()155 from := now156 if t.PinnedUntil > from {157 from = t.PinnedUntil158 }159 // Charge first, pin second. The chain would revert both on an abort, so160 // the order is invisible on mainnet and load-bearing everywhere else: a161 // test, a filetest and any future caller that recovers see a refused162 // boost that pinned nothing.163 points.Spend(who, PinCost)164 if err := board.Pin(id, from+PinBlocks); err != nil {165 panic(err.Error())166 }167 chain.Emit("Boost", "id", id.String(), "by", who.String(),168 "until", strconv.FormatInt(from+PinBlocks, 10))169}170171// Transfer moves the caller's own THREAD. It is what makes the sink a market:172// somebody who wants a pin and has written nothing has to buy from an author.173func Transfer(cur realm, to address, amount int64) {174 if err := points.CallerTeller().Transfer(0, cur, to, amount); err != nil {175 panic(err.Error())176 }177}178179// Approve lets spender draw amount of the caller's THREAD.180func Approve(cur realm, spender address, amount int64) {181 if err := points.CallerTeller().Approve(0, cur, spender, amount); err != nil {182 panic(err.Error())183 }184}185186// TransferFrom spends an allowance the caller was granted.187func TransferFrom(cur realm, from, to address, amount int64) {188 if err := points.CallerTeller().TransferFrom(0, cur, from, to, amount); err != nil {189 panic(err.Error())190 }191}192193// RenderBlock returns the block for the realm CALLING it, which is the form a194// host realm embeds.195//196// It has no cur realm parameter on purpose. A plain read is borrowed: gno197// opens no realm frame for it, so unsafe.CurrentRealm() reports the caller's198// path and the block keys itself with no argument. Adding a cur realm here199// would silently reverse that and key every embed to this realm instead.200func RenderBlock() string {201 return blockFor(unsafe.CurrentRealm().PkgPath())202}203204// RenderBlockFor is [RenderBlock] for a named page. Use it from a crossing205// function, where there is no caller to read off the stack, or when one realm206// renders several pages.207func RenderBlockFor(page string) string { return blockFor(page) }208209// blockFor is the shared body, so no exported read of this realm calls another210// and picks up the wrong path.211func blockFor(page string) string {212 now := runtime.ChainHeight()213 return th.Block(realmPath, page, board.List(page, now, BlockThreads), board.PageLen(page), now)214}215216// Count is how many threads a page holds.217func Count(page string) int { return board.PageLen(page) }218219// Total is how many threads exist, across every page.220func Total() int { return board.Len() }221222// Pages is how many pages have ever been posted on.223func Pages() int { return board.Pages() }224225// Repliers is how many distinct addresses have replied to a thread, which is226// also how much its author has earned from it.227func Repliers(threadID int64) int {228 t, _ := board.Get(store.ID(threadID))229 return t.Repliers()230}231232// PinnedUntil is the height a thread stops being pinned at, or 0.233func PinnedUntil(threadID int64) int64 {234 t, ok := board.Get(store.ID(threadID))235 if !ok {236 return 0237 }238 return t.PinnedUntil239}240241// BalanceOf is an address's THREAD balance.242func BalanceOf(owner address) int64 { return points.BalanceOf(owner) }243244// Allowance is what owner let spender draw.245func Allowance(owner, spender address) int64 {246 return points.Token().Allowance(owner, spender)247}248249// TotalSupply is the THREAD in existence: everything earned, less everything250// burned by Boost.251func TotalSupply() int64 { return points.Supply() }252253// TokenKey is the r/nt/grc20reg key for THREAD.254func TokenKey() string { return tokenKey }255256// caller is the address that called us, checked the one way that is safe.257func caller(cur realm) address {258 if !cur.IsCurrent() {259 panic("spoofed realm: cur is not the live crossing frame")260 }261 return cur.Previous().Address()262}263Allowance(owner string, spender string) int64
Approve(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spender string, amount int64)
BalanceOf(owner string) int64
Boost(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, threadID int64)
Count(page string) int
Pages() int
PinnedUntil(threadID int64) int64
Post(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, page string, body string) int64
Render(path string) string
RenderBlock() string
RenderBlockFor(page string) string
Repliers(threadID int64) int
Reply(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, threadID int64, body string)
TokenKey() string
Total() int
TotalSupply() int64
Transfer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)
TransferFrom(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, from string, to string, amount int64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.