Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v1.gno

6.75 Kb · 259 lines
  1package trialmint
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime"
  7	unsaferealm "chain/runtime/unsafe"
  8	"crypto/sha256"
  9	"strings"
 10
 11	artpkg "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/art3d"
 12	grc721 "gno.land/p/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/grc721v2"
 13	stable "gno.land/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint/stable"
 14	"gno.land/p/nt/seqid/v0"
 15	"gno.land/p/nt/ufmt/v0"
 16)
 17
 18var (
 19	collectionOwnerHere address = "g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4"
 20
 21	mintingOpen = true
 22
 23	maxMintsPerWallet int64 = 0
 24
 25	nextID seqid.ID
 26)
 27
 28func MintAssembled(cur realm, to address) grc721.TokenID {
 29	if !mintingOpen {
 30		panic("minting is closed")
 31	}
 32	if maxMintsPerWallet > 0 && callerAddress() != collectionOwnerHere && stable.MintsByWallet(to) >= maxMintsPerWallet {
 33		panic(ufmt.Sprintf("wallet %s has already reached the %d-mint limit for this collection", to.String(), maxMintsPerWallet))
 34	}
 35
 36	caller := callerAddress()
 37	requiredPrice := stable.EffectivePrice(caller)
 38	if requiredPrice != 0 {
 39		runtime.AssertOriginCall()
 40		if !unsaferealm.PreviousRealm().IsUserCall() {
 41			panic("payment verification requires a direct user call")
 42		}
 43		got := unsaferealm.OriginSend().AmountOf("ugnot")
 44		if got != requiredPrice {
 45			panic(ufmt.Sprintf("payment required: exactly %d ugnot, got %d", requiredPrice, got))
 46		}
 47		b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
 48		b.SendCoins(cur.Address(), stable.Owner(), chain.Coins{{Denom: "ugnot", Amount: got}})
 49	}
 50
 51	tp, comboKey := assembleUniqueTraits()
 52
 53	tid := grc721.TokenID(ufmt.Sprintf("%d", uint64(nextID)+1))
 54	stable.RecordMint(cross(cur), to, caller, tid, tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Metal, tp.Active, comboKey, rarityTier(tp))
 55
 56	nextID.Next()
 57
 58	return tid
 59}
 60
 61type traitPreset struct {
 62	Chain    string
 63	ShapeIdx int
 64	ColorIdx int
 65	Metal    string
 66	Active   bool
 67}
 68
 69func traitComboKey(chainName string, shapeIdx, colorIdx int, metal string, active bool) string {
 70	return ufmt.Sprintf("%s|%d|%d|%s|%t", chainName, shapeIdx, colorIdx, metal, active)
 71}
 72
 73const activeProbability = 0.2
 74
 75type prng struct {
 76	seed string
 77	n    int
 78}
 79
 80func newPRNG(seed string) *prng { return &prng{seed: seed} }
 81
 82func (p *prng) next() uint64 {
 83	p.n++
 84	material := ufmt.Sprintf("%s:%d", p.seed, p.n)
 85	sum := sha256.Sum256([]byte(material))
 86	var v uint64
 87	for i := 0; i < 8; i++ {
 88		v = v<<8 | uint64(sum[i])
 89	}
 90	return v
 91}
 92
 93func (p *prng) floatIn(lo, hi float64) float64 {
 94	frac := float64(p.next()%1000000) / 1000000.0
 95	return lo + frac*(hi-lo)
 96}
 97
 98func (p *prng) intn(n int) int {
 99	if n <= 1 {
100		return 0
101	}
102	return int(p.next() % uint64(n))
103}
104
105func randomTraitPreset(attempt int) traitPreset {
106	material := ufmt.Sprintf("assemble:%d:%d:%s:%d",
107		runtime.ChainHeight(), uint64(nextID), callerAddress().String(), attempt)
108	rnd := newPRNG(material)
109
110	chainName := chainRoster[rnd.intn(len(chainRoster))]
111	stone := artpkg.StoneForChain(chainName)
112	colorIdx := artpkg.PickColorIndex(stone, rnd.next())
113	var shapeIdx int
114	if stone == "Pearl" {
115		shapeIdx = rnd.intn(len(artpkg.PearlShapeNames))
116	} else {
117		cuts := artpkg.CutsForStone(stone)
118		shapeIdx = cuts[rnd.intn(len(cuts))]
119	}
120	metal := artpkg.MetalNames[rnd.intn(len(artpkg.MetalNames))]
121	active := rnd.floatIn(0, 1) < activeProbability || artpkg.ChainAlwaysActive(chainName)
122
123	return traitPreset{Chain: chainName, ShapeIdx: shapeIdx, ColorIdx: colorIdx, Metal: metal, Active: active}
124}
125
126const maxAssembleAttempts = 200
127
128func assembleUniqueTraits() (traitPreset, string) {
129	for attempt := 0; attempt < maxAssembleAttempts; attempt++ {
130		tp := randomTraitPreset(attempt)
131		key := traitComboKey(tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Metal, tp.Active)
132		if stable.IsTraitComboAvailable(key) {
133			return tp, key
134		}
135	}
136	panic("could not find an unused trait combination after many attempts -- the collection may be fully minted")
137}
138
139func IsTraitComboAvailable(chainName string, shapeIdx, colorIdx int, metal string, active bool) bool {
140	key := traitComboKey(chainName, shapeIdx, colorIdx, metal, active)
141	return stable.IsTraitComboAvailable(key)
142}
143
144func MaxUniqueTraitCombos() int64 {
145	var total int64
146	for _, chainName := range chainRoster {
147		stone := artpkg.StoneForChain(chainName)
148		shapeCount := len(artpkg.CutsForStone(stone))
149		if stone == "Pearl" {
150			shapeCount = len(artpkg.PearlShapeNames)
151		}
152		states := int64(2) // active or sunset
153		if artpkg.ChainAlwaysActive(chainName) {
154			states = 1
155		}
156		total += int64(artpkg.ColorPaletteSize(stone)) * int64(shapeCount) * states
157	}
158	return total * int64(len(artpkg.MetalNames))
159}
160
161func ComboProbability(chainName string, shapeIdx, colorIdx int, active bool) float64 {
162	stone := artpkg.StoneForChain(chainName)
163	same := 0
164	for _, c := range chainRoster {
165		if artpkg.StoneForChain(c) == stone {
166			same++
167		}
168	}
169	p := float64(same) / float64(len(chainRoster))
170	if stone == "Pearl" {
171		p /= float64(len(artpkg.PearlShapeNames))
172	} else {
173		p /= float64(len(artpkg.CutsForStone(stone)))
174	}
175	p *= artpkg.ColorProbability(stone, colorIdx)
176	if !artpkg.ChainAlwaysActive(chainName) {
177		if active {
178			p *= activeProbability
179		} else {
180			p *= 1 - activeProbability
181		}
182	}
183	return p
184}
185
186var rarityTiers = []struct {
187	name string
188	maxP float64
189}{
190	{"Legendary", 0.000127233877},
191	{"Epic", 0.000423465423},
192	{"Rare", 0.000754985755},
193	{"Uncommon", 0.00364957265},
194}
195
196func rarityTier(tp traitPreset) string {
197	p := ComboProbability(tp.Chain, tp.ShapeIdx, tp.ColorIdx, tp.Active)
198	for _, t := range rarityTiers {
199		if p <= t.maxP {
200			return t.name
201		}
202	}
203	return "Common"
204}
205
206var chainRoster = []string{
207	"TEST-1", "TEST-2", "TEST-3", "TEST-4", "TEST-5", "TEST-6", "TEST-7",
208	"TEST-8", "TEST-9", "TEST-10", "TEST-11", "TEST-12", "TEST-13",
209	"TOPAZ-1", "SAPPHIRE-1", "PEARL-1", "BETANET", "GNOLAND-1",
210}
211
212func AddChain(cur realm, chainName string) {
213	assertOwner()
214	if chainName == "" {
215		panic("chain name must not be empty")
216	}
217	for _, c := range chainRoster {
218		if c == chainName {
219			panic("chain already in the roster")
220		}
221	}
222	chainRoster = append(chainRoster, chainName)
223}
224
225func ChainRosterSize() int64 { return int64(len(chainRoster)) }
226func ChainRosterCSV() string { return strings.Join(chainRoster, ";") }
227
228func callerAddress() address {
229	return unsaferealm.PreviousRealm().Address()
230}
231
232func assertOwner() {
233	if callerAddress() != collectionOwnerHere {
234		panic("unauthorized: caller is not the collection owner")
235	}
236}
237
238func MintingOpen() bool { return mintingOpen }
239
240func SetMintingOpen(cur realm, open bool) {
241	assertOwner()
242	mintingOpen = open
243	stable.ReportMintingOpen(cross(cur), open)
244}
245
246func MaxMintsPerWallet() int64 { return maxMintsPerWallet }
247
248func SetMaxMintsPerWallet(cur realm, max int64) {
249	assertOwner()
250	if max < 0 {
251		panic("max must be non-negative (0 = unlimited)")
252	}
253	maxMintsPerWallet = max
254	stable.ReportMaxMintsPerWallet(cross(cur), max)
255}
256
257func MintsByWallet(owner address) int64 {
258	return stable.MintsByWallet(owner)
259}