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}