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

v1.gno

6.71 Kb · 259 lines
  1package trialmint2
  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/art3d2"
 12	"gno.land/p/nt/grc721/v0"
 13	stable "gno.land/r/g1wx60nkcp5lfsxmedeg7sfs47tzxkath05u63t4/trialmint2/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}