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

v0 source pure

Package coin is a GRC20 that cannot exist until its author has answered the three questions a social app's token usua...

Readme View source

gno.land/p/moul/x/social/coin/v0

A GRC20 that refuses to exist until its mint rule, its sink and its buyer are written down: New, Earn, Spend, Supply, Earned, Spent, Render.

 1import "gno.land/p/moul/x/social/coin/v0"
 2
 3var points = coin.New("Thread Points", "THREAD", 0, 0, coin.Policy{
 4	Mint:  "1 per distinct address that replies to your thread",
 5	Sink:  "burned to pin a thread to the top of its page",
 6	Buyer: "anyone who wants placement and has not earned it",
 7}, 0, cur)
 8
 9points.Earn(author, 1)   // the mint rule, with exactly one call site
10points.Spend(buyer, 10)  // the sink, which is the only thing that removes supply

The three strings are the whole point. New panics on a blank one, so the declaration happens before the first unit exists rather than in a README written afterwards, and Render prints all three on the issuing realm's own page, where being wrong about them is visible to the people holding the token.

A mint rule with no sink is a scoreboard with a price. Supply only grows, the number is the product, and a holder has no reason to buy one from an earner. Nothing in a package can check that a declared sink is real, which is exactly why it is a declaration and not a validation: the cheap failure is forgetting to decide, not lying.

Earned and Spent are kept separately from the supply because a sink that never fires is invisible in the supply alone. A flat line reads the same as no sink at all; two counters say which it is.

Earn of a non-positive amount is a no-op rather than an abort. A mint rule that computes zero means nothing new happened, and that should not fail the transaction that discovered it. Spend is the opposite and aborts naming both numbers, because its caller is a user about to be told they cannot afford something.

Everything else is the embedded p/nt/grc20: transfers, approvals, balances and events are an ordinary GRC20, so wallets and indexers need to know nothing about this package. Token() hands it over for the realm to expose and to register with a token registry.

The trailing _ int, rlm realm on New is the shape a pure package has to use to reach the caller's frame: a p/ may not declare a crossing function, so the realm token is threaded as a later parameter, exactly as grc20's own tellers do.

Live user: r/moul/x/social/threads.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/x/social/coin/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package coin is a GRC20 that cannot exist until its author has answered the three questions a social app's token usually dodges: what mints it, what burns it, and who has a reason to buy it.

Why a package and not a convention

Every app in the x/social family is asked the same question, "and can it have a token", and the honest answer is only yes when all three of those have an answer. A token with a mint rule and no sink is a scoreboard with a price: supply grows, nothing consumes it, and the number on the leaderboard is the whole product. That is the shape a chain-wide measurement keeps finding, and it is cheap to avoid, so New refuses a Policy with a blank field rather than letting the omission ship.

The three strings are not validated beyond being non-empty, because no package can check that a sink is real. They are a declaration, rendered on the realm's own page by Coin.Render, where being wrong is visible.

The model

Example
1earned  minted by the app, for the behaviour the app wants more of
2spent   burned by the app, for the thing holders actually want
3traded  a plain GRC20 transfer, which is what makes the sink a market

Earning and spending are the app's business and go through Coin.Earn and Coin.Spend, which keep running totals so a reader can see the two halves against each other. Transfers, approvals and balances are the embedded grc20.Token's, unchanged, so wallets and indexers see an ordinary GRC20.

Usage

A realm holds one Coin and never exports the ledger:

Example
1var points = coin.New("Thread Points", "THREAD", 0, 0, coin.Policy{
2	Mint:  "1 per distinct address that replies to your thread",
3	Sink:  "burned to pin a thread to the top of its page",
4	Buyer: "anyone who wants placement and has not earned it",
5}, 0, cur)

The trailing `_ int, rlm realm` is the shape a pure package has to use to reach the caller's frame: a p/ package may not declare a crossing function, so the realm token is threaded as a later parameter, exactly as grc20's own tellers do.

