1// Package coin is a GRC20 that cannot exist until its author has answered the2// three questions a social app's token usually dodges: what mints it, what3// burns it, and who has a reason to buy it.4//5// # Why a package and not a convention6//7// Every app in the x/social family is asked the same question, "and can it8// have a token", and the honest answer is only yes when all three of those9// have an answer. A token with a mint rule and no sink is a scoreboard with a10// price: supply grows, nothing consumes it, and the number on the leaderboard11// is the whole product. That is the shape a chain-wide measurement keeps12// finding, and it is cheap to avoid, so [New] refuses a [Policy] with a blank13// field rather than letting the omission ship.14//15// The three strings are not validated beyond being non-empty, because no16// package can check that a sink is real. They are a declaration, rendered on17// the realm's own page by [Coin.Render], where being wrong is visible.18//19// # The model20//21// earned minted by the app, for the behaviour the app wants more of22// spent burned by the app, for the thing holders actually want23// traded a plain GRC20 transfer, which is what makes the sink a market24//25// Earning and spending are the app's business and go through [Coin.Earn] and26// [Coin.Spend], which keep running totals so a reader can see the two halves27// against each other. Transfers, approvals and balances are the embedded28// [grc20.Token]'s, unchanged, so wallets and indexers see an ordinary GRC20.29//30// # Usage31//32// A realm holds one Coin and never exports the ledger:33//34// var points = coin.New("Thread Points", "THREAD", 0, 0, coin.Policy{35// Mint: "1 per distinct address that replies to your thread",36// Sink: "burned to pin a thread to the top of its page",37// Buyer: "anyone who wants placement and has not earned it",38// }, 0, cur)39//40// The trailing `_ int, rlm realm` is the shape a pure package has to use to41// reach the caller's frame: a p/ package may not declare a crossing function,42// so the realm token is threaded as a later parameter, exactly as grc20's own43// tellers do.44package coin4546import (47 "gno.land/p/moul/kit/num/v0"48 "gno.land/p/moul/kit/ui/v0"49 "gno.land/p/moul/md/v0"50 "gno.land/p/nt/grc20/v0"51 "gno.land/p/nt/seqid/v0"52 "gno.land/p/nt/ufmt/v0"53)5455// Policy is the three declarations a Coin cannot be created without.56//57// Each one is a sentence for a human: it is rendered on the issuing realm's58// page and nothing branches on it.59type Policy struct {60 // Mint says what behaviour earns the token, precisely enough that a61 // reader can work out whether they can farm it.62 Mint string6364 // Sink says what destroys it. "Nothing" is not an answer; if there is65 // no sink, there is no reason for the token to exist.66 Sink string6768 // Buyer says who wants it badly enough to acquire it from someone who69 // earned it. This is the one most often left blank, and the one that70 // decides whether the other two matter.71 Buyer string72}7374// Valid reports whether every field is filled in.75func (p Policy) Valid() bool {76 return p.Mint != "" && p.Sink != "" && p.Buyer != ""77}7879// Coin is a GRC20 plus the policy it was issued under and the running totals80// of the two flows the policy describes.81type Coin struct {82 tok *grc20.Token83 led *grc20.PrivateLedger84 policy Policy8586 earned int64 // cumulative minted87 spent int64 // cumulative burned88}8990// New issues the token. It panics when the policy is incomplete, which is the91// whole point of the package: the declaration happens before the first unit92// exists, not in a README written afterwards.93//94// name, symbol, decimals and id are grc20's own; id distinguishes two tokens95// issued by the same realm.96func New(name, symbol string, decimals int, id seqid.ID, policy Policy, _ int, rlm realm) *Coin {97 if !policy.Valid() {98 panic("coin: a token needs a mint rule, a sink and a buyer; one of the three is empty")99 }100 tok, led := grc20.NewToken(name, symbol, decimals, id, rlm)101 return &Coin{tok: tok, led: led, policy: policy}102}103104// Token returns the GRC20 itself, for the realm to expose as its read API and105// to register with a token registry.106func (c *Coin) Token() *grc20.Token { return c.tok }107108// CallerTeller is the teller that acts as the user who called the realm: it109// moves their own balance and nothing else.110//111// It is exposed, where the ledger is not, because transfer and approve are112// what make the sink a market. Mint and burn stay behind [Coin.Earn] and113// [Coin.Spend] so the mint rule keeps exactly one call site.114func (c *Coin) CallerTeller() grc20.Teller { return c.led.CallerTeller() }115116// Policy returns the declarations the token was issued under.117func (c *Coin) Policy() Policy { return c.policy }118119// Earn mints amount to addr. The realm calls it from the one place its mint120// rule is implemented, so that rule has exactly one call site.121//122// A non-positive amount is a no-op rather than an abort: a mint rule that123// computes zero (nothing new happened) is a normal outcome and should not124// fail the transaction that discovered it.125func (c *Coin) Earn(to address, amount int64) {126 if amount <= 0 {127 return128 }129 if err := c.led.Mint(to, amount); err != nil {130 panic("coin: mint: " + err.Error())131 }132 c.earned += amount133}134135// Spend burns amount from addr, which is how the sink consumes supply.136//137// It aborts when the balance is short, naming both numbers, because the138// caller is a user who is about to be told they cannot afford something.139func (c *Coin) Spend(from address, amount int64) {140 if amount <= 0 {141 panic("coin: spend: amount must be positive")142 }143 if bal := c.tok.BalanceOf(from); bal < amount {144 panic(ufmt.Sprintf("coin: balance %d is short of %d %s", bal, amount, c.tok.GetSymbol()))145 }146 if err := c.led.Burn(from, amount); err != nil {147 panic("coin: burn: " + err.Error())148 }149 c.spent += amount150}151152// BalanceOf is the holder's balance.153func (c *Coin) BalanceOf(addr address) int64 { return c.tok.BalanceOf(addr) }154155// Supply is what exists right now, that is, earned minus spent.156func (c *Coin) Supply() int64 { return c.tok.TotalSupply() }157158// Earned and Spent are the cumulative flows. Their difference is [Coin.Supply]159// and they are kept separately because a sink that never fires is invisible in160// the supply alone: a flat line reads the same as no sink at all.161func (c *Coin) Earned() int64 { return c.earned }162163// Spent is the cumulative amount burned through [Coin.Spend].164func (c *Coin) Spent() int64 { return c.spent }165166// Holders is how many addresses the ledger knows about.167func (c *Coin) Holders() int { return c.tok.KnownAccounts() }168169// Render is the block a realm puts on its own page: the three declarations,170// then the two flows against each other.171//172// The policy strings are escaped: they are constants in practice, but a realm173// could build one from configuration, and this package cannot tell.174func (c *Coin) Render() string {175 out := md.H3(ui.Inline(c.tok.GetName()) + " (" + ui.Inline(c.tok.GetSymbol()) + ")")176177 t := ui.NewTable("", "")178 t.Row("earns", ui.Cell(c.policy.Mint))179 t.Row("burns", ui.Cell(c.policy.Sink))180 t.Row("bought by", ui.Cell(c.policy.Buyer))181 out += t.String()182183 s := ui.NewTable("supply", "earned", "burned", "holders")184 s.Row(185 num.Dec(c.Supply(), c.tok.GetDecimals()),186 num.Dec(c.earned, c.tok.GetDecimals()),187 num.Dec(c.spent, c.tok.GetDecimals()),188 ufmt.Sprintf("%d", c.Holders()),189 )190 out += s.String()191192 return out193}194Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.