1package stable23import (4 runtime "chain/runtime"5 unsaferealm "chain/runtime/unsafe"6 "encoding/base64"7 "strconv"8 "strings"910 "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/avl"11 grc721 "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/grc721v2"12 campaign "gno.land/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint/campaign"13 artpkg "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/art3d"14 "gno.land/p/nt/ufmt/v0"15)1617const (18 CollectionName = "Trial Mint"19 CollectionSymbol = "TRIAL"2021 realmRelPath = "/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint/stable"2223 summaryPageSize = 1024 gnowebListLimit = 1025 pageWindow = 226 tokenPageImageSize = "420"2728 RevealDelayBlocks = 529)3031var (32 nft *grc721.MetadataNFT3334 collectionOwner address = "g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4"3536 approvedVersion string3738 mintPriceUgnot int64 = 1000000000000 // 100 GNOT, per request3940 whitelistBps avl.Tree4142 royaltyBps int64 = 50043 royaltyRecipient address = "g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4"4445 mintedIDs []grc721.TokenID4647 mintedTraitCombos avl.Tree // combo key -> true48 tokenComboKeys avl.Tree // tid.String() -> comboKey string49 mintCountByWallet avl.Tree // address.String() -> int645051 mintingOpenMirror = true52 maxMintsPerWalletMirror int64 = 05354 burnedRecords avl.Tree // tid.String() -> burnRecord55 burnedIDs []grc721.TokenID5657 claimRecords []claimRecord5859 sealedData avl.Tree // tid.String() -> traitPreset60 mintHeight avl.Tree // tid.String() -> int646162 sealedImage string63)6465type traitPreset struct {66 Chain string67 ShapeIdx int68 ColorIdx int69 Metal string70 Active bool71 Rarity string // tier fixed at mint by the version realm (v5.rarityTier)72}7374type claimRecord struct {75 Wallet address76 Bps int6477 MintHeight int6478 Tid grc721.TokenID79}8081type burnRecord struct {82 BurnedBy address83 BurnHeight int6484 Metadata grc721.Metadata85}8687func init(cur realm) {88 nft = grc721.NewNFTWithMetadata(0, cur, CollectionName, CollectionSymbol)89 svg := `<svg xmlns="http://www.w3.org/2000/svg" width="170" height="170" viewBox="0 0 170 170">` +90 `<rect width="170" height="170" fill="#0a0b10"/>` +91 `<circle cx="85" cy="78" r="34" fill="none" stroke="#555" stroke-width="4"/>` +92 `<text x="85" y="83" font-size="12" text-anchor="middle" fill="#999" font-family="monospace">SEALED</text>` +93 `</svg>`94 sealedImage = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))95}9697func Name() string { return CollectionName }98func Symbol() string { return CollectionSymbol }99func TokenCount() int64 { return nft.TokenCount() }100101func OwnerOf(tid grc721.TokenID) (address, error) { return nft.OwnerOf(tid) }102func BalanceOf(owner address) (int64, error) { return nft.BalanceOf(owner) }103func GetApproved(tid grc721.TokenID) (address, error) { return nft.GetApproved(tid) }104func IsApprovedForAll(owner, operator address) bool { return nft.IsApprovedForAll(owner, operator) }105106func ApprovedVersion() string { return approvedVersion }107func Owner() address { return collectionOwner }108func MintPrice() int64 { return mintPriceUgnot }109110func MintedIDs() []grc721.TokenID { return mintedIDs }111func TotalBurned() int64 { return int64(len(burnedIDs)) }112113func MintingOpen() bool { return mintingOpenMirror }114func MaxMintsPerWallet() int64 { return maxMintsPerWalletMirror }115116func MintsByWallet(owner address) int64 {117 n, _ := mintCountByWallet.Get(owner.String()).(int64)118 return n119}120121func IsTraitComboAvailable(comboKey string) bool {122 return !mintedTraitCombos.Has(comboKey)123}124125func WhitelistBps(wallet address) (bps int64, ok bool) {126 if !whitelistBps.Has(wallet.String()) {127 return 0, false128 }129 return whitelistBps.Get(wallet.String()).(int64), true130}131132func WhitelistCount() int64 { return int64(whitelistBps.Size()) }133134func WhitelistEntry(index int) (wallet string, bps int64) {135 w, v := whitelistBps.GetByIndex(index)136 return w, v.(int64)137}138139func EffectivePrice(caller address) int64 {140 if v, ok := whitelistBps.Get(caller.String()).(int64); ok {141 return mintPriceUgnot * v / 10000142 }143 if bps, _, ok := campaign.Peek(caller); ok {144 return mintPriceUgnot * bps / 10000145 }146 return mintPriceUgnot147}148149func RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error) {150 if _, err := nft.OwnerOf(tid); err != nil {151 return "", 0, err152 }153 return royaltyRecipient, salePrice * royaltyBps / 10000, nil154}155156func RoyaltyBps() int64 { return royaltyBps }157func RoyaltyRecipient() address { return royaltyRecipient }158159func callerAddress() address {160 return unsaferealm.PreviousRealm().Address()161}162163func assertOwner() {164 if callerAddress() != collectionOwner {165 panic("unauthorized: caller is not the collection owner")166 }167}168169func SetApprovedVersion(cur realm, pkgpath string) {170 assertOwner()171 approvedVersion = pkgpath172}173174func SetMintPrice(cur realm, ugnot int64) {175 assertOwner()176 if ugnot < 0 {177 panic("price cannot be negative")178 }179 mintPriceUgnot = ugnot180}181182func AddToWhitelist(cur realm, wallet address, bps int64) {183 assertOwner()184 if bps < 0 || bps > 10000 {185 panic("bps must be between 0 and 10000")186 }187 whitelistBps.Set(wallet.String(), bps)188}189190func AddToWhitelistBatch(cur realm, walletsCSV string, bps int64) {191 assertOwner()192 if bps < 0 || bps > 10000 {193 panic("bps must be between 0 and 10000")194 }195 for _, w := range strings.Split(walletsCSV, ",") {196 w = strings.TrimSpace(w)197 if w == "" {198 continue199 }200 whitelistBps.Set(w, bps)201 }202}203204func RemoveFromWhitelist(cur realm, wallet address) {205 assertOwner()206 whitelistBps.Remove(wallet.String())207}208209func SetRoyaltyBps(cur realm, bps int64) {210 assertOwner()211 if bps < 0 || bps > 10000 {212 panic("bps must be between 0 and 10000")213 }214 royaltyBps = bps215}216217func SetRoyaltyRecipient(cur realm, recipient address) {218 assertOwner()219 royaltyRecipient = recipient220}221222func assertCallerIsApprovedVersion(cur realm) {223 caller := cur.Previous().PkgPath()224 if approvedVersion == "" {225 panic("no version realm has been approved yet")226 }227 if caller != approvedVersion {228 panic("unauthorized: caller is not the approved version realm")229 }230}231232func 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 {233 assertCallerIsApprovedVersion(cur)234 if mintedTraitCombos.Has(comboKey) {235 panic("trait combination already minted")236 }237 checkErr(nft.Mint(to, tid))238 checkErr(nft.SetTokenMetadata(to, tid, grc721.Metadata{239 Name: "Sealed",240 Attributes: []grc721.Trait{{TraitType: "Status", Value: "Sealed"}},241 }))242 mintedTraitCombos.Set(comboKey, true)243 tokenComboKeys.Set(tid.String(), comboKey)244 mintCountByWallet.Set(to.String(), MintsByWallet(to)+1)245 mintedIDs = append(mintedIDs, tid)246247 height := runtime.ChainHeight()248 mintHeight.Set(tid.String(), height)249 sealedData.Set(tid.String(), traitPreset{250 Chain: chainName, ShapeIdx: shapeIdx, ColorIdx: colorIdx, Metal: metal, Active: active, Rarity: rarity,251 })252253 if _, hasOverride := whitelistBps.Get(payer.String()).(int64); !hasOverride {254 if bps, ok := campaign.ConsumeDiscount(cross(cur), payer); ok {255 claimRecords = append(claimRecords, claimRecord{256 Wallet: payer,257 Bps: bps,258 MintHeight: height,259 Tid: tid,260 })261 }262 }263 return tid264}265266func ClaimCount() int64 { return int64(len(claimRecords)) }267268func ClaimAt(index int) (wallet address, bps int64, mintHeight_ int64, tid string) {269 r := claimRecords[index]270 return r.Wallet, r.Bps, r.MintHeight, r.Tid.String()271}272273func ReportMintingOpen(cur realm, open bool) {274 assertCallerIsApprovedVersion(cur)275 mintingOpenMirror = open276}277278func ReportMaxMintsPerWallet(cur realm, max int64) {279 assertCallerIsApprovedVersion(cur)280 maxMintsPerWalletMirror = max281}282283func Age(tid grc721.TokenID) int64 {284 h := mintHeight.Get(tid.String())285 if h == nil {286 return -1287 }288 return runtime.ChainHeight() - h.(int64)289}290291func IsRevealed(tid grc721.TokenID) bool {292 if _, isTraitPreset := sealedData.Get(tid.String()).(traitPreset); !isTraitPreset {293 return true294 }295 age := Age(tid)296 return age >= 0 && age >= RevealDelayBlocks297}298299func currentMetadata(tid grc721.TokenID) (grc721.Metadata, error) {300 stored, err := nft.TokenMetadata(tid)301 if err != nil {302 return stored, err303 }304 tp, hadReveal := sealedData.Get(tid.String()).(traitPreset)305 if !hadReveal {306 return stored, nil307 }308 if !IsRevealed(tid) {309 stored.Image = sealedImage // still sealed: shared placeholder, not per-token storage310 return stored, nil311 }312 return assembledMetadata(tid, tp), nil313}314315func listRow(tid grc721.TokenID) (name, image string) {316 tp, ok := sealedData.Get(tid.String()).(traitPreset)317 if !ok {318 m, _ := nft.TokenMetadata(tid)319 return m.Name, m.Image320 }321 if !IsRevealed(tid) {322 return "Sealed", sealedImage323 }324 name = traitOnlyMetadata(tp).Name325 svg := artpkg.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())326 return name, "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))327}328329func assembledMetadata(tid grc721.TokenID, tp traitPreset) grc721.Metadata {330 svg, stone, _, colorName := artpkg.RenderTokenSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())331 shapeName := artpkg.ShapeNames[tp.ShapeIdx]332 if stone == "Pearl" {333 shapeName = artpkg.PearlShapeNames[tp.ShapeIdx]334 }335 image := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))336 status := "Sunset"337 if tp.Active {338 status = "Active"339 }340 return grc721.Metadata{341 Name: pieceDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),342 Description: ufmt.Sprintf("%s %s %s set in a %s ring, minted on %s.", article(colorName), colorName, shapeName, tp.Metal, tp.Chain),343 Image: image,344 BackgroundColor: "0a0b10",345 Attributes: []grc721.Trait{346 {TraitType: "Chain", Value: tp.Chain},347 {TraitType: "Stone", Value: stone},348 {TraitType: "Shape", Value: shapeName},349 {TraitType: "Color", Value: colorName},350 {TraitType: "Ring", Value: tp.Metal},351 {TraitType: "Status", Value: status},352 {TraitType: "Rarity", Value: tp.Rarity},353 },354 }355}356357func article(word string) string {358 if word != "" && strings.ContainsRune("AEIOUaeiou", rune(word[0])) {359 return "An"360 }361 return "A"362}363364func pieceDisplayName(colorName, stone, shape, metal, chain string) string {365 piece := colorName + " " + stone + " " + shape366 if stone == "Quartz" || stone == "" {367 piece = colorName + " " + shape368 }369 return piece + " · " + metal + " Ring · " + chain370}371372func colorNameFor(stone string, colorIdx int) string {373 pairs := strings.Split(artpkg.ColorPaletteCSV(stone), ";")374 if colorIdx < 0 || colorIdx >= len(pairs) {375 return ""376 }377 parts := strings.SplitN(pairs[colorIdx], ":", 2)378 if len(parts) != 2 {379 return ""380 }381 return parts[1]382}383384func traitOnlyMetadata(tp traitPreset) grc721.Metadata {385 stone := artpkg.StoneForChain(tp.Chain)386 shapeName := artpkg.ShapeNames[tp.ShapeIdx]387 if stone == "Pearl" {388 shapeName = artpkg.PearlShapeNames[tp.ShapeIdx]389 }390 colorName := colorNameFor(stone, tp.ColorIdx)391 status := "Sunset"392 if tp.Active {393 status = "Active"394 }395 return grc721.Metadata{396 Name: pieceDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),397 Description: ufmt.Sprintf("%s %s %s set in a %s ring, minted on %s.", article(colorName), colorName, shapeName, tp.Metal, tp.Chain),398 Attributes: []grc721.Trait{399 {TraitType: "Chain", Value: tp.Chain},400 {TraitType: "Stone", Value: stone},401 {TraitType: "Shape", Value: shapeName},402 {TraitType: "Color", Value: colorName},403 {TraitType: "Ring", Value: tp.Metal},404 {TraitType: "Status", Value: status},405 {TraitType: "Rarity", Value: tp.Rarity},406 },407 }408}409410func TokenMetadata(tid grc721.TokenID) (grc721.Metadata, error) { return currentMetadata(tid) }411412func TokenURI(tid grc721.TokenID) (string, error) {413 metadata, err := currentMetadata(tid)414 if err != nil {415 return "", err416 }417 return metadataDataURI(metadata), nil418}419420func jsonEscape(s string) string {421 var b strings.Builder422 for i := 0; i < len(s); i++ {423 c := s[i]424 switch c {425 case '"':426 b.WriteString(`\"`)427 case '\\':428 b.WriteString(`\\`)429 default:430 b.WriteByte(c)431 }432 }433 return b.String()434}435436func metadataDataURI(m grc721.Metadata) string {437 image := m.Image438 if image == "" {439 image = m.ImageData440 }441 var b strings.Builder442 b.WriteString(`{"name":"`)443 b.WriteString(jsonEscape(m.Name))444 b.WriteString(`","description":"`)445 b.WriteString(jsonEscape(m.Description))446 b.WriteString(`","image":"`)447 b.WriteString(jsonEscape(image))448 b.WriteString(`"`)449 if m.BackgroundColor != "" {450 b.WriteString(`,"background_color":"`)451 b.WriteString(jsonEscape(m.BackgroundColor))452 b.WriteString(`"`)453 }454 b.WriteString(`,"attributes":[`)455 for i, t := range m.Attributes {456 if i > 0 {457 b.WriteString(",")458 }459 b.WriteString(`{"trait_type":"`)460 b.WriteString(jsonEscape(t.TraitType))461 b.WriteString(`","value":"`)462 b.WriteString(jsonEscape(t.Value))463 b.WriteString(`"}`)464 }465 b.WriteString("]}")466 return "data:application/json;base64," + base64.StdEncoding.EncodeToString([]byte(b.String()))467}468469func Approve(cur realm, to address, tid grc721.TokenID) {470 checkErr(nft.Approve(callerAddress(), to, tid))471}472473func SetApprovalForAll(cur realm, operator address, approved bool) {474 checkErr(nft.SetApprovalForAll(callerAddress(), operator, approved))475}476477func TransferFrom(cur realm, from, to address, tid grc721.TokenID) {478 checkErr(nft.TransferFrom(callerAddress(), from, to, tid))479}480481func SafeTransferFrom(cur realm, from, to address, tid grc721.TokenID) {482 checkErr(nft.SafeTransferFrom(callerAddress(), from, to, tid))483}484485func Burn(cur realm, tid grc721.TokenID) {486 caller := callerAddress()487 owner, err := nft.OwnerOf(tid)488 checkErr(err)489 if caller != owner {490 panic(grc721.ErrCallerIsNotOwner)491 }492493 key := tid.String()494 var archived grc721.Metadata495 if tp, ok := sealedData.Get(key).(traitPreset); ok {496 archived = traitOnlyMetadata(tp)497 }498499 if comboKey, ok := tokenComboKeys.Get(key).(string); ok {500 mintedTraitCombos.Remove(comboKey)501 tokenComboKeys.Remove(key)502 }503 sealedData.Remove(key)504 mintHeight.Remove(key)505506 checkErr(nft.Burn(tid))507508 burnedRecords.Set(key, burnRecord{509 BurnedBy: caller,510 BurnHeight: runtime.ChainHeight(),511 Metadata: archived,512 })513 burnedIDs = append(burnedIDs, tid)514}515516func traitsToCSV(attrs []grc721.Trait) string {517 pairs := make([]string, len(attrs))518 for i, a := range attrs {519 pairs[i] = a.TraitType + "=" + a.Value520 }521 return strings.Join(pairs, ";")522}523524func TokenSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, false) }525526func thumbSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, true) }527528func tokenSummary(tid grc721.TokenID, thumb bool) (string, error) {529 owner, err := nft.OwnerOf(tid)530 if err != nil {531 return "", err532 }533 var metadata grc721.Metadata534 if tp, ok := sealedData.Get(tid.String()).(traitPreset); thumb && ok && IsRevealed(tid) {535 stored, _ := nft.TokenMetadata(tid)536 metadata = traitOnlyMetadata(tp)537 metadata.ExternalURL = stored.ExternalURL538 metadata.BackgroundColor = "0a0b10"539 svg := artpkg.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())540 metadata.Image = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))541 } else {542 metadata, err = currentMetadata(tid)543 if err != nil {544 return "", err545 }546 }547 enc := base64.StdEncoding548 fields := []string{549 tid.String(),550 owner.String(),551 enc.EncodeToString([]byte(metadata.Name)),552 enc.EncodeToString([]byte(metadata.Description)),553 enc.EncodeToString([]byte(metadata.Image)),554 enc.EncodeToString([]byte(metadata.ExternalURL)),555 enc.EncodeToString([]byte(traitsToCSV(metadata.Attributes))),556 enc.EncodeToString([]byte(metadata.BackgroundColor)),557 }558 return strings.Join(fields, "|"), nil559}560561func CollectionSummary(page int) string {562 total := len(mintedIDs)563 if page < 1 {564 page = 1565 }566 startK := (page - 1) * summaryPageSize567 endK := startK + summaryPageSize568 if endK > total {569 endK = total570 }571 var lines []string572 for k := startK; k < endK; k++ {573 tid := mintedIDs[total-1-k]574 line, err := thumbSummary(tid)575 if err != nil {576 continue577 }578 lines = append(lines, line)579 }580 return strings.Join(lines, "\n")581}582583const (584 walletScanChunk = 100585 walletRenderCap = summaryPageSize586)587588func WalletSummary(owner address, startIdx int64) (summary string, nextIdx int64) {589 total := len(mintedIDs)590 start := int(startIdx)591 if start < 0 {592 start = 0593 }594 end := start + walletScanChunk595 if end > total {596 end = total597 }598 var lines []string599 k := start600 for ; k < end; k++ {601 if len(lines) >= walletRenderCap {602 break603 }604 tid := mintedIDs[total-1-k]605 tokenOwner, err := nft.OwnerOf(tid)606 if err != nil || tokenOwner != owner {607 continue608 }609 line, err := thumbSummary(tid)610 if err != nil {611 continue612 }613 lines = append(lines, line)614 }615 return strings.Join(lines, "\n"), int64(k)616}617618func BurnedTokenSummary(tid grc721.TokenID) (string, error) {619 raw := burnedRecords.Get(tid.String())620 rec, ok := raw.(burnRecord)621 if !ok {622 return "", grc721.ErrInvalidTokenId623 }624 enc := base64.StdEncoding625 fields := []string{626 tid.String(),627 rec.BurnedBy.String(),628 ufmt.Sprintf("%d", rec.BurnHeight),629 enc.EncodeToString([]byte(rec.Metadata.Name)),630 enc.EncodeToString([]byte(rec.Metadata.Description)),631 enc.EncodeToString([]byte(rec.Metadata.Image)),632 enc.EncodeToString([]byte(rec.Metadata.ExternalURL)),633 enc.EncodeToString([]byte(traitsToCSV(rec.Metadata.Attributes))),634 enc.EncodeToString([]byte(rec.Metadata.BackgroundColor)),635 }636 return strings.Join(fields, "|"), nil637}638639func bigTokenImage(image string) string {640 const prefix = "data:image/svg+xml;base64,"641 if !strings.HasPrefix(image, prefix) {642 return image643 }644 raw, err := base64.StdEncoding.DecodeString(image[len(prefix):])645 if err != nil {646 return image647 }648 svg := string(raw)649 resized := strings.Replace(svg, `width="170.00" height="170.00"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)650 if resized == svg {651 resized = strings.Replace(svg, `width="170" height="170"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)652 }653 return prefix + base64.StdEncoding.EncodeToString([]byte(resized))654}655656func renderMintedListPage(b *strings.Builder, page int) {657 total := len(mintedIDs)658 if total == 0 {659 return660 }661 totalPages := (total + gnowebListLimit - 1) / gnowebListLimit662 if page < 1 {663 page = 1664 }665 if page > totalPages {666 page = totalPages667 }668 startK := (page - 1) * gnowebListLimit669 endK := startK + gnowebListLimit670 if endK > total {671 endK = total672 }673674 b.WriteString("\n## Minted so far\n\n")675 if totalPages > 1 {676 b.WriteString(ufmt.Sprintf("_page %d of %d -- %d minted total_\n\n", page, totalPages, total))677 }678 b.WriteString("| Image | Index | Name | Owner |\n|---|---|---|---|\n")679 for k := startK; k < endK; k++ {680 tid := mintedIDs[total-1-k]681 link := realmRelPath + ":token/" + tid.String()682 owner, err := nft.OwnerOf(tid)683 if err != nil {684 raw := burnedRecords.Get(tid.String())685 rec, ok := raw.(burnRecord)686 if !ok {687 continue // shouldn't happen: burned but no archive record688 }689 name := rec.Metadata.Name690 if name == "" {691 name = "Untitled"692 }693 b.WriteString(ufmt.Sprintf("| Burned | [#%s](%s) | %s (burned at block %d) | %s |\n", tid.String(), link, name, rec.BurnHeight, rec.BurnedBy))694 continue695 }696 name, image := listRow(tid)697 imgCell := ""698 if image != "" {699 imgCell = ufmt.Sprintf("[](%s)", name, image, link)700 }701 b.WriteString(ufmt.Sprintf("| %s | [#%s](%s) | %s | %s |\n", imgCell, tid.String(), link, name, owner))702 }703 b.WriteString("\n")704705 renderPageLinks(b, page, totalPages, "page")706}707708func renderPageLinks(b *strings.Builder, page, totalPages int, linkBase string) {709 if totalPages <= 1 {710 return711 }712 b.WriteString("\n")713 parts := []string{}714 if page > 1 {715 parts = append(parts, ufmt.Sprintf("[<< First](%s:%s/1)", realmRelPath, linkBase))716 parts = append(parts, ufmt.Sprintf("[<- newer](%s:%s/%d)", realmRelPath, linkBase, page-1))717 }718 lastShown := 0719 for p := 1; p <= totalPages; p++ {720 if p != 1 && p != totalPages && (p < page-pageWindow || p > page+pageWindow) {721 continue722 }723 if lastShown != 0 && p != lastShown+1 {724 parts = append(parts, "...")725 }726 if p == page {727 parts = append(parts, ufmt.Sprintf("**%d**", p))728 } else {729 parts = append(parts, ufmt.Sprintf("[%d](%s:%s/%d)", p, realmRelPath, linkBase, p))730 }731 lastShown = p732 }733 if page < totalPages {734 parts = append(parts, ufmt.Sprintf("[older ->](%s:%s/%d)", realmRelPath, linkBase, page+1))735 parts = append(parts, ufmt.Sprintf("[Last >>](%s:%s/%d)", realmRelPath, linkBase, totalPages))736 }737 b.WriteString(strings.Join(parts, " - "))738 b.WriteString("\n")739}740741func renderBurnedListPage(b *strings.Builder, page int) {742 total := len(burnedIDs)743 if total == 0 {744 return745 }746 totalPages := (total + gnowebListLimit - 1) / gnowebListLimit747 if page < 1 {748 page = 1749 }750 if page > totalPages {751 page = totalPages752 }753 startK := (page - 1) * gnowebListLimit754 endK := startK + gnowebListLimit755 if endK > total {756 endK = total757 }758759 b.WriteString("\n## Burned tokens\n\n")760 if totalPages > 1 {761 b.WriteString(ufmt.Sprintf("_page %d of %d -- %d burned total_\n\n", page, totalPages, total))762 }763 b.WriteString("| Index | Name | Burned By | Block |\n|---|---|---|---|\n")764 for k := startK; k < endK; k++ {765 tid := burnedIDs[total-1-k]766 raw := burnedRecords.Get(tid.String())767 rec, ok := raw.(burnRecord)768 if !ok {769 continue770 }771 name := rec.Metadata.Name772 if name == "" {773 name = "Untitled"774 }775 b.WriteString(ufmt.Sprintf("| #%s | %s | %s | %d |\n", tid.String(), name, rec.BurnedBy, rec.BurnHeight))776 }777 b.WriteString("\n")778779 renderPageLinks(b, page, totalPages, "burned/page")780}781782func renderBurnedTokenPage(tid grc721.TokenID) string {783 raw := burnedRecords.Get(tid.String())784 rec, ok := raw.(burnRecord)785 if !ok {786 return render404()787 }788 name := rec.Metadata.Name789 if name == "" {790 name = "Untitled"791 }792 var b strings.Builder793 b.WriteString(ufmt.Sprintf("# %s #%s (burned)\n\n[<- back to collection](%s)\n\n", nft.Name(), tid.String(), realmRelPath))794 b.WriteString("## Details\n\n")795 b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", nft.Name()))796 b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))797 b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))798 if rec.Metadata.Description != "" {799 b.WriteString(ufmt.Sprintf("* **Description**: %s\n", rec.Metadata.Description))800 }801 b.WriteString(ufmt.Sprintf("* **Status**: burned by %s at block %d\n", rec.BurnedBy, rec.BurnHeight))802 b.WriteString(ufmt.Sprintf("* **Archive**: [all burned tokens](%s:burned)\n", realmRelPath))803 if len(rec.Metadata.Attributes) > 0 {804 b.WriteString("\n## Traits\n\n")805 for _, attr := range rec.Metadata.Attributes {806 b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))807 }808 }809 return b.String()810}811812func render404() string {813 return ufmt.Sprintf("# 404\n\n[<- back to collection](%s)\n", realmRelPath)814}815816func Render(path string) string {817 if path == "" {818 var b strings.Builder819 b.WriteString(nft.RenderHome())820 b.WriteString("\n## About\n\n")821 b.WriteString(CollectionName + ": each piece's Chain trait names a gno.land chain generation, and its stone is that chain's real stone -- Topaz, Sapphire, Pearl, Diamond for mainnet, Quartz for the testnets without one. Only real cuts and colours; every image is drawn on-chain. Delayed-reveal mint (real metadata sealed for a short delay after minting).\n\n")822 b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", Owner()))823 b.WriteString(ufmt.Sprintf("* **Assembled trait combinations minted**: %d\n", int64(mintedTraitCombos.Size())))824 b.WriteString("* **Max supply**: unlimited\n")825 if mintingOpenMirror {826 b.WriteString("* **Minting**: open\n")827 } else {828 b.WriteString("* **Minting**: closed\n")829 }830 if mintPriceUgnot > 0 {831 b.WriteString(ufmt.Sprintf("* **Mint price**: %d ugnot\n", mintPriceUgnot))832 } else {833 b.WriteString("* **Mint price**: free\n")834 }835 b.WriteString(ufmt.Sprintf("* **Reveal delay**: %d block(s)\n", RevealDelayBlocks))836 if maxMintsPerWalletMirror > 0 {837 b.WriteString(ufmt.Sprintf("* **Max mints per wallet**: %d\n", maxMintsPerWalletMirror))838 } else {839 b.WriteString("* **Max mints per wallet**: unlimited\n")840 }841 if royaltyBps > 0 {842 b.WriteString(ufmt.Sprintf("* **Secondary sale royalty**: %d.%02d%% (EIP-2981 RoyaltyInfo)\n", royaltyBps/100, royaltyBps%100))843 }844 if len(burnedIDs) > 0 {845 b.WriteString(ufmt.Sprintf("* **Burned**: %d -- [view burned tokens ->](%s:burned)\n", len(burnedIDs), realmRelPath))846 }847848 renderMintedListPage(&b, 1)849 return b.String()850 }851852 parts := strings.Split(path, "/")853 if len(parts) == 2 && parts[0] == "page" {854 page, err := strconv.Atoi(parts[1])855 if err != nil || page < 1 {856 return render404()857 }858 var b strings.Builder859 b.WriteString(ufmt.Sprintf("# %s -- minted tokens\n\n[<- back to collection](%s)\n", nft.Name(), realmRelPath))860 renderMintedListPage(&b, page)861 return b.String()862 }863 if parts[0] == "burned" {864 page := 1865 if len(parts) == 3 && parts[1] == "page" {866 p, err := strconv.Atoi(parts[2])867 if err != nil || p < 1 {868 return render404()869 }870 page = p871 } else if len(parts) != 1 {872 return render404()873 }874 var b strings.Builder875 b.WriteString(ufmt.Sprintf("# %s -- burned tokens\n\n[<- back to collection](%s)\n", nft.Name(), realmRelPath))876 renderBurnedListPage(&b, page)877 return b.String()878 }879 if len(parts) == 2 && parts[0] == "token" {880 tid := grc721.TokenID(parts[1])881 owner, err := nft.OwnerOf(tid)882 if err != nil {883 return renderBurnedTokenPage(tid)884 }885 metadata, _ := currentMetadata(tid)886 var b strings.Builder887 b.WriteString(ufmt.Sprintf("# %s #%s\n\n[<- back to collection](%s)\n\n", nft.Name(), tid.String(), realmRelPath))888 image := metadata.Image889 if image == "" {890 image = metadata.ImageData891 }892 name := metadata.Name893 if name == "" {894 name = "Untitled"895 }896 if image != "" {897 b.WriteString(ufmt.Sprintf("\n\n", name, bigTokenImage(image)))898 }899900 b.WriteString("## Details\n\n")901 b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", nft.Name()))902 b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))903 b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))904 if metadata.Description != "" {905 b.WriteString(ufmt.Sprintf("* **Description**: %s\n", metadata.Description))906 }907 b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", owner))908 b.WriteString(ufmt.Sprintf("* **Revealed**: %t (age %d block(s))\n", IsRevealed(tid), Age(tid)))909910 if len(metadata.Attributes) > 0 {911 b.WriteString("\n## Traits\n\n")912 for _, attr := range metadata.Attributes {913 b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))914 }915 }916 return b.String()917 }918919 return render404()920}921922func checkErr(err error) {923 if err != nil {924 panic(err)925 }926}927AddToWhitelist(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})
IsApprovedForAll(owner string, operator string) bool
IsRevealed(tid string) bool
IsTraitComboAvailable(comboKey string) bool
MaxMintsPerWallet() int64
MintedIDs() []gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/grc721v2.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
TokenMetadata(tid string) (struct{Image string; ImageData string; ExternalURL string; Description string; Name string; Attributes []gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/grc721v2.Trait; BackgroundColor string; AnimationURL string; YoutubeURL 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.