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)", 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("\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}1179AddToWhitelist(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.
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.