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/g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr/goldenticket/v2

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v2
Namespace
g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr / goldenticket
Files
3 (gnomod.toml)
Exported functions
11
Module
gno.land/r/g1sw5xklxjjuv0yvuxy5f5s3l3mnj0nqq626a9wr/goldenticket/v2
gno
0.9

Files (3)

  • gnomod.tomltoml
  • goldenticket.gnogno
  • goldenticket_test.gnogno
goldenticket.gnogno
1// Package goldenticket is a 1/1 GRC721 NFT: one golden ticket, minted to the2// publisher when the package is added. It checks that a gno.land collection3// shows in the Adena wallet, built the way gnofly's planes are:4//5//   - the collection is a gno.land/p/nt/grc721/v0 token made with NewToken,6//     whose NewToken and Transfer events Adena finds on its indexer;7//   - the read functions take a grc721.TokenID and return (value, error), as8//     Adena calls them: OwnerOf("1"), BalanceOf("g1..."), TokenURI("1") as9//     the <img src>, TokenMetadata("1") as JSON with camelCase traitType.10//11// v2 of goldenticket: the image is no longer a constant. The publisher is12// the admin and moves it with SetImage, as the planes' SetImageBase does, so13// it can live where both Adena (any https: image) and gno.land's pages (an14// allowlist: GitHub, imgur, IPFS) load it.15//16// Transfer moves the ticket, so it can reach any Adena account.17package goldenticket1819import (20	"errors"21	"strings"2223	"gno.land/p/nt/grc721/v0"24)2526const (27	tokenName   = "Golden Ticket"28	tokenSymbol = "GOLD"29	ticket      = grc721.TokenID("1")3031	// The gno icon from gnolang/workshops, served by raw.githubusercontent.com:32	// gno.land's content policy allows *.githubusercontent.com images.33	defaultImage = "https://raw.githubusercontent.com/gnolang/workshops/main/assets/gno-icon-white.png"3435	errNoTicket = "goldenticket: there is one ticket, id 1"36	errNotOwner = "goldenticket: caller does not hold the ticket"37	errBadOwner = "goldenticket: invalid recipient address"38	errNotAdmin = "goldenticket: caller is not the admin"39	errBadImage = "goldenticket: image must be an https URL, 12 to 300 characters of [A-Za-z0-9./_-:?=&%~+]"40)4142var (43	// Token is the collection, read-only: Token.OwnerOf("1"), Token.BalanceOf.44	Token  *grc721.Token45	ledger *grc721.PrivateLedger4647	admin address // the publisher: the only one who sets the image48	image string  // what TokenURI returns, and Render and TokenMetadata show49)5051// init runs once, when the package is added to the chain. This is the only52// moment cur.Previous() is the publisher, who receives the ticket and is the53// admin.54//55// Under `gno test` there is no publisher and the address is empty. The tests56// call bootstrap themselves with a real address.57func init(cur realm) {58	bootstrap(cur.Previous().Address(), cur)59}6061// bootstrap creates the collection and mints the ticket to publisher; with62// no valid publisher, under `gno test`, it mints nothing.63func bootstrap(publisher address, rlm realm) {64	Token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm)65	admin = publisher66	image = defaultImage67	if !publisher.IsValid() {68		return69	}70	if err := ledger.Mint(publisher, ticket); err != nil {71		panic(err)72	}73}7475// Name and Symbol, as GRC721 tokens have them. Adena reads both from the76// NewToken event.77func Name() string   { return tokenName }78func Symbol() string { return tokenSymbol }7980// Admin returns who may set the image.81func Admin() address { return admin }8283// Image returns the current image URL.84func Image() string { return image }8586// SetImage points the ticket at another image, at once: TokenURI is computed87// on every read. Admin only, by a direct call.88func SetImage(cur realm, url string) {89	caller := cur.Previous()90	if !caller.IsUserCall() || caller.Address() != admin {91		panic(errNotAdmin)92	}93	if !validImage(url) {94		panic(errBadImage)95	}96	image = url97}9899// validImage reports whether s is an https URL of 12 to 300 characters of100// [A-Za-z0-9./_-:?=&%~+]. The charset keeps it safe in JSON and markdown101// without escaping: no quote, bracket, parenthesis, space or backslash.102func validImage(s string) bool {103	if len(s) < 12 || len(s) > 300 || !strings.HasPrefix(s, "https://") {104		return false105	}106	for i := 0; i < len(s); i++ {107		c := s[i]108		if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') &&109			!strings.ContainsRune("./_-:?=&%~+", rune(c)) {110			return false111		}112	}113	return true114}115116// OwnerOf returns who holds the ticket, or an error for any other id.117func OwnerOf(tid grc721.TokenID) (address, error) {118	if tid != ticket {119		return "", errors.New(errNoTicket)120	}121	return Token.OwnerOf(tid)122}123124// BalanceOf returns 1 for the holder of the ticket, 0 for everyone else.125func BalanceOf(addr address) (int64, error) {126	return Token.BalanceOf(addr)127}128129// TokenURI returns the image of the ticket: what Adena shows.130func TokenURI(tid grc721.TokenID) (string, error) {131	if _, err := OwnerOf(tid); err != nil {132		return "", err133	}134	return image, nil135}136137// TokenMetadata returns the metadata of the ticket as JSON, with the keys138// Adena reads.139func TokenMetadata(tid grc721.TokenID) (string, error) {140	if _, err := OwnerOf(tid); err != nil {141		return "", err142	}143	return `{"name":"` + tokenName + ` #1","description":"A 1/1 on gno.land, minted to check that collections show in Adena.",` +144		`"image":"` + image + `","attributes":[{"traitType":"edition","value":"1 of 1"},{"traitType":"metal","value":"gold"}]}`, nil145}146147// Transfer gives the ticket to someone else. Only its holder can, by a148// direct call (maketx call or Adena), not through another realm.149func Transfer(cur realm, to address) {150	caller := cur.Previous()151	if !caller.IsUserCall() {152		panic(errNotOwner)153	}154	if !to.IsValid() {155		panic(errBadOwner)156	}157	holder, err := Token.OwnerOf(ticket)158	if err != nil || holder != caller.Address() {159		panic(errNotOwner)160	}161	if err := ledger.TransferFrom(holder, holder, to, ticket); err != nil {162		panic(err)163	}164}165166// Render shows the ticket and who holds it.167func Render(_ string) string {168	var b strings.Builder169	b.WriteString("# " + tokenName + "\n\n")170	b.WriteString("A 1/1 GRC721 on gno.land, minted to check that collections show in Adena.\n\n")171	b.WriteString("![" + tokenName + "](" + image + ")\n\n")172	holder, err := Token.OwnerOf(ticket)173	if err != nil {174		b.WriteString("Not minted.\n")175		return b.String()176	}177	b.WriteString("Held by `" + holder.String() + "`.\n")178	return b.String()179}180

Functions

  • Admin() string

  • BalanceOf(addr string) (int64, interface {Error func() string})

  • Image() string

  • Name() string

  • OwnerOf(tid string) (string, interface {Error func() string})

  • Render(string) string

  • SetImage(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}, url string)

  • Symbol() string

  • TokenMetadata(tid string) (string, interface {Error func() string})

  • TokenURI(tid string) (string, interface {Error func() string})

  • 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)

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.