Transaction

1AE5C52635EAA1…7FCD80405C7C

Block 408,415 · index 0 · indexed

Summary

Hash
1AE5C52635EAA106B0902CD7F64506A63E971B00984B575D49177FCD80405C7C
Block
408,415
Size
26841 bytes
Gas used
34,855,031 / 80,868,800
Fee
242606ugnot
Status
success

Messages

#1AddPackagegno.land/r/moul/x/moultest/v011 arguments
Attached funds
11000000ugnot

Arguments · 11

  1. #1moultest
  2. #2README.md
  3. #3# `gno.land/r/moul/x/moultest/v0` **A native coin called `moultest`, and a faucet with four ceilings on it.** Not a GRC20: the chain's own bank holds it, exactly as it holds GNOT. ``` /gno.land/r/moul/x/moultest/v0:moultest ``` That string is the whole coin. There is no name, no symbol, no decimals field, no ledger in this realm and no entry in any registry, because a native denom has none of those things. 1000 moultest is a thousand moultest. ## Help yourself ``` gnokey maketx call -pkgpath gno.land/r/moul/x/moultest/v0 -func Claim ... ``` 10,000 per claim, to the caller and never to an address the caller names, once every 100 blocks, 100,000 per address for life, 1,000,000 ever. Burning gives none of it back: issuance is counted monotonically, or claim-burn-claim would be an unlimited faucet. The supply ceiling is there so the experiment ends, not so the faucet scales. ## What the coin can do that a GRC20 cannot Moving it needs nothing from this realm. It is a bank message, so any wallet sends it with no realm call, no `Approve`, no allowance outliving the transfer: ``` gnokey maketx send -to <address> -send 2500/gno.land/r/moul/x/moultest/v0:moultest ... ``` It can also ride a call. The `-send` envelope of a transaction carries coins into the realm being called, before a line of its code runs, which is what `Tip` uses: ``` gnokey maketx call -pkgpath gno.land/r/moul/x/moultest/v0 -func Tip \ -args <address> -args "nice work" \ -send 1000/gno.land/r/moul/x/moultest/v0:moultest ... ``` `Tip` forwards that envelope to the recipient and writes the note on a public board. It spends through a `BankerTypeOriginSend` banker, whose entire authority *is* the envelope: it cannot touch the realm's own balance, and a test pins that by leaving 5,000 moultest at the realm address and checking that exactly the 250 that arrived leaves again. That banker type is only available to a call whose entry frame is the realm named by the message, so `Tip` needs `maketx call` and does not work under `maketx run`. ## What it costs, which is the more interesting half A GRC20's mint and burn sit behind a ledger this realm chooses to guard. A native coin's sit behind a banker, and `banker.RemoveCoin` takes an **arbitrary address**. Nothing on the chain stops an issuing realm from deleting anybody's balance at any time. This realm does not expose that. `Burn` reads the caller from the call frame and takes no address parameter, so it can only destroy the caller's own coins. But "does not expose it" is the entire protection, and it lasts exactly as long as the code deployed at this path. Worth knowing before treating any realm-issued native coin as something you own. ## Redeploys `private = true`, so this path can be redeployed with corrected code instead of being abandoned for a `/v1`. The coins survive that (they are bank state, held at their owners' addresses, and the denom embeds the package path, which does not move) and everything on the render page does not: claim history, caps consumed and the tip board all return to their init values, handing every account a fresh lifetime allowance. Acceptable for a coin that is worthless by construction, and not acceptable for one that is not. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/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/r/moul/x/moultest/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/moultest/v0/deps.png) > 🧪 **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](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/moultest/v0" gno = "0.9" private = true
  6. #6moultest.gno
  7. #7// Package moultest issues `moultest`, a NATIVE coin, and gives it away. // // Native here means the chain's own bank holds it, exactly as it holds GNOT. // It is not a GRC20: there is no ledger in this realm's storage, no balance // map, no Transfer function, no allowance. A realm with the right banker calls // IssueCoin once and from that moment the coin is a first-class chain object, // and this realm has no further say in who holds it. // // # What that buys, and what it costs // // The interesting half is what disappears. Moving moultest needs no code here // at all: a plain bank send does it, from any wallet, with no realm call and no // approval dance, because the transfer is a tm2 bank message rather than a // function call. It can also RIDE a transaction: the `-send` envelope of a call // carries it into the realm being called, which is the one thing no GRC20 can // do, and [Tip] exists to show it. The account page of any explorer lists it // beside GNOT with nothing registered anywhere. // // The half that is worse is the authority. A GRC20's mint and burn live behind // a PrivateLedger this realm chooses to guard; a native coin's live in a banker // minted from a realm handle, and banker.RemoveCoin takes an ARBITRARY address. // Nothing in the chain stops the issuing realm from deleting anybody's balance // at any time. This realm does not expose that (see [Burn], which is scoped to // the caller and to nobody else), but "does not expose it" is the only // protection there is, and it lasts exactly as long as the deployed code. That // is the real asymmetry against a GRC20, not the ergonomics. // // There is no decimals field either, nor a name, nor a symbol: a native denom // is a string and nothing else. 1000 moultest is a thousand moultest. // // # Why it is capped // // Free money with no ceiling is a spam vector rather than a faucet, so [Claim] // is bounded four ways, and the caps are the point of the experiment as much as // the coin is: // // - [ClaimAmount] per call, to the CALLER only, never to an address the // caller names, so nobody can dust a stranger with it. // - [ClaimEvery] blocks of cooldown between two claims by one account. // - [MaxPerAccount] over that account's lifetime. // - [MaxSupply] over the realm's lifetime. // // [Burn] does not give any of it back. Issuance is counted monotonically, so // claim-burn-claim cannot walk around the per-account cap; what burning moves // is the circulating supply, which is why the two numbers are reported // separately. // // # It is a private realm // // gnomod.toml declares private = true, so this path can be redeployed with // corrected code instead of being abandoned for a v1. The price is measured // elsewhere in this repo and applies here too: the coins survive a redeploy // (they are bank state, held at their owners' addresses, and the denom embeds // the package path, which does not move), and everything on this page does not. // Claim history, caps consumed and the tip board all return to their init // values, which would hand every account a fresh [MaxPerAccount]. For a coin // that is worthless by construction that is an acceptable trade; it would not // be for one that is not. package moultest import ( "chain" "chain/banker" "chain/runtime" "chain/runtime/unsafe" "gno.land/p/nt/avl/v0" "gno.land/p/nt/ufmt/v0" ) const ( // Name is the coin's base denom: the part after the colon. The chain caps // it at 16 lowercase characters. Name = "moultest" // Path is this realm's package path. The denom embeds it verbatim, so the // coin and the code are permanently the same name. Path = "gno.land/r/moul/x/moultest/v0" // Link is Path as a gnoweb route. Link = "/r/moul/x/moultest/v0" ) // Denom is the full chain denom, "/" + [Path] + ":" + [Name]. This is the // string a wallet, an explorer or a `-send` flag needs; the bank knows nothing // about the realm behind it. var Denom = chain.CoinDenom(Path, Name) const ( // ClaimAmount is issued per successful [Claim]. ClaimAmount = int64(10_000) // ClaimEvery is the cooldown, in blocks, between two claims by one account. ClaimEvery = int64(100) // MaxPerAccount is the lifetime ceiling on what one address can claim here, // ten claims' worth. Burning does not raise it. MaxPerAccount = int64(100_000) // MaxSupply is the lifetime ceiling on what this realm can ever issue: a // hundred claims' worth, all told. Nothing lowers it, so the coin cannot be // inflated after the fact, and the experiment has an end rather than a // budget. A faucet meant to serve a crowd would put the ceiling somewhere // else; this one is meant to run out. MaxSupply = int64(1_000_000) // MaxNote is the longest tip note kept, in bytes. MaxNote = 120 // MaxTips is how many tips the board shows. Older ones fall off. MaxTips = 20 ) type account struct { lastClaim int64 // block height of the most recent claim claims int64 // how many times this address has claimed issued int64 // lifetime total issued to it, never decremented } // Tip is one entry of the public board written by [Tip]. type tip struct { from address to address amount int64 note string height int64 } var ( accounts = avl.NewTree() // address string -> *account // issued and burned are counted here rather than derived from the bank: // TotalCoin reports what CIRCULATES, and the caps are about what was ever // minted. The two differ by exactly `burned`, which a test pins. issued int64 burned int64 tips []*tip // most recent last, at most MaxTips ) // Claim issues [ClaimAmount] of moultest to the caller and returns it. // // It never issues to an address the caller names. That is deliberate: a faucet // that mints to a third party is a way to spray a denom nobody asked for into // strangers' wallets, and their account page then carries it forever. func Claim(cur realm) int64 { who := caller(cur) a := accountOf(who) now := runtime.ChainHeight() if a.claims > 0 && now < a.lastClaim+ClaimEvery { panic(ufmt.Sprintf("too soon: next claim at block %d, current %d", a.lastClaim+ClaimEvery, now)) } if a.issued+ClaimAmount > MaxPerAccount { panic(ufmt.Sprintf("account cap reached: %d of %d already claimed by this address", a.issued, MaxPerAccount)) } if issued+ClaimAmount > MaxSupply { panic(ufmt.Sprintf("supply cap reached: %d of %d already issued", issued, MaxSupply)) } a.lastClaim = now a.claims++ a.issued += ClaimAmount accounts.Set(who.String(), a) issued += ClaimAmount banker.NewBanker(banker.BankerTypeRealmIssue, cur).IssueCoin(who, Denom, ClaimAmount) return ClaimAmount } // Burn destroys `amount` of the CALLER's moultest, and only the caller's. // // The banker this realm holds could remove coins from any address on the chain // without asking, so the `who` here is read from the call frame and is never a // parameter. A clawback is what an issuing realm is always able to write; the // choice not to is made here, once, and cannot be revisited without a redeploy. // // Burning lowers what circulates and does not return any cap headroom: see // [MaxPerAccount]. func Burn(cur realm, amount int64) { if amount <= 0 { panic("burn a positive amount") } who := caller(cur) if bal := BalanceOf(who); bal < amount { panic(ufmt.Sprintf("balance is %d, cannot burn %d", bal, amount)) } banker.NewBanker(banker.BankerTypeRealmIssue, cur).RemoveCoin(who, Denom, amount) burned += amount } // Tip forwards the moultest attached to THIS transaction to `to`, and writes // the note on a public board. // // The coin arrives in the `-send` envelope of the call, credited to this // realm's address before a line of this function runs, and leaves through a // BankerTypeOriginSend banker, whose whole authority is that envelope: it can // spend what this message paid in and not one coin more, not even out of the // realm's own balance. So the realm can route a payment it was handed while // being structurally unable to touch anything else. // // A GRC20 has no equivalent. Its tokens cannot ride a message, so the same flow // costs an Approve, then a call, then an allowance that outlives both. // // Anything else in the envelope (the GNOT covering a storage deposit, say) is // left alone and stays with the realm. func Tip(cur realm, to address, note string) int64 { who := caller(cur) if !to.IsValid() { panic("tip a valid address") } if to == who { panic("tip somebody else") } if len(note) > MaxNote { panic(ufmt.Sprintf("note is %d bytes, max %d", len(note), MaxNote)) } amount := unsafe.OriginSend().AmountOf(Denom) if amount <= 0 { panic("attach moultest to the transaction: -send " + Denom) } b := banker.NewBanker(banker.BankerTypeOriginSend, cur) b.SendCoins(cur.Address(), to, chain.NewCoins(chain.NewCoin(Denom, amount))) tips = append(tips, &tip{ from: who, to: to, amount: amount, note: note, height: runtime.ChainHeight(), }) if len(tips) > MaxTips { tips = tips[len(tips)-MaxTips:] } return amount } // BalanceOf returns what the bank holds of moultest for `who`. It reads the // chain, not this realm: a balance this realm never saw counts the same. func BalanceOf(who address) int64 { return banker.NewReadonlyBanker().GetCoin(who, Denom) } // Circulating returns how much moultest exists right now, from the bank. func Circulating() int64 { return banker.NewReadonlyBanker().TotalCoin(Denom) } // Issued returns how much this realm has ever issued. It only grows, and it is // what [MaxSupply] caps. func Issued() int64 { return issued } // Burned returns how much has been destroyed through [Burn]. func Burned() int64 { return burned } // NextClaim returns the block height at which `who` may claim again, and // whether they have ever claimed at all. func NextClaim(who address) (int64, bool) { a := accounts.Get(who.String()) if a == nil { return 0, false } return a.(*account).lastClaim + ClaimEvery, true } // ClaimedBy returns the lifetime total issued to `who` by this realm, which is // what [MaxPerAccount] caps. func ClaimedBy(who address) int64 { a := accounts.Get(who.String()) if a == nil { return 0 } return a.(*account).issued } // Accounts returns how many addresses have ever claimed. func Accounts() int { return accounts.Size() } func accountOf(who address) *account { if v := accounts.Get(who.String()); v != nil { return v.(*account) } return &account{} } // caller is the account or realm that crossed into this one. func caller(cur realm) address { if !cur.IsCurrent() { panic("moultest: stale realm token") } return cur.Previous().Address() }
  8. #8moultest_test.gno
  9. #9package moultest import ( "chain" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) // testing.SetRealm governs only the crossing calls made from the frame that // called it, so every account switch below is written inline in the test body // rather than in a helper. func TestDenom(t *testing.T) { // The denom is what a wallet, an explorer and a -send flag all type, and it // is immutable once the first coin exists. Pin the literal: deriving it from // Path would restate the code rather than check it. uassert.Equal(t, "/gno.land/r/moul/x/moultest/v0:moultest", Denom) } func TestClaimIssuesRealCoins(cur realm, t *testing.T) { alice := testutils.TestAddress("mt-alice") testing.SetRealm(testing.NewUserRealm(alice)) _, ever := NextClaim(alice) uassert.False(t, ever, "no claim recorded before the first one") uassert.Equal(t, int64(0), BalanceOf(alice)) before := Circulating() got := Claim(cross(cur)) uassert.Equal(t, ClaimAmount, got) // The balance comes from the bank, not from realm storage: nothing in this // realm records who holds what. uassert.Equal(t, ClaimAmount, BalanceOf(alice)) uassert.Equal(t, before+ClaimAmount, Circulating()) uassert.Equal(t, ClaimAmount, ClaimedBy(alice)) next, ever := NextClaim(alice) uassert.True(t, ever, "a cooldown is recorded") uassert.Equal(t, ClaimEvery, next-123, "first test block is 123, so the cooldown lands there") } func TestClaimCooldown(cur realm, t *testing.T) { bob := testutils.TestAddress("mt-bob") testing.SetRealm(testing.NewUserRealm(bob)) Claim(cross(cur)) uassert.AbortsContains(t, cur, "too soon", func() { Claim(cross(cur)) }) testing.SkipHeights(ClaimEvery - 1) uassert.AbortsContains(t, cur, "too soon", func() { Claim(cross(cur)) }) testing.SkipHeights(1) Claim(cross(cur)) uassert.Equal(t, 2*ClaimAmount, BalanceOf(bob)) } func TestAccountCap(cur realm, t *testing.T) { carol := testutils.TestAddress("mt-carol") testing.SetRealm(testing.NewUserRealm(carol)) for ClaimedBy(carol)+ClaimAmount <= MaxPerAccount { Claim(cross(cur)) testing.SkipHeights(ClaimEvery) } uassert.Equal(t, MaxPerAccount, ClaimedBy(carol)) uassert.AbortsContains(t, cur, "account cap reached", func() { Claim(cross(cur)) }) // Waiting does not help: the cooldown is not what stopped it. testing.SkipHeights(ClaimEvery * 10) uassert.AbortsContains(t, cur, "account cap reached", func() { Claim(cross(cur)) }) } func TestBurnIsScopedToTheCaller(cur realm, t *testing.T) { dave := testutils.TestAddress("mt-dave") erin := testutils.TestAddress("mt-erin") testing.SetRealm(testing.NewUserRealm(erin)) Claim(cross(cur)) erinBefore := BalanceOf(erin) testing.SetRealm(testing.NewUserRealm(dave)) Claim(cross(cur)) issuedBefore, burnedBefore, circBefore := Issued(), Burned(), Circulating() claimedBefore := ClaimedBy(dave) Burn(cross(cur), 400) uassert.Equal(t, ClaimAmount-400, BalanceOf(dave)) uassert.Equal(t, circBefore-400, Circulating(), "burning destroys the coin, it is not moved") uassert.Equal(t, burnedBefore+400, Burned()) // Issuance is monotonic on purpose: if burning gave cap headroom back, // claim-burn-claim would be an unlimited faucet. uassert.Equal(t, issuedBefore, Issued()) uassert.Equal(t, claimedBefore, ClaimedBy(dave)) // Burn takes no address, so erin cannot be reached from dave's call. uassert.Equal(t, erinBefore, BalanceOf(erin)) uassert.AbortsContains(t, cur, "cannot burn", func() { Burn(cross(cur), 1_000_000) }) uassert.AbortsContains(t, cur, "positive amount", func() { Burn(cross(cur), 0) }) uassert.AbortsContains(t, cur, "positive amount", func() { Burn(cross(cur), -5) }) } func TestTipRidesTheEnvelope(cur realm, t *testing.T) { frank := testutils.TestAddress("mt-frank") grace := testutils.TestAddress("mt-grace") // On chain the -send envelope is credited to this realm's address before // the function runs. The test harness has no bank-send primitive, so the // credit is issued directly here; SetOriginSend is what the banker checks // against, and it is the half that bounds what Tip may spend. realmAddr := chain.PackageAddress(Path) envelope := chain.NewCoins(chain.NewCoin(Denom, 250)) testing.IssueCoins(realmAddr, envelope) // Unrelated coins already sitting at the realm's address. A RealmSend // banker could spend these; the OriginSend one may not, and that is the // property being pinned. testing.IssueCoins(realmAddr, chain.NewCoins(chain.NewCoin(Denom, 5_000))) realmBefore := BalanceOf(realmAddr) testing.SetRealm(testing.NewUserRealm(frank)) testing.SetOriginSend(envelope) sent := Tip(cross(cur), grace, "nice work") uassert.Equal(t, int64(250), sent) uassert.Equal(t, int64(250), BalanceOf(grace), "the tip landed on the recipient") uassert.Equal(t, realmBefore-250, BalanceOf(realmAddr), "exactly the envelope left the realm, and nothing else it was holding") uassert.Equal(t, 1, len(tips)) uassert.Equal(t, "nice work", tips[0].note) testing.SetOriginSend(nil) } func TestTipRefusals(cur realm, t *testing.T) { heidi := testutils.TestAddress("mt-heidi") ivan := testutils.TestAddress("mt-ivan") testing.SetRealm(testing.NewUserRealm(heidi)) testing.SetOriginSend(nil) uassert.AbortsContains(t, cur, "attach moultest", func() { Tip(cross(cur), ivan, "hi") }) envelope := chain.NewCoins(chain.NewCoin(Denom, 10)) testing.IssueCoins(chain.PackageAddress(Path), envelope) testing.SetOriginSend(envelope) uassert.AbortsContains(t, cur, "tip somebody else", func() { Tip(cross(cur), heidi, "me") }) uassert.AbortsContains(t, cur, "valid address", func() { Tip(cross(cur), "nope", "hi") }) uassert.AbortsContains(t, cur, "note is", func() { Tip(cross(cur), ivan, strings.Repeat("x", MaxNote+1)) }) testing.SetOriginSend(nil) } func TestSupplyCap(cur realm, t *testing.T) { // Fill the remaining supply with fresh accounts, each capped at // MaxPerAccount, then check that the next one is refused. Issuance carries // across tests, so the loop starts from whatever is left rather than from // MaxSupply. names := []string{"cap-a", "cap-b", "cap-c", "cap-d", "cap-e", "cap-f", "cap-g", "cap-h", "cap-i", "cap-j", "cap-k", "cap-l"} for _, name := range names { if Issued()+ClaimAmount > MaxSupply { break } who := testutils.TestAddress(name) testing.SetRealm(testing.NewUserRealm(who)) for ClaimedBy(who)+ClaimAmount <= MaxPerAccount && Issued()+ClaimAmount <= MaxSupply { Claim(cross(cur)) testing.SkipHeights(ClaimEvery) } } uassert.Equal(t, MaxSupply, Issued(), "the accounts above exhaust the supply exactly") late := testutils.TestAddress("cap-late") testing.SetRealm(testing.NewUserRealm(late)) uassert.AbortsContains(t, cur, "supply cap reached", func() { Claim(cross(cur)) }) // Burning does not reopen the faucet either: circulating drops, issued does // not, and the cap is on issued. testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("cap-a"))) Burn(cross(cur), 500) uassert.Equal(t, MaxSupply, Issued()) testing.SetRealm(testing.NewUserRealm(late)) uassert.AbortsContains(t, cur, "supply cap reached", func() { Claim(cross(cur)) }) } func TestRender(t *testing.T) { index := Render("") uassert.True(t, strings.Contains(index, "# moultest"), "heading:\n"+index) uassert.True(t, strings.Contains(index, Denom), "the denom is on the page") uassert.True(t, strings.Contains(index, "| issued, ever |"), "supply table:\n"+index) uassert.True(t, strings.Contains(index, "nice work"), "the tip board is shown:\n"+index) frank := testutils.TestAddress("mt-frank") acct := Render(frank.String()) uassert.True(t, strings.Contains(acct, frank.String()), "the account section names it:\n"+acct) uassert.True(t, strings.Contains(acct, "claimed for life"), acct) // A path that is not an address renders the index without an account block. uassert.False(t, strings.Contains(Render("not-an-address"), "claimed for life"), "a junk path adds no account section") }
  10. #10render.gno
  11. #11package moultest import ( "chain/runtime" "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/md/v0" "gno.land/p/nt/ufmt/v0" ) // Render shows the coin, what is left under each cap, and the tip board. // `?<address>` adds that address's balance and claim state. func Render(path string) string { var sb strings.Builder sb.WriteString(md.H1("moultest: a native coin you can help yourself to")) sb.WriteString("\nNot a GRC20. The chain's bank holds it, so it moves with a plain send, " + "shows up beside GNOT on any account page, and can ride the `-send` envelope of a " + "transaction. There is no ledger in this realm and no registry entry anywhere.\n\n") sb.WriteString(md.LanguageCodeBlock("", Denom)) sb.WriteString("\n") sb.WriteString(renderSupply()) sb.WriteString(renderAccount(path)) sb.WriteString(renderTips()) sb.WriteString(renderUsage()) return sb.String() } func renderSupply() string { t := ui.NewTable("", "amount", "cap", "left") t.Row("issued, ever", ufmt.Sprintf("%d", issued), ufmt.Sprintf("%d", MaxSupply), ufmt.Sprintf("%d", MaxSupply-issued)) t.Row("circulating", ufmt.Sprintf("%d", Circulating()), "", "") t.Row("burned", ufmt.Sprintf("%d", burned), "", "") t.Row("accounts", ufmt.Sprintf("%d", accounts.Size()), "", "") s := md.H2("Supply") + "\n" + t.String() s += "\n" + ufmt.Sprintf( "`%d` per claim, once every `%d` blocks, `%d` per address for life. "+ "Burning lowers what circulates and gives back no headroom, "+ "or claim-burn-claim would be an unlimited faucet.\n", ClaimAmount, ClaimEvery, MaxPerAccount) return s + "\n" } func renderAccount(path string) string { who := address(path) if path == "" || !who.IsValid() { return "" } s := md.H2("Account "+ui.AddrText(who)) + "\n" t := ui.NewTable("", "") t.Row("address", ui.AddrFull(who)) t.Row("balance", ufmt.Sprintf("%d moultest", BalanceOf(who))) t.Row("claimed for life", ufmt.Sprintf("%d of %d", ClaimedBy(who), MaxPerAccount)) next, ever := NextClaim(who) switch { case !ever: t.Row("next claim", "now, this address has never claimed") case runtime.ChainHeight() >= next: t.Row("next claim", "now") default: t.Row("next claim", ufmt.Sprintf("block %d (%d to go)", next, next-runtime.ChainHeight())) } return s + t.String() + "\n" } func renderTips() string { s := md.H2("Tips") + "\n" if len(tips) == 0 { return s + ui.Empty("Nobody has tipped anybody yet.") + "\n" } t := ui.NewTable("block", "from", "to", "amount", "note") // Most recent first; the slice keeps them oldest first. for i := len(tips) - 1; i >= 0; i-- { e := tips[i] t.Row(ufmt.Sprintf("%d", e.height), ui.Addr(e.from), ui.Addr(e.to), ufmt.Sprintf("%d", e.amount), ui.Cell(e.note)) } return s + t.String() + "\n" } func renderUsage() string { s := md.H2("Use it") + "\n" s += md.BulletItem(ui.Action("Claim "+ufmt.Sprintf("%d moultest", ClaimAmount), "Claim")) s += "\n" s += "Then it is an ordinary coin, and moving it needs nothing from this realm:\n\n" s += md.LanguageCodeBlock("", ""+ "# help yourself\n"+ "gnokey maketx call -pkgpath "+Path+" -func Claim ...\n\n"+ "# send some to a friend: a bank message, no realm call at all\n"+ "gnokey maketx send -to <address> -send 2500"+Denom+" ...\n\n"+ "# tip somebody through this realm, note included. The coin rides the\n"+ "# envelope, which is the thing a GRC20 cannot do.\n"+ "gnokey maketx call -pkgpath "+Path+" -func Tip \\\n"+ " -args <address> -args \"nice work\" -send 1000"+Denom+" ...\n\n"+ "# destroy some of your own\n"+ "gnokey maketx call -pkgpath "+Path+" -func Burn -args 1000 ...") s += "\n" + md.Italic("Worthless by construction. Do not price it.") + "\n" return s }

Result log

msg:0,success:true,log:,events:[]

← Back to block 408,415