Functions 1

func New

1func New(name, symbol string, decimals int, id seqid.ID, policy Policy, _ int, rlm realm) *Coin
source

New issues the token. It panics when the policy is incomplete, which is the whole point of the package: the declaration happens before the first unit exists, not in a README written afterwards.

name, symbol, decimals and id are grc20's own; id distinguishes two tokens issued by the same realm.

Types 2

type Coin

struct
1type Coin struct {
2	tok    *grc20.Token
3	led    *grc20.PrivateLedger
4	policy Policy
5
6	earned int64 // cumulative minted
7	spent  int64 // cumulative burned
8}
source

Coin is a GRC20 plus the policy it was issued under and the running totals of the two flows the policy describes.

Methods on Coin

func BalanceOf

method on Coin
1func (c *Coin) BalanceOf(addr address) int64
source

BalanceOf is the holder's balance.

func CallerTeller

method on Coin
1func (c *Coin) CallerTeller() grc20.Teller
source

CallerTeller is the teller that acts as the user who called the realm: it moves their own balance and nothing else.

It is exposed, where the ledger is not, because transfer and approve are what make the sink a market. Mint and burn stay behind Coin.Earn and Coin.Spend so the mint rule keeps exactly one call site.

func Earn

method on Coin
1func (c *Coin) Earn(to address, amount int64)
source

Earn mints amount to addr. The realm calls it from the one place its mint rule is implemented, so that rule has exactly one call site.

A non-positive amount is a no-op rather than an abort: a mint rule that computes zero (nothing new happened) is a normal outcome and should not fail the transaction that discovered it.

func Earned

method on Coin
1func (c *Coin) Earned() int64
source

Earned and Spent are the cumulative flows. Their difference is Coin.Supply and they are kept separately because a sink that never fires is invisible in the supply alone: a flat line reads the same as no sink at all.

func Holders

method on Coin
1func (c *Coin) Holders() int
source

Holders is how many addresses the ledger knows about.

func Policy

method on Coin
1func (c *Coin) Policy() Policy
source

Policy returns the declarations the token was issued under.

func Render

method on Coin
1func (c *Coin) Render() string
source

Render is the block a realm puts on its own page: the three declarations, then the two flows against each other.

The policy strings are escaped: they are constants in practice, but a realm could build one from configuration, and this package cannot tell.

func Spend

method on Coin
1func (c *Coin) Spend(from address, amount int64)
source

Spend burns amount from addr, which is how the sink consumes supply.

It aborts when the balance is short, naming both numbers, because the caller is a user who is about to be told they cannot afford something.

func Spent

method on Coin
1func (c *Coin) Spent() int64
source

Spent is the cumulative amount burned through Coin.Spend.

func Supply

method on Coin
1func (c *Coin) Supply() int64
source

Supply is what exists right now, that is, earned minus spent.

func Token

method on Coin
1func (c *Coin) Token() *grc20.Token
source

Token returns the GRC20 itself, for the realm to expose as its read API and to register with a token registry.

type Policy

struct
 1type Policy struct {
 2	// Mint says what behaviour earns the token, precisely enough that a
 3	// reader can work out whether they can farm it.
 4	Mint string
 5
 6	// Sink says what destroys it. "Nothing" is not an answer; if there is
 7	// no sink, there is no reason for the token to exist.
 8	Sink string
 9
10	// Buyer says who wants it badly enough to acquire it from someone who
11	// earned it. This is the one most often left blank, and the one that
12	// decides whether the other two matter.
13	Buyer string
14}
source

Policy is the three declarations a Coin cannot be created without.

Each one is a sentence for a human: it is rendered on the issuing realm's page and nothing branches on it.

Methods on Policy

func Valid

method on Policy
1func (p Policy) Valid() bool
source

Valid reports whether every field is filled in.

Imports 6

Source Files 4