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/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gems/stable

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
stable
Namespace
g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems / gems
Files
2 (gnomod.toml)
Exported functions
53
Module
gno.land/r/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gems/stable
gno
0.9

Files (2)

  • gnomod.tomltoml
  • stable.gnogno
stable.gnogno
1// Package stablev4 is the permanent half of the gems6v2 split-realm2// design (stablev3 plus two fixes from the 2026-09-15 Pearl test run:3// a burned token's own page, and no per-token copy of the sealed image) -- a candidate successor to the real gems6/stable, built to4// prove two things together before either goes anywhere near mainnet:5// a delayed-reveal mint (fixing the OutOfGasError problem the real6// gems6/v1's instant-reveal MintAssembled has against Adena's own gas7// estimation -- confirmed on a throwaway single-realm spike,8// gno.land/r/g1gn6t0q9wenwhdda47rkrpfd63kcxjvyp7eqwku/revealpoc,9// before this real split-realm version was attempted), and the10// approved gemsartfab art package (crystal-look repaint + a real Rose11// Cut cascade fix + a facet-merge seam fix on Round Brilliant/Oval).12//13// Deployed under a test address (not the vesting wallet) so this whole14// prototype -- stable, campaign, v2, and gemsartfab itself -- is15// self-contained and doesn't need the real collection's own keys to16// validate. Folding a confirmed-good design into the real mainnet17// deploy is a separate step once this proves out.18//19// Reveal mechanism: MintAssembled records a trait selection and a20// SEALED placeholder image immediately (cheap, see RecordMint's own21// doc comment for why this is the whole point). TokenURI/TokenMetadata22// compute the real image via gemsartfab -- but ONLY once23// RevealDelayBlocks has passed -- through currentMetadata, which is24// the one addition to the real stable's own read path. This DOES mean25// this realm, unlike the real gems6/stable, imports an art package26// directly: unavoidable given gno's static imports (the realm serving27// TokenURI has to know how to render, and rendering can't be deferred28// to "whichever realm is currently approved" the way minting can,29// since a future version's own art package isn't importable before it30// exists). Matches the tradeoff the older g20 collection already31// accepted for the exact same reason -- not a new problem introduced32// here.33//34// Everything else (ownership, whitelist, royalty, claim log, campaign35// wiring) is unchanged from the real gems6/stable -- see that36// package's own doc comments for the full design history.37//38// Ledger: the standard gno.land/p/nt/grc721/v0 (2026-09-26), not a39// vendored grc721. Adena lists only collections whose NewToken/Transfer40// events come from that exact package (see ~/gno-land-dev-notes.md), and41// chain.Emit stamps the emitting package, so this cannot be retrofitted42// later. The package stores ownership only; every token's metadata is43// computed here from its traits (sealedData), never stored per token.44// TokenURI returns the image itself (Adena puts it straight into an45// <img>); TokenMetadata returns the metadata as a JSON string, and46// TokenJSON the classic base64 JSON data URI for marketplaces that want it.47package stable4849import (50	runtime "chain/runtime"51	unsaferealm "chain/runtime/unsafe"52	"encoding/base64"53	"strconv"54	"strings"5556	"gno.land/p/nt/avl/v0"57	"gno.land/p/nt/grc721/v0"58	"gno.land/p/nt/seqid/v0"59	campaign "gno.land/r/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gems/campaign"60	gemsart "gno.land/p/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gemsart3d2"61	"gno.land/p/nt/ufmt/v0"62)6364const (65	CollectionName   = "GNO Gems"66	CollectionSymbol = "GEMS"6768	// realmRelPath: this realm's own gnoweb-relative path, baked in as69	// a compile-time constant -- see the real gems6/stable's own70	// comment on why (unsaferealm.CurrentRealm() resolves differently71	// nested inside Render than called directly). Bumped for this72	// deployment, per the gemsartfab handoff's own reminder that this73	// gets missed.74	realmRelPath = "/r/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gems/stable"7576	// 6, not 10: every read has a 3,000,000,000 gas ceiling (vm77	// maxGasQuery) and a 3D token renders for up to ~265M (Cushion); 878	// renders plus page overhead ran out of gas on a local chain, 6 leave79	// about half the budget spare80	// summaryPageSize: tokens per CollectionSummary / WalletSummary call81	// (the mint site's grids). Thumbnail renders, like the gnoweb table;82	// TokenSummary of one token keeps the full art.83	summaryPageSize = 1084	// gnowebListLimit: rows per page of the gnoweb collection table. Rows85	// use the art's thumbnail render (~145-165M gas, vs ~245-265M full),86	// so 10 fit well under the 3B query ceiling; the token page and the87	// summaries the mint site reads keep the full art.88	gnowebListLimit = 1089	pageWindow         = 290	tokenPageImageSize = "420"9192	// RevealDelayBlocks: kept short so this candidate design can be93	// tested end-to-end in one sitting -- a real production value94	// would be tuned separately before mainnet.95	RevealDelayBlocks = 596)9798var (99	token  *grc721.Token100	ledger *grc721.PrivateLedger // private: only this realm mints, burns and moves tokens101102	collectionOwner address = "g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems"103104	approvedVersion string105106	mintPriceUgnot int64 = 100000000 // 100 GNOT, per request107108	whitelistBps avl.Tree109110	royaltyBps       int64   = 500111	royaltyRecipient address = "g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems"112113	mintedIDs []grc721.TokenID114115	mintedTraitCombos avl.Tree // combo key -> true116	tokenComboKeys    avl.Tree // tid.String() -> comboKey string117	mintCountByWallet avl.Tree // address.String() -> int64118119	mintingOpenMirror       = true120	maxMintsPerWalletMirror int64 = 0121122	burnedRecords avl.Tree // tid.String() -> burnRecord123	burnedIDs     []grc721.TokenID124125	claimRecords []claimRecord126127	// sealedData/mintHeight: the reveal mechanism's own state. tid ->128	// its locked-in trait selection / the block it was minted at.129	// Deleted from both on Burn (releases their storage deposit, same130	// as tokenComboKeys already does) and never touched again once a131	// token is revealed -- IsRevealed keeps working after that point132	// purely from Age, no separate "already revealed" flag needed.133	sealedData avl.Tree // tid.String() -> traitPreset134	mintHeight avl.Tree // tid.String() -> int64135136	sealedImage string137)138139// traitPreset is one token's locked-in trait selection -- everything140// gemsartfab needs to render the real image once revealed. Owned here141// (not imported from whichever realm minted it) since stable is the142// one place this needs to survive any future mint-mechanics version143// swap.144type traitPreset struct {145	Chain    string146	ShapeIdx int147	ColorIdx int148	Metal    string149	Active   bool150	Rarity   string // tier fixed at mint by the version realm (v5.rarityTier)151}152153type claimRecord struct {154	Wallet     address155	Bps        int64156	MintHeight int64157	Tid        grc721.TokenID158}159160// burnRecord keeps a burned token's compact trait selection, not its161// expanded name/description/attribute strings: the text is rebuilt on read162// (traitOnlyMetadata), so a burn frees more storage deposit.163type burnRecord struct {164	BurnedBy   address165	BurnHeight int64166	Traits     traitPreset167}168169func (r burnRecord) metadata() Metadata {170	if r.Traits.Chain == "" {171		return Metadata{Name: "Untitled"}172	}173	return traitOnlyMetadata(r.Traits)174}175176func init(cur realm) {177	// emits NewToken; the token id becomes "<this realm>.<symbol>.0000000"178	token, ledger = grc721.NewToken(CollectionName, CollectionSymbol, seqid.ID(0), cur)179	svg := `<svg xmlns="http://www.w3.org/2000/svg" width="170" height="170" viewBox="0 0 170 170">` +180		`<rect width="170" height="170" fill="#0a0b10"/>` +181		`<circle cx="85" cy="78" r="34" fill="none" stroke="#555" stroke-width="4"/>` +182		`<text x="85" y="83" font-size="12" text-anchor="middle" fill="#999" font-family="monospace">SEALED</text>` +183		`</svg>`184	sealedImage = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))185}186187// Metadata/Trait: the shape every reader here builds (grc721/v0 keeps no188// metadata of its own). Never stored per token -- see currentMetadata.189type Trait struct {190	TraitType string191	Value     string192}193194type Metadata struct {195	Name            string196	Description     string197	Image           string198	BackgroundColor string199	Attributes      []Trait200}201202/* ------------------------------ standard reads ------------------------------ */203204func Name() string      { return CollectionName }205func Symbol() string    { return CollectionSymbol }206func TokenCount() int64 { return token.TotalSupply() }207func TokenID() string   { return token.ID() }208209// GetName/GetSymbol: the Teller-style names some tools look for.210func GetName() string   { return CollectionName }211func GetSymbol() string { return CollectionSymbol }212213func OwnerOf(tid grc721.TokenID) (address, error)     { return token.OwnerOf(tid) }214func BalanceOf(owner address) (int64, error)          { return token.BalanceOf(owner) }215func GetApproved(tid grc721.TokenID) (address, error) { return token.GetApproved(tid) }216func IsApprovedForAll(owner, operator address) bool   { return token.IsApprovedForAll(owner, operator) }217218func ApprovedVersion() string { return approvedVersion }219func Owner() address          { return collectionOwner }220func MintPrice() int64        { return mintPriceUgnot }221222func MintedIDs() []grc721.TokenID { return mintedIDs }223func TotalBurned() int64          { return int64(len(burnedIDs)) }224225func MintingOpen() bool        { return mintingOpenMirror }226func MaxMintsPerWallet() int64 { return maxMintsPerWalletMirror }227228func MintsByWallet(owner address) int64 {229	n, _ := mintCountByWallet.Get(owner.String()).(int64)230	return n231}232233func IsTraitComboAvailable(comboKey string) bool {234	return !mintedTraitCombos.Has(comboKey)235}236237func WhitelistBps(wallet address) (bps int64, ok bool) {238	if !whitelistBps.Has(wallet.String()) {239		return 0, false240	}241	return whitelistBps.Get(wallet.String()).(int64), true242}243244func WhitelistCount() int64 { return int64(whitelistBps.Size()) }245246func WhitelistEntry(index int) (wallet string, bps int64) {247	w, v := whitelistBps.GetByIndex(index)248	return w, v.(int64)249}250251func EffectivePrice(caller address) int64 {252	if v, ok := whitelistBps.Get(caller.String()).(int64); ok {253		return mintPriceUgnot * v / 10000254	}255	if bps, _, ok := campaign.Peek(caller); ok {256		return mintPriceUgnot * bps / 10000257	}258	return mintPriceUgnot259}260261func RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error) {262	if _, err := token.OwnerOf(tid); err != nil {263		return "", 0, err264	}265	return royaltyRecipient, salePrice * royaltyBps / 10000, nil266}267268func RoyaltyBps() int64         { return royaltyBps }269func RoyaltyRecipient() address { return royaltyRecipient }270271/* -------------------------------- owner-gated -------------------------------- */272273func callerAddress() address {274	return unsaferealm.PreviousRealm().Address()275}276277func assertOwner() {278	if callerAddress() != collectionOwner {279		panic("unauthorized: caller is not the collection owner")280	}281}282283func SetApprovedVersion(cur realm, pkgpath string) {284	assertOwner()285	approvedVersion = pkgpath286}287288func SetMintPrice(cur realm, ugnot int64) {289	assertOwner()290	if ugnot < 0 {291		panic("price cannot be negative")292	}293	mintPriceUgnot = ugnot294}295296func AddToWhitelist(cur realm, wallet address, bps int64) {297	assertOwner()298	if bps < 0 || bps > 10000 {299		panic("bps must be between 0 and 10000")300	}301	whitelistBps.Set(wallet.String(), bps)302}303304func AddToWhitelistBatch(cur realm, walletsCSV string, bps int64) {305	assertOwner()306	if bps < 0 || bps > 10000 {307		panic("bps must be between 0 and 10000")308	}309	for _, w := range strings.Split(walletsCSV, ",") {310		w = strings.TrimSpace(w)311		if w == "" {312			continue313		}314		whitelistBps.Set(w, bps)315	}316}317318func RemoveFromWhitelist(cur realm, wallet address) {319	assertOwner()320	whitelistBps.Remove(wallet.String())321}322323func SetRoyaltyBps(cur realm, bps int64) {324	assertOwner()325	if bps < 0 || bps > 10000 {326		panic("bps must be between 0 and 10000")327	}328	royaltyBps = bps329}330331func SetRoyaltyRecipient(cur realm, recipient address) {332	assertOwner()333	royaltyRecipient = recipient334}335336/* ------------------------------- version-gated -------------------------------- */337338func assertCallerIsApprovedVersion(cur realm) {339	caller := cur.Previous().PkgPath()340	if approvedVersion == "" {341		panic("no version realm has been approved yet")342	}343	if caller != approvedVersion {344		panic("unauthorized: caller is not the approved version realm")345	}346}347348// RecordMint: the ENTIRE point of this v2 candidate. Unlike the real349// gems6/stable's own RecordMint, this does NOT take a full, rendered350// grc721.Metadata -- it takes the raw trait fields instead, and writes351// a small fixed-size SEALED placeholder to nft's own storage. The real352// image is never computed here, or anywhere, during a mint transaction353// -- see currentMetadata for where it actually happens (a plain read,354// unbound by any tx-level gas-wanted). Only the approved version realm355// may call this. Reserves comboKey atomically with the mint, same356// authoritative-check role the real stable's RecordMint plays.357func RecordMint(cur realm, to address, payer address, tid grc721.TokenID, chainName string, shapeIdx, colorIdx int, metal string, active bool, comboKey string, rarity string) grc721.TokenID {358	assertCallerIsApprovedVersion(cur)359	if mintedTraitCombos.Has(comboKey) {360		panic("trait combination already minted")361	}362	// Ownership only: no metadata is stored per token, not even a363	// "Sealed" record -- currentMetadata derives it from sealedData.364	checkErr(ledger.Mint(to, tid))365	mintedTraitCombos.Set(comboKey, true)366	tokenComboKeys.Set(tid.String(), comboKey)367	mintCountByWallet.Set(to.String(), MintsByWallet(to)+1)368	mintedIDs = append(mintedIDs, tid)369370	height := runtime.ChainHeight()371	mintHeight.Set(tid.String(), height)372	sealedData.Set(tid.String(), traitPreset{373		Chain: chainName, ShapeIdx: shapeIdx, ColorIdx: colorIdx, Metal: metal, Active: active, Rarity: rarity,374	})375376	if _, hasOverride := whitelistBps.Get(payer.String()).(int64); !hasOverride {377		if bps, ok := campaign.ConsumeDiscount(cross(cur), payer); ok {378			claimRecords = append(claimRecords, claimRecord{379				Wallet:     payer,380				Bps:        bps,381				MintHeight: height,382				Tid:        tid,383			})384		}385	}386	return tid387}388389func ClaimCount() int64 { return int64(len(claimRecords)) }390391func ClaimAt(index int) (wallet address, bps int64, mintHeight_ int64, tid string) {392	r := claimRecords[index]393	return r.Wallet, r.Bps, r.MintHeight, r.Tid.String()394}395396func ReportMintingOpen(cur realm, open bool) {397	assertCallerIsApprovedVersion(cur)398	mintingOpenMirror = open399}400401func ReportMaxMintsPerWallet(cur realm, max int64) {402	assertCallerIsApprovedVersion(cur)403	maxMintsPerWalletMirror = max404}405406/* -------------------------------- reveal logic ------------------------------- */407408// Age returns real blocks elapsed since tid was minted, or -1 if it409// doesn't exist / was never sealed.410func Age(tid grc721.TokenID) int64 {411	h := mintHeight.Get(tid.String())412	if h == nil {413		return -1414	}415	return runtime.ChainHeight() - h.(int64)416}417418// IsRevealed reports whether tid's real metadata is visible yet.419func IsRevealed(tid grc721.TokenID) bool {420	if _, isTraitPreset := sealedData.Get(tid.String()).(traitPreset); !isTraitPreset {421		return true422	}423	age := Age(tid)424	return age >= 0 && age >= RevealDelayBlocks425}426427// currentMetadata is what every reader-facing function below actually428// returns: nft's own stored (sealed) metadata before reveal, or the429// real image assembled fresh from gemsartfab after -- computed on this430// plain read, never inside a mint or any other gas-metered write. This431// is the one addition to the real gems6/stable's own read path.432func currentMetadata(tid grc721.TokenID) (Metadata, error) {433	if _, err := token.OwnerOf(tid); err != nil {434		return Metadata{}, err435	}436	tp, ok := sealedData.Get(tid.String()).(traitPreset)437	if !ok {438		return Metadata{}, grc721.ErrInvalidTokenId439	}440	if !IsRevealed(tid) {441		return sealedMetadata(), nil442	}443	return assembledMetadata(tid, tp), nil444}445446// sealedMetadata: what every token shows until its reveal block, the one447// shared placeholder image (not per-token storage).448func sealedMetadata() Metadata {449	return Metadata{Name: "Sealed", Image: sealedImage, BackgroundColor: "0a0b10", Attributes: []Trait{{TraitType: "Status", Value: "Sealed"}}}450}451452// listRow is one gnoweb table row's name and image: the name from the453// traits (no render), the image the art's thumbnail render once revealed,454// the shared sealed placeholder before.455func listRow(tid grc721.TokenID) (name, image string) {456	tp, ok := sealedData.Get(tid.String()).(traitPreset)457	if !ok {458		return "Untitled", ""459	}460	if !IsRevealed(tid) {461		return "Sealed", sealedImage462	}463	name = traitOnlyMetadata(tp).Name464	svg := gemsart.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())465	return name, "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))466}467468func assembledMetadata(tid grc721.TokenID, tp traitPreset) Metadata {469	svg, stone, _, colorName := gemsart.RenderTokenSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())470	shapeName := gemsart.ShapeNames[tp.ShapeIdx]471	if stone == "Pearl" {472		shapeName = gemsart.PearlShapeNames[tp.ShapeIdx]473	}474	image := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))475	status := "Sunset"476	if tp.Active {477		status = "Active"478	}479	return Metadata{480		Name:            gemDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),481		Description:     ufmt.Sprintf("%s %s %s set in %s %s ring, minted on %s.", article(colorName), colorName, shapeName, strings.ToLower(article(tp.Metal)), tp.Metal, tp.Chain),482		Image:           image,483		BackgroundColor: "0a0b10",484		Attributes: []Trait{485			{TraitType: "Chain", Value: tp.Chain},486			{TraitType: "Stone", Value: stone},487			{TraitType: "Shape", Value: shapeName},488			{TraitType: "Color", Value: colorName},489			{TraitType: "Ring", Value: tp.Metal},490			{TraitType: "Status", Value: status},491			{TraitType: "Rarity", Value: tp.Rarity},492		},493	}494}495496// article is "An" before a vowel sound (Amethyst, Imperial, Orange), else "A".497func article(word string) string {498	if word != "" && strings.ContainsRune("AEIOUaeiou", rune(word[0])) {499		return "An"500	}501	return "A"502}503504func gemDisplayName(colorName, stone, shape, metal, chain string) string {505	// "Amethyst Oval · Gold Ring · TEST-7": the piece, its setting, its506	// chain, split by a middle dot (the old "X -- Y Ring - Z Chain" was a507	// parsing delimiter that leaked into the name). Quartz colour names are508	// the variety names and already say what the stone is (Amethyst,509	// Citrine, Rose Quartz, Rock Crystal...): "Amethyst Oval", not510	// "Amethyst Quartz Oval".511	piece := colorName + " " + stone + " " + shape512	if stone == "Quartz" || stone == "" {513		piece = colorName + " " + shape514	}515	return piece + " · " + metal + " Ring · " + chain516}517518// colorNameFor looks up a stone's colorIdx-th color name from519// gemsart.ColorPaletteCSV ("hex:name;hex:name;...") -- a cheap parse,520// not a render. Kept in sync with gemsart.RenderTokenSVG's own internal521// colorForStone by construction (same palette source), without ever522// calling RenderTokenSVG itself.523func colorNameFor(stone string, colorIdx int) string {524	pairs := strings.Split(gemsart.ColorPaletteCSV(stone), ";")525	if colorIdx < 0 || colorIdx >= len(pairs) {526		return ""527	}528	parts := strings.SplitN(pairs[colorIdx], ":", 2)529	if len(parts) != 2 {530		return ""531	}532	return parts[1]533}534535// traitOnlyMetadata builds the archived record straight from tp's raw536// fields -- no gemsart.RenderTokenSVG call, so no image, ever. This is537// what Burn archives: the burn record was never meant to keep the538// picture (see Burn's own comment), so there's no reason to pay the539// render cost to build one just to immediately discard it.540func traitOnlyMetadata(tp traitPreset) Metadata {541	stone := gemsart.StoneForChain(tp.Chain)542	shapeName := gemsart.ShapeNames[tp.ShapeIdx]543	if stone == "Pearl" {544		shapeName = gemsart.PearlShapeNames[tp.ShapeIdx]545	}546	colorName := colorNameFor(stone, tp.ColorIdx)547	status := "Sunset"548	if tp.Active {549		status = "Active"550	}551	return Metadata{552		Name:            gemDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),553		Description:     ufmt.Sprintf("%s %s %s set in %s %s ring, minted on %s.", article(colorName), colorName, shapeName, strings.ToLower(article(tp.Metal)), tp.Metal, tp.Chain),554		BackgroundColor: "0a0b10",555		Attributes: []Trait{556			{TraitType: "Chain", Value: tp.Chain},557			{TraitType: "Stone", Value: stone},558			{TraitType: "Shape", Value: shapeName},559			{TraitType: "Color", Value: colorName},560			{TraitType: "Ring", Value: tp.Metal},561			{TraitType: "Status", Value: status},562			{TraitType: "Rarity", Value: tp.Rarity},563		},564	}565}566567// TokenURI is the token's image itself, as a data URI: the sealed568// placeholder until the reveal block, then the full art. Adena puts it569// straight into an <img>, which also runs the art's CSS animation.570func TokenURI(tid grc721.TokenID) (string, error) {571	metadata, err := currentMetadata(tid)572	if err != nil {573		return "", err574	}575	return metadata.Image, nil576}577578// TokenMetadata is the metadata as a JSON string in the camelCase shape579// Adena reads (name, description, backgroundColor, attributes of580// {traitType, value}). No image in it, so it costs no render: the image581// is TokenURI.582func TokenMetadata(tid grc721.TokenID) (string, error) {583	if _, err := token.OwnerOf(tid); err != nil {584		return "", err585	}586	m := sealedMetadata()587	if tp, ok := sealedData.Get(tid.String()).(traitPreset); ok && IsRevealed(tid) {588		m = traitOnlyMetadata(tp)589	}590	m.Image = ""591	return metadataJSON(m, "traitType", "backgroundColor"), nil592}593594// TokenJSON is the classic OpenSea-style metadata (snake_case keys, the595// image inline) as a base64 JSON data URI, for marketplaces that expect a596// tokenURI of that shape.597func TokenJSON(tid grc721.TokenID) (string, error) {598	metadata, err := currentMetadata(tid)599	if err != nil {600		return "", err601	}602	return "data:application/json;base64," + base64.StdEncoding.EncodeToString([]byte(metadataJSON(metadata, "trait_type", "background_color"))), nil603}604605func jsonEscape(s string) string {606	var b strings.Builder607	for i := 0; i < len(s); i++ {608		c := s[i]609		switch c {610		case '"':611			b.WriteString(`\"`)612		case '\\':613			b.WriteString(`\\`)614		default:615			b.WriteByte(c)616		}617	}618	return b.String()619}620621func metadataJSON(m Metadata, traitKey, bgKey string) string {622	var b strings.Builder623	b.WriteString(`{"name":"`)624	b.WriteString(jsonEscape(m.Name))625	b.WriteString(`","description":"`)626	b.WriteString(jsonEscape(m.Description))627	b.WriteString(`"`)628	if m.Image != "" {629		b.WriteString(`,"image":"`)630		b.WriteString(jsonEscape(m.Image))631		b.WriteString(`"`)632	}633	if m.BackgroundColor != "" {634		b.WriteString(`,"` + bgKey + `":"`)635		b.WriteString(jsonEscape(m.BackgroundColor))636		b.WriteString(`"`)637	}638	b.WriteString(`,"attributes":[`)639	for i, t := range m.Attributes {640		if i > 0 {641			b.WriteString(",")642		}643		b.WriteString(`{"` + traitKey + `":"`)644		b.WriteString(jsonEscape(t.TraitType))645		b.WriteString(`","value":"`)646		b.WriteString(jsonEscape(t.Value))647		b.WriteString(`"}`)648	}649	b.WriteString("]}")650	return b.String()651}652653/* -------------------------- Token-owner actions --------------------------- */654655func Approve(cur realm, to address, tid grc721.TokenID) {656	checkErr(ledger.Approve(callerAddress(), to, tid))657}658659func SetApprovalForAll(cur realm, operator address, approved bool) {660	checkErr(ledger.SetApprovalForAll(callerAddress(), operator, approved))661}662663func TransferFrom(cur realm, from, to address, tid grc721.TokenID) {664	checkErr(ledger.TransferFrom(callerAddress(), from, to, tid))665}666667// SafeTransferFrom: kept for the standard signature. grc721/v0 has no668// receiver-contract hook, so it is TransferFrom.669func SafeTransferFrom(cur realm, from, to address, tid grc721.TokenID) {670	checkErr(ledger.TransferFrom(callerAddress(), from, to, tid))671}672673// Burn destroys a token. Caller must be its current owner. Archives the674// real trait combination as text -- always the real traits, even if675// burned before the reveal delay passed, since sealedData already has676// them regardless of whether TokenURI has started showing them677// publicly yet. Deliberately never renders or stores an image: the678// archive was only ever a text record (see BurnedTokenSummary/679// renderMintedListPage's own "Burned" row), so calling680// gemsart.RenderTokenSVG here just to immediately discard its output681// was pure waste -- confirmed live, a burn on an already-revealed token682// cost ~165M gas, almost entirely that one re-render, roughly the same683// cost the whole reveal-delay design exists to avoid paying on mint.684// Releases the trait combo AND the reveal bookkeeping (sealedData/685// mintHeight) -- maximizes the real storage-deposit refund, same686// reasoning the real gems6/stable's own Burn already documents.687func Burn(cur realm, tid grc721.TokenID) {688	caller := callerAddress()689	owner, err := token.OwnerOf(tid)690	checkErr(err)691	if caller != owner {692		panic(grc721.ErrCallerIsNotOwner)693	}694695	key := tid.String()696	archived, _ := sealedData.Get(key).(traitPreset)697698	if comboKey, ok := tokenComboKeys.Get(key).(string); ok {699		mintedTraitCombos.Remove(comboKey)700		tokenComboKeys.Remove(key)701	}702	sealedData.Remove(key)703	mintHeight.Remove(key)704705	checkErr(ledger.Burn(tid))706707	burnedRecords.Set(key, burnRecord{708		BurnedBy:   caller,709		BurnHeight: runtime.ChainHeight(),710		Traits:     archived,711	})712	burnedIDs = append(burnedIDs, tid)713}714715/* ------------------------------ gnoweb display ------------------------------ */716717func traitsToCSV(attrs []Trait) string {718	pairs := make([]string, len(attrs))719	for i, a := range attrs {720		pairs[i] = a.TraitType + "=" + a.Value721	}722	return strings.Join(pairs, ";")723}724725func TokenSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, false) }726727// thumbSummary is TokenSummary with the art's thumbnail render and the728// metadata built from the traits, so a page of them stays well under the729// query gas ceiling.730func thumbSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, true) }731732func tokenSummary(tid grc721.TokenID, thumb bool) (string, error) {733	owner, err := token.OwnerOf(tid)734	if err != nil {735		return "", err736	}737	var metadata Metadata738	if tp, ok := sealedData.Get(tid.String()).(traitPreset); thumb && ok && IsRevealed(tid) {739		metadata = traitOnlyMetadata(tp)740		svg := gemsart.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())741		metadata.Image = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))742	} else {743		metadata, err = currentMetadata(tid)744		if err != nil {745			return "", err746		}747	}748	enc := base64.StdEncoding749	fields := []string{750		tid.String(),751		owner.String(),752		enc.EncodeToString([]byte(metadata.Name)),753		enc.EncodeToString([]byte(metadata.Description)),754		enc.EncodeToString([]byte(metadata.Image)),755		"", // external URL: none756		enc.EncodeToString([]byte(traitsToCSV(metadata.Attributes))),757		enc.EncodeToString([]byte(metadata.BackgroundColor)),758	}759	return strings.Join(fields, "|"), nil760}761762func CollectionSummary(page int) string {763	total := len(mintedIDs)764	if page < 1 {765		page = 1766	}767	startK := (page - 1) * summaryPageSize768	endK := startK + summaryPageSize769	if endK > total {770		endK = total771	}772	var lines []string773	for k := startK; k < endK; k++ {774		tid := mintedIDs[total-1-k]775		line, err := thumbSummary(tid)776		if err != nil {777			continue778		}779		lines = append(lines, line)780	}781	return strings.Join(lines, "\n")782}783784// walletScanChunk bounds how many ids one WalletSummary call scans;785// walletRenderCap bounds how many of the caller's OWN tokens it renders.786// The second bound is the one that matters: every matched token is a787// full SVG render (TokenSummary -> currentMetadata -> gemsart), and one788// vm/qeval has a fixed gas budget -- a wallet holding 30 pieces made789// the unbounded version fail outright with "out of gas in location:790// CPUCycles" on Pearl (2026-09-16), taking the mint page's "Your791// minted Gems" panel down for exactly the collectors who care most.792// A client pages by calling again with the returned nextIdx until it793// reaches the collection size, same protocol as before; it just gets794// more, smaller pages for a big holder.795const (796	walletScanChunk = 100797	walletRenderCap = summaryPageSize798)799800func WalletSummary(owner address, startIdx int64) (summary string, nextIdx int64) {801	total := len(mintedIDs)802	start := int(startIdx)803	if start < 0 {804		start = 0805	}806	end := start + walletScanChunk807	if end > total {808		end = total809	}810	var lines []string811	k := start812	for ; k < end; k++ {813		if len(lines) >= walletRenderCap {814			break815		}816		tid := mintedIDs[total-1-k]817		tokenOwner, err := token.OwnerOf(tid)818		if err != nil || tokenOwner != owner {819			continue820		}821		line, err := thumbSummary(tid)822		if err != nil {823			continue824		}825		lines = append(lines, line)826	}827	return strings.Join(lines, "\n"), int64(k)828}829830func BurnedTokenSummary(tid grc721.TokenID) (string, error) {831	raw := burnedRecords.Get(tid.String())832	rec, ok := raw.(burnRecord)833	if !ok {834		return "", grc721.ErrInvalidTokenId835	}836	m := rec.metadata()837	enc := base64.StdEncoding838	fields := []string{839		tid.String(),840		rec.BurnedBy.String(),841		ufmt.Sprintf("%d", rec.BurnHeight),842		enc.EncodeToString([]byte(m.Name)),843		enc.EncodeToString([]byte(m.Description)),844		"", // image: a burn keeps none845		"", // external URL: none846		enc.EncodeToString([]byte(traitsToCSV(m.Attributes))),847		enc.EncodeToString([]byte(m.BackgroundColor)),848	}849	return strings.Join(fields, "|"), nil850}851852func bigTokenImage(image string) string {853	const prefix = "data:image/svg+xml;base64,"854	if !strings.HasPrefix(image, prefix) {855		return image856	}857	raw, err := base64.StdEncoding.DecodeString(image[len(prefix):])858	if err != nil {859		return image860	}861	svg := string(raw)862	resized := strings.Replace(svg, `width="170.00" height="170.00"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)863	if resized == svg {864		resized = strings.Replace(svg, `width="170" height="170"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)865	}866	return prefix + base64.StdEncoding.EncodeToString([]byte(resized))867}868869// shortAddr is how lists show an address: g1 + 4 characters … last 4.870func shortAddr(a string) string {871	if len(a) <= 14 {872		return a873	}874	return a[:6] + "…" + a[len(a)-4:]875}876877func renderMintedListPage(b *strings.Builder, page int) {878	total := len(mintedIDs)879	if total == 0 {880		return881	}882	totalPages := (total + gnowebListLimit - 1) / gnowebListLimit883	if page < 1 {884		page = 1885	}886	if page > totalPages {887		page = totalPages888	}889	startK := (page - 1) * gnowebListLimit890	endK := startK + gnowebListLimit891	if endK > total {892		endK = total893	}894895	b.WriteString("\n## Minted so far\n\n")896	if totalPages > 1 {897		b.WriteString(ufmt.Sprintf("_page %d of %d -- %d minted total_\n\n", page, totalPages, total))898	}899	// Image column centred (so is a burned row's "Burned"). Owners are900	// shortened to g1xxxx…xxxx: the full 40-character address made the901	// Owner column so wide that gnoweb broke "Index" over two lines. The902	// token's own page still shows the full address.903	b.WriteString("| Image | Index | Name | Owner |\n|:---:|:---:|---|---|\n")904	for k := startK; k < endK; k++ {905		tid := mintedIDs[total-1-k]906		link := realmRelPath + ":token/" + tid.String()907		owner, err := token.OwnerOf(tid)908		if err != nil {909			// Burned -- stays in its original chronological position910			// (never skipped: a listing that silently drops rows reads911			// as broken, not as "nothing to see here" -- confirmed live912			// against the real gems6/stable, where an all-burned page913			// rendered as a blank table with no explanation). Pulled914			// from the permanent burnedRecords archive, image always915			// empty by design (see Burn's own doc comment) so "Burned"916			// stands in for it. No wall-clock date is available inside917			// a gno.land realm (only block height) -- shown as that918			// instead of a fabricated date.919			raw := burnedRecords.Get(tid.String())920			rec, ok := raw.(burnRecord)921			if !ok {922				continue // shouldn't happen: burned but no archive record923			}924			name := rec.metadata().Name925			b.WriteString(ufmt.Sprintf("| Burned | [#%s](%s) | %s (burned at block %d) | %s |\n", tid.String(), link, name, rec.BurnHeight, shortAddr(rec.BurnedBy.String())))926			continue927		}928		name, image := listRow(tid)929		imgCell := ""930		if image != "" {931			imgCell = ufmt.Sprintf("[![%s](%s)](%s)", name, image, link)932		}933		b.WriteString(ufmt.Sprintf("| %s | [#%s](%s) | %s | %s |\n", imgCell, tid.String(), link, name, shortAddr(owner.String())))934	}935	b.WriteString("\n")936937	renderPageLinks(b, page, totalPages, "page")938}939940func renderPageLinks(b *strings.Builder, page, totalPages int, linkBase string) {941	if totalPages <= 1 {942		return943	}944	b.WriteString("\n")945	parts := []string{}946	if page > 1 {947		parts = append(parts, ufmt.Sprintf("[<< First](%s:%s/1)", realmRelPath, linkBase))948		parts = append(parts, ufmt.Sprintf("[<- newer](%s:%s/%d)", realmRelPath, linkBase, page-1))949	}950	lastShown := 0951	for p := 1; p <= totalPages; p++ {952		if p != 1 && p != totalPages && (p < page-pageWindow || p > page+pageWindow) {953			continue954		}955		if lastShown != 0 && p != lastShown+1 {956			parts = append(parts, "...")957		}958		if p == page {959			parts = append(parts, ufmt.Sprintf("**%d**", p))960		} else {961			parts = append(parts, ufmt.Sprintf("[%d](%s:%s/%d)", p, realmRelPath, linkBase, p))962		}963		lastShown = p964	}965	if page < totalPages {966		parts = append(parts, ufmt.Sprintf("[older ->](%s:%s/%d)", realmRelPath, linkBase, page+1))967		parts = append(parts, ufmt.Sprintf("[Last >>](%s:%s/%d)", realmRelPath, linkBase, totalPages))968	}969	b.WriteString(strings.Join(parts, " - "))970	b.WriteString("\n")971}972973func renderBurnedListPage(b *strings.Builder, page int) {974	total := len(burnedIDs)975	if total == 0 {976		return977	}978	totalPages := (total + gnowebListLimit - 1) / gnowebListLimit979	if page < 1 {980		page = 1981	}982	if page > totalPages {983		page = totalPages984	}985	startK := (page - 1) * gnowebListLimit986	endK := startK + gnowebListLimit987	if endK > total {988		endK = total989	}990991	b.WriteString("\n## Burned tokens\n\n")992	if totalPages > 1 {993		b.WriteString(ufmt.Sprintf("_page %d of %d -- %d burned total_\n\n", page, totalPages, total))994	}995	b.WriteString("| Index | Name | Burned By | Block |\n|:---:|---|---|---|\n")996	for k := startK; k < endK; k++ {997		tid := burnedIDs[total-1-k]998		raw := burnedRecords.Get(tid.String())999		rec, ok := raw.(burnRecord)1000		if !ok {1001			continue1002		}1003		name := rec.metadata().Name1004		b.WriteString(ufmt.Sprintf("| #%s | %s | %s | %d |\n", tid.String(), name, shortAddr(rec.BurnedBy.String()), rec.BurnHeight))1005	}1006	b.WriteString("\n")10071008	renderPageLinks(b, page, totalPages, "burned/page")1009}10101011// renderBurnedTokenPage is the token route for an id that no longer1012// exists. If it was burned, its archived record -- name, traits, who1013// burned it, at what block -- is shown: the minted list links every1014// burned row here, and stablev3 answered that link with a 404, which1015// read as a broken site (found in the 2026-09-15 Pearl test run).1016// Anything else really is a 404.1017func renderBurnedTokenPage(tid grc721.TokenID) string {1018	raw := burnedRecords.Get(tid.String())1019	rec, ok := raw.(burnRecord)1020	if !ok {1021		return render404()1022	}1023	m := rec.metadata()1024	name := m.Name1025	var b strings.Builder1026	b.WriteString(ufmt.Sprintf("# %s #%s (burned)\n\n[<- back to collection](%s)\n\n", CollectionName, tid.String(), realmRelPath))1027	b.WriteString("## Details\n\n")1028	b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", CollectionName))1029	b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))1030	b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))1031	if m.Description != "" {1032		b.WriteString(ufmt.Sprintf("* **Description**: %s\n", m.Description))1033	}1034	b.WriteString(ufmt.Sprintf("* **Status**: burned by %s at block %d\n", rec.BurnedBy, rec.BurnHeight))1035	b.WriteString(ufmt.Sprintf("* **Archive**: [all burned tokens](%s:burned)\n", realmRelPath))1036	if len(m.Attributes) > 0 {1037		b.WriteString("\n## Traits\n\n")1038		for _, attr := range m.Attributes {1039			b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))1040		}1041	}1042	return b.String()1043}10441045// gnotString: ugnot as GNOT, trailing zeros trimmed (100000000 -> "100",1046// 1500000 -> "1.5").1047func gnotString(ugnot int64) string {1048	whole, frac := ugnot/1_000_000, ugnot%1_000_0001049	if frac == 0 {1050		return strconv.FormatInt(whole, 10)1051	}1052	f := strconv.FormatInt(frac+1_000_000, 10)[1:]1053	return strconv.FormatInt(whole, 10) + "." + strings.TrimRight(f, "0")1054}10551056// bpsPercent: basis points as a percentage (500 -> "5%", 250 -> "2.5%").1057// ufmt ignores zero-padding, so "%d.%02d" printed 500 as "5.0%".1058func bpsPercent(bps int64) string {1059	if bps%100 == 0 {1060		return strconv.FormatInt(bps/100, 10) + "%"1061	}1062	f := strconv.FormatInt(bps%100+100, 10)[1:]1063	return strconv.FormatInt(bps/100, 10) + "." + strings.TrimRight(f, "0") + "%"1064}10651066func render404() string {1067	return ufmt.Sprintf("# 404\n\n[<- back to collection](%s)\n", realmRelPath)1068}10691070func Render(path string) string {1071	if path == "" {1072		var b strings.Builder1073		b.WriteString(token.RenderHome())1074		b.WriteString("\n## About\n\n")1075		b.WriteString(CollectionName + " are gemstones captured in an anti-gravity field. Each stone floats, suspended, at the heart of the ring around it -- gold, platinum, silver, bronze or obsidian -- which holds it in place and turns it into a display piece for the collector.\n\n")1076		b.WriteString("The collection is inspired by the names of gno.land's chains. Every piece's Chain trait names a gno.land chain generation, and its stone is that chain's own: Topaz, Sapphire and Pearl for the testnets that carry those names, Diamond for mainnet, and Quartz for the testnets without one. Only real cuts and colours; every image is drawn on-chain. Delayed-reveal mint: each piece's traits stay sealed for a few blocks after it is minted.\n\n")1077		b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", Owner()))1078		b.WriteString(ufmt.Sprintf("* **Assembled trait combinations minted**: %d\n", int64(mintedTraitCombos.Size())))1079		b.WriteString("* **Max supply**: unlimited\n")1080		if mintingOpenMirror {1081			b.WriteString("* **Minting**: open\n")1082		} else {1083			b.WriteString("* **Minting**: closed\n")1084		}1085		if mintPriceUgnot > 0 {1086			b.WriteString("* **Mint price**: " + gnotString(mintPriceUgnot) + " GNOT\n")1087		} else {1088			b.WriteString("* **Mint price**: free\n")1089		}1090		b.WriteString(ufmt.Sprintf("* **Reveal delay**: %d block(s)\n", RevealDelayBlocks))1091		if maxMintsPerWalletMirror > 0 {1092			b.WriteString(ufmt.Sprintf("* **Max mints per wallet**: %d\n", maxMintsPerWalletMirror))1093		} else {1094			b.WriteString("* **Max mints per wallet**: unlimited\n")1095		}1096		if royaltyBps > 0 {1097			b.WriteString("* **Secondary sale royalty**: " + bpsPercent(royaltyBps) + " (EIP-2981 RoyaltyInfo)\n")1098		}1099		if len(burnedIDs) > 0 {1100			b.WriteString(ufmt.Sprintf("* **Burned**: %d -- [view burned tokens ->](%s:burned)\n", len(burnedIDs), realmRelPath))1101		}11021103		renderMintedListPage(&b, 1)1104		return b.String()1105	}11061107	parts := strings.Split(path, "/")1108	if len(parts) == 2 && parts[0] == "page" {1109		page, err := strconv.Atoi(parts[1])1110		if err != nil || page < 1 {1111			return render404()1112		}1113		var b strings.Builder1114		b.WriteString(ufmt.Sprintf("# %s -- minted tokens\n\n[<- back to collection](%s)\n", CollectionName, realmRelPath))1115		renderMintedListPage(&b, page)1116		return b.String()1117	}1118	if parts[0] == "burned" {1119		page := 11120		if len(parts) == 3 && parts[1] == "page" {1121			p, err := strconv.Atoi(parts[2])1122			if err != nil || p < 1 {1123				return render404()1124			}1125			page = p1126		} else if len(parts) != 1 {1127			return render404()1128		}1129		var b strings.Builder1130		b.WriteString(ufmt.Sprintf("# %s -- burned tokens\n\n[<- back to collection](%s)\n", CollectionName, realmRelPath))1131		renderBurnedListPage(&b, page)1132		return b.String()1133	}1134	if len(parts) == 2 && parts[0] == "token" {1135		tid := grc721.TokenID(parts[1])1136		owner, err := token.OwnerOf(tid)1137		if err != nil {1138			return renderBurnedTokenPage(tid)1139		}1140		metadata, _ := currentMetadata(tid)1141		var b strings.Builder1142		b.WriteString(ufmt.Sprintf("# %s #%s\n\n[<- back to collection](%s)\n\n", CollectionName, tid.String(), realmRelPath))1143		image := metadata.Image1144		name := metadata.Name1145		if name == "" {1146			name = "Untitled"1147		}1148		if image != "" {1149			b.WriteString(ufmt.Sprintf("![%s](%s)\n\n", name, bigTokenImage(image)))1150		}11511152		b.WriteString("## Details\n\n")1153		b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", CollectionName))1154		b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))1155		b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))1156		if metadata.Description != "" {1157			b.WriteString(ufmt.Sprintf("* **Description**: %s\n", metadata.Description))1158		}1159		b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", owner))1160		b.WriteString(ufmt.Sprintf("* **Revealed**: %t (age %d block(s))\n", IsRevealed(tid), Age(tid)))11611162		if len(metadata.Attributes) > 0 {1163			b.WriteString("\n## Traits\n\n")1164			for _, attr := range metadata.Attributes {1165				b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))1166			}1167		}1168		return b.String()1169	}11701171	return render404()1172}11731174func checkErr(err error) {1175	if err != nil {1176		panic(err)1177	}1178}1179

