1package stable23import (4 runtime "chain/runtime"5 unsaferealm "chain/runtime/unsafe"6 "encoding/base64"7 "strconv"8 "strings"910 "gno.land/p/nt/avl/v0"11 "gno.land/p/nt/grc721/v0"12 "gno.land/p/nt/seqid/v0"13 campaign "gno.land/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint2/campaign"14 artpkg "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/art3d2"15 "gno.land/p/nt/ufmt/v0"16)1718const (19 CollectionName = "Trial Mint"20 CollectionSymbol = "TRIAL"2122 realmRelPath = "/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint2/stable"2324 summaryPageSize = 1025 gnowebListLimit = 1026 pageWindow = 227 tokenPageImageSize = "420"2829 RevealDelayBlocks = 530)3132var (33 token *grc721.Token34 ledger *grc721.PrivateLedger // private: only this realm mints, burns and moves tokens3536 collectionOwner address = "g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4"3738 approvedVersion string3940 mintPriceUgnot int64 = 1000000000000 // 100 GNOT, per request4142 whitelistBps avl.Tree4344 royaltyBps int64 = 50045 royaltyRecipient address = "g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4"4647 mintedIDs []grc721.TokenID4849 mintedTraitCombos avl.Tree // combo key -> true50 tokenComboKeys avl.Tree // tid.String() -> comboKey string51 mintCountByWallet avl.Tree // address.String() -> int645253 mintingOpenMirror = true54 maxMintsPerWalletMirror int64 = 05556 burnedRecords avl.Tree // tid.String() -> burnRecord57 burnedIDs []grc721.TokenID5859 claimRecords []claimRecord6061 sealedData avl.Tree // tid.String() -> traitPreset62 mintHeight avl.Tree // tid.String() -> int646364 sealedImage string65)6667type traitPreset struct {68 Chain string69 ShapeIdx int70 ColorIdx int71 Metal string72 Active bool73 Rarity string // tier fixed at mint by the version realm (v5.rarityTier)74}7576type claimRecord struct {77 Wallet address78 Bps int6479 MintHeight int6480 Tid grc721.TokenID81}8283type burnRecord struct {84 BurnedBy address85 BurnHeight int6486 Traits traitPreset87}8889func (r burnRecord) metadata() Metadata {90 if r.Traits.Chain == "" {91 return Metadata{Name: "Untitled"}92 }93 return traitOnlyMetadata(r.Traits)94}9596func init(cur realm) {97 token, ledger = grc721.NewToken(CollectionName, CollectionSymbol, seqid.ID(0), cur)98 svg := `<svg xmlns="http://www.w3.org/2000/svg" width="170" height="170" viewBox="0 0 170 170">` +99 `<rect width="170" height="170" fill="#0a0b10"/>` +100 `<circle cx="85" cy="78" r="34" fill="none" stroke="#555" stroke-width="4"/>` +101 `<text x="85" y="83" font-size="12" text-anchor="middle" fill="#999" font-family="monospace">SEALED</text>` +102 `</svg>`103 sealedImage = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))104}105106type Trait struct {107 TraitType string108 Value string109}110111type Metadata struct {112 Name string113 Description string114 Image string115 BackgroundColor string116 Attributes []Trait117}118119func Name() string { return CollectionName }120func Symbol() string { return CollectionSymbol }121func TokenCount() int64 { return token.TotalSupply() }122func TokenID() string { return token.ID() }123124func GetName() string { return CollectionName }125func GetSymbol() string { return CollectionSymbol }126127func OwnerOf(tid grc721.TokenID) (address, error) { return token.OwnerOf(tid) }128func BalanceOf(owner address) (int64, error) { return token.BalanceOf(owner) }129func GetApproved(tid grc721.TokenID) (address, error) { return token.GetApproved(tid) }130func IsApprovedForAll(owner, operator address) bool { return token.IsApprovedForAll(owner, operator) }131132func ApprovedVersion() string { return approvedVersion }133func Owner() address { return collectionOwner }134func MintPrice() int64 { return mintPriceUgnot }135136func MintedIDs() []grc721.TokenID { return mintedIDs }137func TotalBurned() int64 { return int64(len(burnedIDs)) }138139func MintingOpen() bool { return mintingOpenMirror }140func MaxMintsPerWallet() int64 { return maxMintsPerWalletMirror }141142func MintsByWallet(owner address) int64 {143 n, _ := mintCountByWallet.Get(owner.String()).(int64)144 return n145}146147func IsTraitComboAvailable(comboKey string) bool {148 return !mintedTraitCombos.Has(comboKey)149}150151func WhitelistBps(wallet address) (bps int64, ok bool) {152 if !whitelistBps.Has(wallet.String()) {153 return 0, false154 }155 return whitelistBps.Get(wallet.String()).(int64), true156}157158func WhitelistCount() int64 { return int64(whitelistBps.Size()) }159160func WhitelistEntry(index int) (wallet string, bps int64) {161 w, v := whitelistBps.GetByIndex(index)162 return w, v.(int64)163}164165func EffectivePrice(caller address) int64 {166 if v, ok := whitelistBps.Get(caller.String()).(int64); ok {167 return mintPriceUgnot * v / 10000168 }169 if bps, _, ok := campaign.Peek(caller); ok {170 return mintPriceUgnot * bps / 10000171 }172 return mintPriceUgnot173}174175func RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error) {176 if _, err := token.OwnerOf(tid); err != nil {177 return "", 0, err178 }179 return royaltyRecipient, salePrice * royaltyBps / 10000, nil180}181182func RoyaltyBps() int64 { return royaltyBps }183func RoyaltyRecipient() address { return royaltyRecipient }184185func callerAddress() address {186 return unsaferealm.PreviousRealm().Address()187}188189func assertOwner() {190 if callerAddress() != collectionOwner {191 panic("unauthorized: caller is not the collection owner")192 }193}194195func SetApprovedVersion(cur realm, pkgpath string) {196 assertOwner()197 approvedVersion = pkgpath198}199200func SetMintPrice(cur realm, ugnot int64) {201 assertOwner()202 if ugnot < 0 {203 panic("price cannot be negative")204 }205 mintPriceUgnot = ugnot206}207208func AddToWhitelist(cur realm, wallet address, bps int64) {209 assertOwner()210 if bps < 0 || bps > 10000 {211 panic("bps must be between 0 and 10000")212 }213 whitelistBps.Set(wallet.String(), bps)214}215216func AddToWhitelistBatch(cur realm, walletsCSV string, bps int64) {217 assertOwner()218 if bps < 0 || bps > 10000 {219 panic("bps must be between 0 and 10000")220 }221 for _, w := range strings.Split(walletsCSV, ",") {222 w = strings.TrimSpace(w)223 if w == "" {224 continue225 }226 whitelistBps.Set(w, bps)227 }228}229230func RemoveFromWhitelist(cur realm, wallet address) {231 assertOwner()232 whitelistBps.Remove(wallet.String())233}234235func SetRoyaltyBps(cur realm, bps int64) {236 assertOwner()237 if bps < 0 || bps > 10000 {238 panic("bps must be between 0 and 10000")239 }240 royaltyBps = bps241}242243func SetRoyaltyRecipient(cur realm, recipient address) {244 assertOwner()245 royaltyRecipient = recipient246}247248func assertCallerIsApprovedVersion(cur realm) {249 caller := cur.Previous().PkgPath()250 if approvedVersion == "" {251 panic("no version realm has been approved yet")252 }253 if caller != approvedVersion {254 panic("unauthorized: caller is not the approved version realm")255 }256}257258func 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 {259 assertCallerIsApprovedVersion(cur)260 if mintedTraitCombos.Has(comboKey) {261 panic("trait combination already minted")262 }263 checkErr(ledger.Mint(to, tid))264 mintedTraitCombos.Set(comboKey, true)265 tokenComboKeys.Set(tid.String(), comboKey)266 mintCountByWallet.Set(to.String(), MintsByWallet(to)+1)267 mintedIDs = append(mintedIDs, tid)268269 height := runtime.ChainHeight()270 mintHeight.Set(tid.String(), height)271 sealedData.Set(tid.String(), traitPreset{272 Chain: chainName, ShapeIdx: shapeIdx, ColorIdx: colorIdx, Metal: metal, Active: active, Rarity: rarity,273 })274275 if _, hasOverride := whitelistBps.Get(payer.String()).(int64); !hasOverride {276 if bps, ok := campaign.ConsumeDiscount(cross(cur), payer); ok {277 claimRecords = append(claimRecords, claimRecord{278 Wallet: payer,279 Bps: bps,280 MintHeight: height,281 Tid: tid,282 })283 }284 }285 return tid286}287288func ClaimCount() int64 { return int64(len(claimRecords)) }289290func ClaimAt(index int) (wallet address, bps int64, mintHeight_ int64, tid string) {291 r := claimRecords[index]292 return r.Wallet, r.Bps, r.MintHeight, r.Tid.String()293}294295func ReportMintingOpen(cur realm, open bool) {296 assertCallerIsApprovedVersion(cur)297 mintingOpenMirror = open298}299300func ReportMaxMintsPerWallet(cur realm, max int64) {301 assertCallerIsApprovedVersion(cur)302 maxMintsPerWalletMirror = max303}304305func Age(tid grc721.TokenID) int64 {306 h := mintHeight.Get(tid.String())307 if h == nil {308 return -1309 }310 return runtime.ChainHeight() - h.(int64)311}312313func IsRevealed(tid grc721.TokenID) bool {314 if _, isTraitPreset := sealedData.Get(tid.String()).(traitPreset); !isTraitPreset {315 return true316 }317 age := Age(tid)318 return age >= 0 && age >= RevealDelayBlocks319}320321func currentMetadata(tid grc721.TokenID) (Metadata, error) {322 if _, err := token.OwnerOf(tid); err != nil {323 return Metadata{}, err324 }325 tp, ok := sealedData.Get(tid.String()).(traitPreset)326 if !ok {327 return Metadata{}, grc721.ErrInvalidTokenId328 }329 if !IsRevealed(tid) {330 return sealedMetadata(), nil331 }332 return assembledMetadata(tid, tp), nil333}334335func sealedMetadata() Metadata {336 return Metadata{Name: "Sealed", Image: sealedImage, BackgroundColor: "0a0b10", Attributes: []Trait{{TraitType: "Status", Value: "Sealed"}}}337}338339func listRow(tid grc721.TokenID) (name, image string) {340 tp, ok := sealedData.Get(tid.String()).(traitPreset)341 if !ok {342 return "Untitled", ""343 }344 if !IsRevealed(tid) {345 return "Sealed", sealedImage346 }347 name = traitOnlyMetadata(tp).Name348 svg := artpkg.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())349 return name, "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))350}351352func assembledMetadata(tid grc721.TokenID, tp traitPreset) Metadata {353 svg, stone, _, colorName := artpkg.RenderTokenSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())354 shapeName := artpkg.ShapeNames[tp.ShapeIdx]355 if stone == "Pearl" {356 shapeName = artpkg.PearlShapeNames[tp.ShapeIdx]357 }358 image := "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))359 status := "Sunset"360 if tp.Active {361 status = "Active"362 }363 return Metadata{364 Name: pieceDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),365 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),366 Image: image,367 BackgroundColor: "0a0b10",368 Attributes: []Trait{369 {TraitType: "Chain", Value: tp.Chain},370 {TraitType: "Stone", Value: stone},371 {TraitType: "Shape", Value: shapeName},372 {TraitType: "Color", Value: colorName},373 {TraitType: "Ring", Value: tp.Metal},374 {TraitType: "Status", Value: status},375 {TraitType: "Rarity", Value: tp.Rarity},376 },377 }378}379380func article(word string) string {381 if word != "" && strings.ContainsRune("AEIOUaeiou", rune(word[0])) {382 return "An"383 }384 return "A"385}386387func pieceDisplayName(colorName, stone, shape, metal, chain string) string {388 piece := colorName + " " + stone + " " + shape389 if stone == "Quartz" || stone == "" {390 piece = colorName + " " + shape391 }392 return piece + " · " + metal + " Ring · " + chain393}394395func colorNameFor(stone string, colorIdx int) string {396 pairs := strings.Split(artpkg.ColorPaletteCSV(stone), ";")397 if colorIdx < 0 || colorIdx >= len(pairs) {398 return ""399 }400 parts := strings.SplitN(pairs[colorIdx], ":", 2)401 if len(parts) != 2 {402 return ""403 }404 return parts[1]405}406407func traitOnlyMetadata(tp traitPreset) Metadata {408 stone := artpkg.StoneForChain(tp.Chain)409 shapeName := artpkg.ShapeNames[tp.ShapeIdx]410 if stone == "Pearl" {411 shapeName = artpkg.PearlShapeNames[tp.ShapeIdx]412 }413 colorName := colorNameFor(stone, tp.ColorIdx)414 status := "Sunset"415 if tp.Active {416 status = "Active"417 }418 return Metadata{419 Name: pieceDisplayName(colorName, stone, shapeName, tp.Metal, tp.Chain),420 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),421 BackgroundColor: "0a0b10",422 Attributes: []Trait{423 {TraitType: "Chain", Value: tp.Chain},424 {TraitType: "Stone", Value: stone},425 {TraitType: "Shape", Value: shapeName},426 {TraitType: "Color", Value: colorName},427 {TraitType: "Ring", Value: tp.Metal},428 {TraitType: "Status", Value: status},429 {TraitType: "Rarity", Value: tp.Rarity},430 },431 }432}433434func TokenURI(tid grc721.TokenID) (string, error) {435 metadata, err := currentMetadata(tid)436 if err != nil {437 return "", err438 }439 return metadata.Image, nil440}441442func TokenMetadata(tid grc721.TokenID) (string, error) {443 if _, err := token.OwnerOf(tid); err != nil {444 return "", err445 }446 m := sealedMetadata()447 if tp, ok := sealedData.Get(tid.String()).(traitPreset); ok && IsRevealed(tid) {448 m = traitOnlyMetadata(tp)449 }450 m.Image = ""451 return metadataJSON(m, "traitType", "backgroundColor"), nil452}453454func TokenJSON(tid grc721.TokenID) (string, error) {455 metadata, err := currentMetadata(tid)456 if err != nil {457 return "", err458 }459 return "data:application/json;base64," + base64.StdEncoding.EncodeToString([]byte(metadataJSON(metadata, "trait_type", "background_color"))), nil460}461462func jsonEscape(s string) string {463 var b strings.Builder464 for i := 0; i < len(s); i++ {465 c := s[i]466 switch c {467 case '"':468 b.WriteString(`\"`)469 case '\\':470 b.WriteString(`\\`)471 default:472 b.WriteByte(c)473 }474 }475 return b.String()476}477478func metadataJSON(m Metadata, traitKey, bgKey string) string {479 var b strings.Builder480 b.WriteString(`{"name":"`)481 b.WriteString(jsonEscape(m.Name))482 b.WriteString(`","description":"`)483 b.WriteString(jsonEscape(m.Description))484 b.WriteString(`"`)485 if m.Image != "" {486 b.WriteString(`,"image":"`)487 b.WriteString(jsonEscape(m.Image))488 b.WriteString(`"`)489 }490 if m.BackgroundColor != "" {491 b.WriteString(`,"` + bgKey + `":"`)492 b.WriteString(jsonEscape(m.BackgroundColor))493 b.WriteString(`"`)494 }495 b.WriteString(`,"attributes":[`)496 for i, t := range m.Attributes {497 if i > 0 {498 b.WriteString(",")499 }500 b.WriteString(`{"` + traitKey + `":"`)501 b.WriteString(jsonEscape(t.TraitType))502 b.WriteString(`","value":"`)503 b.WriteString(jsonEscape(t.Value))504 b.WriteString(`"}`)505 }506 b.WriteString("]}")507 return b.String()508}509510func Approve(cur realm, to address, tid grc721.TokenID) {511 checkErr(ledger.Approve(callerAddress(), to, tid))512}513514func SetApprovalForAll(cur realm, operator address, approved bool) {515 checkErr(ledger.SetApprovalForAll(callerAddress(), operator, approved))516}517518func TransferFrom(cur realm, from, to address, tid grc721.TokenID) {519 checkErr(ledger.TransferFrom(callerAddress(), from, to, tid))520}521522func SafeTransferFrom(cur realm, from, to address, tid grc721.TokenID) {523 checkErr(ledger.TransferFrom(callerAddress(), from, to, tid))524}525526func Burn(cur realm, tid grc721.TokenID) {527 caller := callerAddress()528 owner, err := token.OwnerOf(tid)529 checkErr(err)530 if caller != owner {531 panic(grc721.ErrCallerIsNotOwner)532 }533534 key := tid.String()535 archived, _ := sealedData.Get(key).(traitPreset)536537 if comboKey, ok := tokenComboKeys.Get(key).(string); ok {538 mintedTraitCombos.Remove(comboKey)539 tokenComboKeys.Remove(key)540 }541 sealedData.Remove(key)542 mintHeight.Remove(key)543544 checkErr(ledger.Burn(tid))545546 burnedRecords.Set(key, burnRecord{547 BurnedBy: caller,548 BurnHeight: runtime.ChainHeight(),549 Traits: archived,550 })551 burnedIDs = append(burnedIDs, tid)552}553554func traitsToCSV(attrs []Trait) string {555 pairs := make([]string, len(attrs))556 for i, a := range attrs {557 pairs[i] = a.TraitType + "=" + a.Value558 }559 return strings.Join(pairs, ";")560}561562func TokenSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, false) }563564func thumbSummary(tid grc721.TokenID) (string, error) { return tokenSummary(tid, true) }565566func tokenSummary(tid grc721.TokenID, thumb bool) (string, error) {567 owner, err := token.OwnerOf(tid)568 if err != nil {569 return "", err570 }571 var metadata Metadata572 if tp, ok := sealedData.Get(tid.String()).(traitPreset); thumb && ok && IsRevealed(tid) {573 metadata = traitOnlyMetadata(tp)574 svg := artpkg.RenderTokenThumbSVG(tp.Chain, tp.Metal, tp.ShapeIdx, tp.ColorIdx, tp.Active, tid.String())575 metadata.Image = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg))576 } else {577 metadata, err = currentMetadata(tid)578 if err != nil {579 return "", err580 }581 }582 enc := base64.StdEncoding583 fields := []string{584 tid.String(),585 owner.String(),586 enc.EncodeToString([]byte(metadata.Name)),587 enc.EncodeToString([]byte(metadata.Description)),588 enc.EncodeToString([]byte(metadata.Image)),589 "", // external URL: none590 enc.EncodeToString([]byte(traitsToCSV(metadata.Attributes))),591 enc.EncodeToString([]byte(metadata.BackgroundColor)),592 }593 return strings.Join(fields, "|"), nil594}595596func CollectionSummary(page int) string {597 total := len(mintedIDs)598 if page < 1 {599 page = 1600 }601 startK := (page - 1) * summaryPageSize602 endK := startK + summaryPageSize603 if endK > total {604 endK = total605 }606 var lines []string607 for k := startK; k < endK; k++ {608 tid := mintedIDs[total-1-k]609 line, err := thumbSummary(tid)610 if err != nil {611 continue612 }613 lines = append(lines, line)614 }615 return strings.Join(lines, "\n")616}617618const (619 walletScanChunk = 100620 walletRenderCap = summaryPageSize621)622623func WalletSummary(owner address, startIdx int64) (summary string, nextIdx int64) {624 total := len(mintedIDs)625 start := int(startIdx)626 if start < 0 {627 start = 0628 }629 end := start + walletScanChunk630 if end > total {631 end = total632 }633 var lines []string634 k := start635 for ; k < end; k++ {636 if len(lines) >= walletRenderCap {637 break638 }639 tid := mintedIDs[total-1-k]640 tokenOwner, err := token.OwnerOf(tid)641 if err != nil || tokenOwner != owner {642 continue643 }644 line, err := thumbSummary(tid)645 if err != nil {646 continue647 }648 lines = append(lines, line)649 }650 return strings.Join(lines, "\n"), int64(k)651}652653func BurnedTokenSummary(tid grc721.TokenID) (string, error) {654 raw := burnedRecords.Get(tid.String())655 rec, ok := raw.(burnRecord)656 if !ok {657 return "", grc721.ErrInvalidTokenId658 }659 m := rec.metadata()660 enc := base64.StdEncoding661 fields := []string{662 tid.String(),663 rec.BurnedBy.String(),664 ufmt.Sprintf("%d", rec.BurnHeight),665 enc.EncodeToString([]byte(m.Name)),666 enc.EncodeToString([]byte(m.Description)),667 "", // image: a burn keeps none668 "", // external URL: none669 enc.EncodeToString([]byte(traitsToCSV(m.Attributes))),670 enc.EncodeToString([]byte(m.BackgroundColor)),671 }672 return strings.Join(fields, "|"), nil673}674675func bigTokenImage(image string) string {676 const prefix = "data:image/svg+xml;base64,"677 if !strings.HasPrefix(image, prefix) {678 return image679 }680 raw, err := base64.StdEncoding.DecodeString(image[len(prefix):])681 if err != nil {682 return image683 }684 svg := string(raw)685 resized := strings.Replace(svg, `width="170.00" height="170.00"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)686 if resized == svg {687 resized = strings.Replace(svg, `width="170" height="170"`, `width="`+tokenPageImageSize+`" height="`+tokenPageImageSize+`"`, 1)688 }689 return prefix + base64.StdEncoding.EncodeToString([]byte(resized))690}691692func shortAddr(a string) string {693 if len(a) <= 14 {694 return a695 }696 return a[:6] + "…" + a[len(a)-4:]697}698699func renderMintedListPage(b *strings.Builder, page int) {700 total := len(mintedIDs)701 if total == 0 {702 return703 }704 totalPages := (total + gnowebListLimit - 1) / gnowebListLimit705 if page < 1 {706 page = 1707 }708 if page > totalPages {709 page = totalPages710 }711 startK := (page - 1) * gnowebListLimit712 endK := startK + gnowebListLimit713 if endK > total {714 endK = total715 }716717 b.WriteString("\n## Minted so far\n\n")718 if totalPages > 1 {719 b.WriteString(ufmt.Sprintf("_page %d of %d -- %d minted total_\n\n", page, totalPages, total))720 }721 b.WriteString("| Image | Index | Name | Owner |\n|:---:|:---:|---|---|\n")722 for k := startK; k < endK; k++ {723 tid := mintedIDs[total-1-k]724 link := realmRelPath + ":token/" + tid.String()725 owner, err := token.OwnerOf(tid)726 if err != nil {727 raw := burnedRecords.Get(tid.String())728 rec, ok := raw.(burnRecord)729 if !ok {730 continue // shouldn't happen: burned but no archive record731 }732 name := rec.metadata().Name733 b.WriteString(ufmt.Sprintf("| Burned | [#%s](%s) | %s (burned at block %d) | %s |\n", tid.String(), link, name, rec.BurnHeight, shortAddr(rec.BurnedBy.String())))734 continue735 }736 name, image := listRow(tid)737 imgCell := ""738 if image != "" {739 imgCell = ufmt.Sprintf("[](%s)", name, image, link)740 }741 b.WriteString(ufmt.Sprintf("| %s | [#%s](%s) | %s | %s |\n", imgCell, tid.String(), link, name, shortAddr(owner.String())))742 }743 b.WriteString("\n")744745 renderPageLinks(b, page, totalPages, "page")746}747748func renderPageLinks(b *strings.Builder, page, totalPages int, linkBase string) {749 if totalPages <= 1 {750 return751 }752 b.WriteString("\n")753 parts := []string{}754 if page > 1 {755 parts = append(parts, ufmt.Sprintf("[<< First](%s:%s/1)", realmRelPath, linkBase))756 parts = append(parts, ufmt.Sprintf("[<- newer](%s:%s/%d)", realmRelPath, linkBase, page-1))757 }758 lastShown := 0759 for p := 1; p <= totalPages; p++ {760 if p != 1 && p != totalPages && (p < page-pageWindow || p > page+pageWindow) {761 continue762 }763 if lastShown != 0 && p != lastShown+1 {764 parts = append(parts, "...")765 }766 if p == page {767 parts = append(parts, ufmt.Sprintf("**%d**", p))768 } else {769 parts = append(parts, ufmt.Sprintf("[%d](%s:%s/%d)", p, realmRelPath, linkBase, p))770 }771 lastShown = p772 }773 if page < totalPages {774 parts = append(parts, ufmt.Sprintf("[older ->](%s:%s/%d)", realmRelPath, linkBase, page+1))775 parts = append(parts, ufmt.Sprintf("[Last >>](%s:%s/%d)", realmRelPath, linkBase, totalPages))776 }777 b.WriteString(strings.Join(parts, " - "))778 b.WriteString("\n")779}780781func renderBurnedListPage(b *strings.Builder, page int) {782 total := len(burnedIDs)783 if total == 0 {784 return785 }786 totalPages := (total + gnowebListLimit - 1) / gnowebListLimit787 if page < 1 {788 page = 1789 }790 if page > totalPages {791 page = totalPages792 }793 startK := (page - 1) * gnowebListLimit794 endK := startK + gnowebListLimit795 if endK > total {796 endK = total797 }798799 b.WriteString("\n## Burned tokens\n\n")800 if totalPages > 1 {801 b.WriteString(ufmt.Sprintf("_page %d of %d -- %d burned total_\n\n", page, totalPages, total))802 }803 b.WriteString("| Index | Name | Burned By | Block |\n|:---:|---|---|---|\n")804 for k := startK; k < endK; k++ {805 tid := burnedIDs[total-1-k]806 raw := burnedRecords.Get(tid.String())807 rec, ok := raw.(burnRecord)808 if !ok {809 continue810 }811 name := rec.metadata().Name812 b.WriteString(ufmt.Sprintf("| #%s | %s | %s | %d |\n", tid.String(), name, shortAddr(rec.BurnedBy.String()), rec.BurnHeight))813 }814 b.WriteString("\n")815816 renderPageLinks(b, page, totalPages, "burned/page")817}818819func renderBurnedTokenPage(tid grc721.TokenID) string {820 raw := burnedRecords.Get(tid.String())821 rec, ok := raw.(burnRecord)822 if !ok {823 return render404()824 }825 m := rec.metadata()826 name := m.Name827 var b strings.Builder828 b.WriteString(ufmt.Sprintf("# %s #%s (burned)\n\n[<- back to collection](%s)\n\n", CollectionName, tid.String(), realmRelPath))829 b.WriteString("## Details\n\n")830 b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", CollectionName))831 b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))832 b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))833 if m.Description != "" {834 b.WriteString(ufmt.Sprintf("* **Description**: %s\n", m.Description))835 }836 b.WriteString(ufmt.Sprintf("* **Status**: burned by %s at block %d\n", rec.BurnedBy, rec.BurnHeight))837 b.WriteString(ufmt.Sprintf("* **Archive**: [all burned tokens](%s:burned)\n", realmRelPath))838 if len(m.Attributes) > 0 {839 b.WriteString("\n## Traits\n\n")840 for _, attr := range m.Attributes {841 b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))842 }843 }844 return b.String()845}846847func render404() string {848 return ufmt.Sprintf("# 404\n\n[<- back to collection](%s)\n", realmRelPath)849}850851func Render(path string) string {852 if path == "" {853 var b strings.Builder854 b.WriteString(token.RenderHome())855 b.WriteString("\n## About\n\n")856 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")857 b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", Owner()))858 b.WriteString(ufmt.Sprintf("* **Assembled trait combinations minted**: %d\n", int64(mintedTraitCombos.Size())))859 b.WriteString("* **Max supply**: unlimited\n")860 if mintingOpenMirror {861 b.WriteString("* **Minting**: open\n")862 } else {863 b.WriteString("* **Minting**: closed\n")864 }865 if mintPriceUgnot > 0 {866 b.WriteString(ufmt.Sprintf("* **Mint price**: %d ugnot\n", mintPriceUgnot))867 } else {868 b.WriteString("* **Mint price**: free\n")869 }870 b.WriteString(ufmt.Sprintf("* **Reveal delay**: %d block(s)\n", RevealDelayBlocks))871 if maxMintsPerWalletMirror > 0 {872 b.WriteString(ufmt.Sprintf("* **Max mints per wallet**: %d\n", maxMintsPerWalletMirror))873 } else {874 b.WriteString("* **Max mints per wallet**: unlimited\n")875 }876 if royaltyBps > 0 {877 b.WriteString(ufmt.Sprintf("* **Secondary sale royalty**: %d.%02d%% (EIP-2981 RoyaltyInfo)\n", royaltyBps/100, royaltyBps%100))878 }879 if len(burnedIDs) > 0 {880 b.WriteString(ufmt.Sprintf("* **Burned**: %d -- [view burned tokens ->](%s:burned)\n", len(burnedIDs), realmRelPath))881 }882883 renderMintedListPage(&b, 1)884 return b.String()885 }886887 parts := strings.Split(path, "/")888 if len(parts) == 2 && parts[0] == "page" {889 page, err := strconv.Atoi(parts[1])890 if err != nil || page < 1 {891 return render404()892 }893 var b strings.Builder894 b.WriteString(ufmt.Sprintf("# %s -- minted tokens\n\n[<- back to collection](%s)\n", CollectionName, realmRelPath))895 renderMintedListPage(&b, page)896 return b.String()897 }898 if parts[0] == "burned" {899 page := 1900 if len(parts) == 3 && parts[1] == "page" {901 p, err := strconv.Atoi(parts[2])902 if err != nil || p < 1 {903 return render404()904 }905 page = p906 } else if len(parts) != 1 {907 return render404()908 }909 var b strings.Builder910 b.WriteString(ufmt.Sprintf("# %s -- burned tokens\n\n[<- back to collection](%s)\n", CollectionName, realmRelPath))911 renderBurnedListPage(&b, page)912 return b.String()913 }914 if len(parts) == 2 && parts[0] == "token" {915 tid := grc721.TokenID(parts[1])916 owner, err := token.OwnerOf(tid)917 if err != nil {918 return renderBurnedTokenPage(tid)919 }920 metadata, _ := currentMetadata(tid)921 var b strings.Builder922 b.WriteString(ufmt.Sprintf("# %s #%s\n\n[<- back to collection](%s)\n\n", CollectionName, tid.String(), realmRelPath))923 image := metadata.Image924 name := metadata.Name925 if name == "" {926 name = "Untitled"927 }928 if image != "" {929 b.WriteString(ufmt.Sprintf("\n\n", name, bigTokenImage(image)))930 }931932 b.WriteString("## Details\n\n")933 b.WriteString(ufmt.Sprintf("* **Collection**: %s\n", CollectionName))934 b.WriteString(ufmt.Sprintf("* **Name**: %s\n", name))935 b.WriteString(ufmt.Sprintf("* **Index**: #%s\n", tid.String()))936 if metadata.Description != "" {937 b.WriteString(ufmt.Sprintf("* **Description**: %s\n", metadata.Description))938 }939 b.WriteString(ufmt.Sprintf("* **Owner**: %s\n", owner))940 b.WriteString(ufmt.Sprintf("* **Revealed**: %t (age %d block(s))\n", IsRevealed(tid), Age(tid)))941942 if len(metadata.Attributes) > 0 {943 b.WriteString("\n## Traits\n\n")944 for _, attr := range metadata.Attributes {945 b.WriteString(ufmt.Sprintf("* **%s**: %s\n", attr.TraitType, attr.Value))946 }947 }948 return b.String()949 }950951 return render404()952}953954func checkErr(err error) {955 if err != nil {956 panic(err)957 }958}959AddToWhitelist(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.