PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/moul/x/social/threads/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
moul / x / social / threads
Files
5 (README)(gnomod.toml)
Exported functions
18
Module
gno.land/r/moul/x/social/threads/v0
gno
0.9
This realm implements a recognized token.THREAD →

Files (5)

  • README.mdmarkdown
  • gnomod.tomltoml
  • render.gnogno
  • threads.gnogno
  • threads_test.gnogno
threads.gnogno
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}263

Functions

  • Allowance(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.

Rendered

RenderedRawgnoweb ↗

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.