Functions

  • AddToWhitelist(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}, wallet string, bps int64)

  • AddToWhitelistBatch(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}, walletsCSV string, bps int64)

  • Age(tid 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}, to string, tid string)

  • ApprovedVersion() string

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

  • Burn(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}, tid string)

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

  • ClaimAt(index int) (wallet string, bps int64, mintHeight_ int64, tid string)

  • ClaimCount() int64

  • CollectionSummary(page int) string

  • EffectivePrice(caller string) int64

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

  • GetName() string

  • GetSymbol() string

  • IsApprovedForAll(owner string, operator string) bool

  • IsRevealed(tid string) bool

  • IsTraitComboAvailable(comboKey string) bool

  • MaxMintsPerWallet() int64

  • MintedIDs() []gno.land/p/nt/grc721/v0.TokenID

  • MintingOpen() bool

  • MintPrice() int64

  • MintsByWallet(owner string) int64

  • Name() string

  • Owner() string

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

  • RecordMint(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, payer string, tid string, chainName string, shapeIdx int, colorIdx int, metal string, active bool, comboKey string, rarity string) string

  • RemoveFromWhitelist(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}, wallet string)

  • Render(path string) string

  • ReportMaxMintsPerWallet(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}, max int64)

  • ReportMintingOpen(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}, open bool)

  • RoyaltyBps() int64

  • RoyaltyInfo(tid string, salePrice int64) (string, int64, interface {Error func() string})

  • RoyaltyRecipient() string

  • SafeTransferFrom(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, tid string)

  • SetApprovalForAll(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}, operator string, approved bool)

  • SetApprovedVersion(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}, pkgpath string)

  • SetMintPrice(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}, ugnot int64)

  • SetRoyaltyBps(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}, bps int64)

  • SetRoyaltyRecipient(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}, recipient string)

  • Symbol() string

  • TokenCount() int64

  • TokenID() string

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

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

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

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

  • TotalBurned() 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, tid string)

  • WalletSummary(owner string, startIdx int64) (summary string, nextIdx int64)

  • WhitelistBps(wallet string) (bps int64, ok bool)

  • WhitelistCount() int64

  • WhitelistEntry(index int) (wallet string, bps 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.