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.

sale.gno

7.60 Kb · 277 lines
  1package guardians_pilot_v1
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime/unsafe"
  7	"time"
  8
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/ufmt/v0"
 11)
 12
 13const (
 14	salePhaseClosed    = "CLOSED"
 15	salePhaseAllowlist = "ALLOWLIST"
 16	salePhasePublic    = "PUBLIC"
 17	salePhaseOpen      = "OPEN"
 18	saleDenom          = "ugnot"
 19)
 20
 21type saleConfiguration struct {
 22	allowlistStart int64
 23	allowlistEnd   int64
 24	publicStart    int64
 25	publicEnd      int64
 26	allowlistPrice int64
 27	publicPrice    int64
 28	allowlistLimit int
 29	publicLimit    int
 30	treasury       address
 31}
 32
 33var (
 34	saleConfig              saleConfiguration
 35	saleConfigured          bool
 36	salePaused              = true
 37	allowlistMintedByWallet avl.Tree
 38	publicMintedByWallet    avl.Tree
 39)
 40
 41// ConfigureSale stores timestamps as Unix seconds and prices in ugnot.
 42// Configuration is only mutable while the sale is paused.
 43func ConfigureSale(
 44	cur realm,
 45	allowlistStart, allowlistEnd int64,
 46	publicStart, publicEnd int64,
 47	allowlistPrice, publicPrice int64,
 48	allowlistLimit, publicLimit int,
 49	treasury address,
 50) {
 51	assertAdmin(cur.Previous().Address())
 52	if !salePaused {
 53		panic("guardians: pause sale before configuration")
 54	}
 55	if !validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd) {
 56		panic("guardians: invalid sale schedule")
 57	}
 58	if !validAllowlistPrice(allowlistPrice) || !validSalePrice(publicPrice) {
 59		panic("guardians: invalid sale price")
 60	}
 61	if !validWalletLimit(allowlistLimit) || !validWalletLimit(publicLimit) {
 62		panic("guardians: invalid wallet limit")
 63	}
 64	if allowlistPrice != 0 || publicPrice != 1000000 || allowlistLimit != 1 || publicLimit != 3 {
 65		panic("guardians: pilot requires free 1 + paid 3 at 1 GNOT")
 66	}
 67	if treasury != address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8") {
 68		panic("guardians: unexpected pilot treasury")
 69	}
 70	if !treasury.IsValid() {
 71		panic("guardians: invalid sale treasury")
 72	}
 73
 74	saleConfig = saleConfiguration{
 75		allowlistStart: allowlistStart,
 76		allowlistEnd:   allowlistEnd,
 77		publicStart:    publicStart,
 78		publicEnd:      publicEnd,
 79		allowlistPrice: allowlistPrice,
 80		publicPrice:    publicPrice,
 81		allowlistLimit: allowlistLimit,
 82		publicLimit:    publicLimit,
 83		treasury:       treasury,
 84	}
 85	saleConfigured = true
 86
 87	chain.Emit(
 88		"SaleConfigured",
 89		"allowlistStart", ufmt.Sprintf("%d", allowlistStart),
 90		"allowlistEnd", ufmt.Sprintf("%d", allowlistEnd),
 91		"publicStart", ufmt.Sprintf("%d", publicStart),
 92		"publicEnd", ufmt.Sprintf("%d", publicEnd),
 93		"treasury", treasury.String(),
 94	)
 95}
 96
 97func SetSalePaused(cur realm, paused bool) {
 98	assertAdmin(cur.Previous().Address())
 99	if !paused && !saleConfigured {
100		panic("guardians: configure sale before unpausing")
101	}
102	if !paused && allowlistRoot == "" {
103		panic("guardians: configure allowlist root before unpausing")
104	}
105	if !paused && revealState != revealStateCommitted {
106		panic("guardians: commit reveal configuration before unpausing")
107	}
108	if !paused {
109		allowlistRootLocked = true
110	}
111	salePaused = paused
112	chain.Emit("SalePauseChanged", "paused", ufmt.Sprintf("%t", paused))
113}
114
115func SalePhase() string { return salePhaseAt(time.Now().Unix()) }
116
117// Activate once, after metadata and proofs are ready. Both purchase modes share
118// the same on-chain 24-hour window beginning in this activation transaction.
119func ActivatePilotSale(cur realm) {
120	assertAdmin(cur.Previous().Address())
121	if saleConfigured {
122		panic("guardians: pilot sale already configured")
123	}
124	start := time.Now().Unix()
125	end := start + 86400
126	ConfigureSale(cur, start, end, start, end, 0, 1000000, 1, 3, address("g1ht2848ahr20tqq7086thg5h9vrw4836suzard8"))
127	SetSalePaused(cur, false)
128}
129
130func SalePhaseAt(unixTime int64) string { return salePhaseAt(unixTime) }
131
132func salePhaseAt(unixTime int64) string {
133	if salePaused || !saleConfigured {
134		return salePhaseClosed
135	}
136	if unixTime >= saleConfig.publicStart && unixTime < saleConfig.publicEnd {
137		return salePhaseOpen
138	}
139	return salePhaseClosed
140}
141
142func SalePaused() bool { return salePaused }
143
144func SaleConfigured() bool { return saleConfigured }
145
146func SaleTreasury() address { return saleConfig.treasury }
147
148func AllowlistStart() int64 { return saleConfig.allowlistStart }
149
150func AllowlistEnd() int64 { return saleConfig.allowlistEnd }
151
152func PublicStart() int64 { return saleConfig.publicStart }
153
154func PublicEnd() int64 { return saleConfig.publicEnd }
155
156func AllowlistPrice() int64 { return saleConfig.allowlistPrice }
157
158func PublicPrice() int64 { return saleConfig.publicPrice }
159
160func AllowlistWalletLimit() int { return saleConfig.allowlistLimit }
161
162func PublicWalletLimit() int { return saleConfig.publicLimit }
163
164func AllowlistMinted(owner address) int { return mintedCount(&allowlistMintedByWallet, owner) }
165
166func PublicMinted(owner address) int { return mintedCount(&publicMintedByWallet, owner) }
167
168func NextSaleTimestamp() int64 {
169	now := time.Now().Unix()
170	if !saleConfigured {
171		return 0
172	}
173	if now < saleConfig.allowlistStart {
174		return saleConfig.allowlistStart
175	}
176	if now < saleConfig.allowlistEnd {
177		return saleConfig.allowlistEnd
178	}
179	if now < saleConfig.publicStart {
180		return saleConfig.publicStart
181	}
182	if now < saleConfig.publicEnd {
183		return saleConfig.publicEnd
184	}
185	return 0
186}
187
188// MintPublic accepts only direct Adena/EOA calls. This pairing is critical:
189// OriginSend describes actual receipt by this realm only for a direct user call.
190func MintPublic(cur realm, quantity int) {
191	if !cur.Previous().IsUserCall() {
192		panic("guardians: only direct user calls can mint")
193	}
194	if SalePhase() != salePhaseOpen {
195		panic("guardians: public sale is not active")
196	}
197
198	caller := cur.Previous().Address()
199	assertSaleMint(quantity, PublicMinted(caller), saleConfig.publicLimit)
200	required := requiredPayment(saleConfig.publicPrice, quantity)
201	sent := unsafe.OriginSend()
202	assertExactPayment(sent, required)
203
204	mintTokens(caller, quantity)
205	publicMintedByWallet.Set(caller.String(), PublicMinted(caller)+quantity)
206	forwardPayment(cur, sent)
207
208	chain.Emit(
209		"PublicMint",
210		"minter", caller.String(),
211		"quantity", ufmt.Sprintf("%d", quantity),
212		"paid", ufmt.Sprintf("%d", required),
213	)
214}
215
216func assertSaleMint(quantity, alreadyMinted, walletLimit int) {
217	assertMintQuantity(quantity)
218	if alreadyMinted+quantity > walletLimit {
219		panic("guardians: wallet mint limit exceeded")
220	}
221}
222
223func requiredPayment(unitPrice int64, quantity int) int64 {
224	if unitPrice < 0 || !validMintQuantity(quantity) {
225		panic("guardians: invalid payment calculation")
226	}
227	return unitPrice * int64(quantity)
228}
229
230func assertExactPayment(sent chain.Coins, required int64) {
231	if required == 0 {
232		if len(sent) != 0 {
233			panic("guardians: free mint must not include payment")
234		}
235		return
236	}
237	if !isExactPayment(sent, required) {
238		panic(ufmt.Sprintf("guardians: exact payment required: %dugnot", required))
239	}
240}
241
242func isExactPayment(sent chain.Coins, required int64) bool {
243	return len(sent) == 1 && sent[0].Denom == saleDenom && sent[0].Amount == required
244}
245
246func forwardPayment(cur realm, sent chain.Coins) {
247	if len(sent) == 0 {
248		return
249	}
250	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
251	b.SendCoins(cur.Address(), saleConfig.treasury, sent)
252}
253
254func mintedCount(tree *avl.Tree, owner address) int {
255	v := tree.Get(owner.String())
256	if v == nil {
257		return 0
258	}
259	return v.(int)
260}
261
262func validSaleSchedule(allowlistStart, allowlistEnd, publicStart, publicEnd int64) bool {
263	return allowlistStart > 0 && allowlistStart == publicStart &&
264		allowlistEnd == publicEnd && publicEnd- publicStart == 86400
265}
266
267func validSalePrice(price int64) bool {
268	return price > 0 && price <= 9223372036854775807/int64(maxMintBatch)
269}
270
271func validAllowlistPrice(price int64) bool {
272	return price >= 0 && price <= 9223372036854775807/int64(maxMintBatch)
273}
274
275func validWalletLimit(limit int) bool {
276	return limit > 0 && int64(limit) <= maxSupply
277}