1// Package gems6v2 (deployed at .../gems6v2/v5, wired to ../stablev5; v4 with the gemsart3d2 art) is the mint-mechanics2// half of the gems6v2 candidate design: trait assembly and chain-3// roster management live here, same as the real gems6/v1. The one4// real difference is what MintAssembled does with the assembled5// traits: it never renders anything -- no gemsart.RenderTokenSVG call6// at all -- it just hands the raw trait fields to ../stable's7// RecordMint, which writes a cheap sealed placeholder. Rendering8// happens later, lazily, on a read (see ../stable's currentMetadata).9// This realm still imports gemsartfab, but only for its roster helper10// functions (StoneForChain, ColorPaletteSize, ShapeNames, ...) used to11// pick a valid random combination -- never for the expensive render12// itself.13package gems1415import (16 "chain"17 "chain/banker"18 "chain/runtime"19 unsaferealm "chain/runtime/unsafe"20 "crypto/sha256"21 "strings"2223 gemsart "gno.land/p/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gemsart3d2"24 "gno.land/p/nt/grc721/v0"25 stable "gno.land/r/g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems/gems/stable"26 "gno.land/p/nt/seqid/v0"27 "gno.land/p/nt/ufmt/v0"28)2930var (31 collectionOwnerHere address = "g17cjym5e9hhws46lt6329pv2gtx2ay0503hgems"3233 mintingOpen = true3435 maxMintsPerWallet int64 = 03637 nextID seqid.ID38)3940/* -------------------------------- Minting ---------------------------------- */4142// MintAssembled: assembles a fresh random (chain, shape, color, metal,43// active) combination, verifies payment against ../stable's discount-44// adjusted price, then hands the RAW trait fields (not a rendered45// image) to ../stable.RecordMint -- the render happens later, lazily,46// on a read. This is the whole fix: nothing shape-dependent runs47// inside this transaction, so its real gas cost stays small and48// uniform regardless of which random trait combination gets drawn49// (confirmed on the throwaway revealpoc spike before this real50// split-realm version was built).51func MintAssembled(cur realm, to address) grc721.TokenID {52 if !mintingOpen {53 panic("minting is closed")54 }55 if maxMintsPerWallet > 0 && callerAddress() != collectionOwnerHere && stable.MintsByWallet(to) >= maxMintsPerWallet {56 panic(ufmt.Sprintf("wallet %s has already reached the %d-mint limit for this collection", to.String(), maxMintsPerWallet))57 }5859 caller := callerAddress()60 requiredPrice := stable.EffectivePrice(caller)61 if requiredPrice != 0 {62 runtime.AssertOriginCall()63 if !unsaferealm.PreviousRealm().IsUserCall() {64 panic("payment verification requires a direct user call")65 }66 got := unsaferealm.OriginSend().AmountOf("ugnot")67 if got != requiredPrice {68 panic(ufmt.Sprintf("payment required: exactly %d ugnot, got %d", requiredPrice, got))69 }70 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)71 b.SendCoins(cur.Address(), stable.Owner(), chain.Coins{{Denom: "ugnot", Amount: got}})72 }7374 tp, comboKey := assembleUniqueTraits()7576 // plain numbers from 1 (#1, #2, ...), not seqid's padded base-3277 // string ("000003q"): what collectors and marketplaces expect to read78 tid := grc721.TokenID(ufmt.Sprintf("%d", uint64(nextID)+1))79 stable.RecordMint(cross(cur), to, caller, tid, tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Metal, tp.Active, comboKey, rarityTier(tp))8081 nextID.Next()8283 return tid84}8586/* --------------------------- trait assembly --------------------------- */8788type traitPreset struct {89 Chain string90 ShapeIdx int91 ColorIdx int92 Metal string93 Active bool94}9596func traitComboKey(chainName string, shapeIdx, colorIdx int, metal string, active bool) string {97 return ufmt.Sprintf("%s|%d|%d|%s|%t", chainName, shapeIdx, colorIdx, metal, active)98}99100const activeProbability = 0.2101102type prng struct {103 seed string104 n int105}106107func newPRNG(seed string) *prng { return &prng{seed: seed} }108109func (p *prng) next() uint64 {110 p.n++111 material := ufmt.Sprintf("%s:%d", p.seed, p.n)112 sum := sha256.Sum256([]byte(material))113 var v uint64114 for i := 0; i < 8; i++ {115 v = v<<8 | uint64(sum[i])116 }117 return v118}119120func (p *prng) floatIn(lo, hi float64) float64 {121 frac := float64(p.next()%1000000) / 1000000.0122 return lo + frac*(hi-lo)123}124125func (p *prng) intn(n int) int {126 if n <= 1 {127 return 0128 }129 return int(p.next() % uint64(n))130}131132func randomTraitPreset(attempt int) traitPreset {133 material := ufmt.Sprintf("assemble:%d:%d:%s:%d",134 runtime.ChainHeight(), uint64(nextID), callerAddress().String(), attempt)135 rnd := newPRNG(material)136137 chainName := chainRoster[rnd.intn(len(chainRoster))]138 stone := gemsart.StoneForChain(chainName)139 // colour by the stone's real-world odds (gemsart.colorWeights)140 colorIdx := gemsart.PickColorIndex(stone, rnd.next())141 // only cuts the stone is really sold in; pearls have their own shapes142 var shapeIdx int143 if stone == "Pearl" {144 shapeIdx = rnd.intn(len(gemsart.PearlShapeNames))145 } else {146 cuts := gemsart.CutsForStone(stone)147 shapeIdx = cuts[rnd.intn(len(cuts))]148 }149 metal := gemsart.MetalNames[rnd.intn(len(gemsart.MetalNames))]150 // mainnet never sunsets: always Active, so metadata matches the image151 active := rnd.floatIn(0, 1) < activeProbability || gemsart.ChainAlwaysActive(chainName)152153 return traitPreset{Chain: chainName, ShapeIdx: shapeIdx, ColorIdx: colorIdx, Metal: metal, Active: active}154}155156const maxAssembleAttempts = 200157158func assembleUniqueTraits() (traitPreset, string) {159 for attempt := 0; attempt < maxAssembleAttempts; attempt++ {160 tp := randomTraitPreset(attempt)161 key := traitComboKey(tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Metal, tp.Active)162 if stable.IsTraitComboAvailable(key) {163 return tp, key164 }165 }166 panic("could not find an unused trait combination after many attempts -- the collection may be fully minted")167}168169func IsTraitComboAvailable(chainName string, shapeIdx, colorIdx int, metal string, active bool) bool {170 key := traitComboKey(chainName, shapeIdx, colorIdx, metal, active)171 return stable.IsTraitComboAvailable(key)172}173174func MaxUniqueTraitCombos() int64 {175 var total int64176 for _, chainName := range chainRoster {177 stone := gemsart.StoneForChain(chainName)178 shapeCount := len(gemsart.CutsForStone(stone))179 if stone == "Pearl" {180 shapeCount = len(gemsart.PearlShapeNames)181 }182 states := int64(2) // active or sunset183 if gemsart.ChainAlwaysActive(chainName) {184 states = 1185 }186 total += int64(gemsart.ColorPaletteSize(stone)) * int64(shapeCount) * states187 }188 return total * int64(len(gemsart.MetalNames))189}190191/* -------------------------------- rarity -------------------------------- */192193// ComboProbability is the chance (0..1) that one mint draws this piece's194// look: its stone (the share of roster chains that carry it), its cut195// (even among the stone's cuts), its colour (the stone's colour odds) and196// its status (20% active; always active on mainnet). Ring metal and the197// chain's own name are equally likely for every piece, so they leave the198// ranking unchanged and are left out.199func ComboProbability(chainName string, shapeIdx, colorIdx int, active bool) float64 {200 stone := gemsart.StoneForChain(chainName)201 same := 0202 for _, c := range chainRoster {203 if gemsart.StoneForChain(c) == stone {204 same++205 }206 }207 p := float64(same) / float64(len(chainRoster))208 if stone == "Pearl" {209 p /= float64(len(gemsart.PearlShapeNames))210 } else {211 p /= float64(len(gemsart.CutsForStone(stone)))212 }213 p *= gemsart.ColorProbability(stone, colorIdx)214 if !gemsart.ChainAlwaysActive(chainName) {215 if active {216 p *= activeProbability217 } else {218 p *= 1 - activeProbability219 }220 }221 return p222}223224// rarityTiers are upper bounds on ComboProbability, rarest first. Set225// (2026-09-25) from the full distribution of all 691 looks on the226// 18-chain roster, each bound the midpoint between two neighbouring227// probability values, so no two equally likely looks fall on different228// sides and float rounding cannot move one. Shares of mints:229// Legendary 1.3%, Epic 3.5%, Rare 11.1%, Uncommon 25.0%, Common 59.1%.230// A tier is fixed at mint (stored with the token); adding a chain later231// only changes the odds of new mints, never an existing token's tier.232var rarityTiers = []struct {233 name string234 maxP float64235}{236 {"Legendary", 0.000127233877},237 {"Epic", 0.000423465423},238 {"Rare", 0.000754985755},239 {"Uncommon", 0.00364957265},240}241242func rarityTier(tp traitPreset) string {243 p := ComboProbability(tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Active)244 for _, t := range rarityTiers {245 if p <= t.maxP {246 return t.name247 }248 }249 return "Common"250}251252/* ----------------------------- Chain roster ---------------------------- */253254var chainRoster = []string{255 "TEST-1", "TEST-2", "TEST-3", "TEST-4", "TEST-5", "TEST-6", "TEST-7",256 "TEST-8", "TEST-9", "TEST-10", "TEST-11", "TEST-12", "TEST-13",257 "TOPAZ-1", "SAPPHIRE-1", "PEARL-1", "BETANET", "GNOLAND-1",258}259260func AddChain(cur realm, chainName string) {261 assertOwner()262 if chainName == "" {263 panic("chain name must not be empty")264 }265 for _, c := range chainRoster {266 if c == chainName {267 panic("chain already in the roster")268 }269 }270 chainRoster = append(chainRoster, chainName)271}272273func ChainRosterSize() int64 { return int64(len(chainRoster)) }274func ChainRosterCSV() string { return strings.Join(chainRoster, ";") }275276/* ----------------------------- Owner controls -------------------------------- */277278func callerAddress() address {279 return unsaferealm.PreviousRealm().Address()280}281282func assertOwner() {283 if callerAddress() != collectionOwnerHere {284 panic("unauthorized: caller is not the collection owner")285 }286}287288func MintingOpen() bool { return mintingOpen }289290func SetMintingOpen(cur realm, open bool) {291 assertOwner()292 mintingOpen = open293 stable.ReportMintingOpen(cross(cur), open)294}295296func MaxMintsPerWallet() int64 { return maxMintsPerWallet }297298func SetMaxMintsPerWallet(cur realm, max int64) {299 assertOwner()300 if max < 0 {301 panic("max must be non-negative (0 = unlimited)")302 }303 maxMintsPerWallet = max304 stable.ReportMaxMintsPerWallet(cross(cur), max)305}306307func MintsByWallet(owner address) int64 {308 return stable.MintsByWallet(owner)309}310AddChain(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}, chainName string)
ChainRosterCSV() string
ChainRosterSize() int64
ComboProbability(chainName string, shapeIdx int, colorIdx int, active bool) float64
IsTraitComboAvailable(chainName string, shapeIdx int, colorIdx int, metal string, active bool) bool
MaxMintsPerWallet() int64
MaxUniqueTraitCombos() int64
MintAssembled(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) string
MintingOpen() bool
MintsByWallet(owner string) int64
SetMaxMintsPerWallet(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)
SetMintingOpen(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)
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.