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

gnowardrobe.gno

9.62 Kb · 339 lines
  1// Package gnodrobe — GnoDrobe Collection, GRC-721 NFT minter realm.
  2//
  3// Пользователь собирает гнома (hat + beard) на сайте GnoDrobe и вызывает
  4// Mint(hatID, beardID), прикладывая к вызову ровно
  5// (hatPrice + beardPrice) ugnot. Средства сразу уходят владельцу
  6// коллекции (collectionOwner), а минтер получает NFT с on-chain
  7// метаданными (OpenSea-совместимый data URI: имя, описание, трейты).
  8//
  9// Ограничения: maxSupply (общий тираж), maxMintsPerWallet (лимит на
 10// один кошелёк), royalty 1% (декларируется в метаданных; полноценный
 11// сбор роялти при вторичных продажах — на маркетплейсе).
 12package gnowardrobe
 13
 14import (
 15	"chain"
 16	"chain/banker"
 17	runtime "chain/runtime"
 18	unsaferealm "chain/runtime/unsafe"
 19	"strconv"
 20	"strings"
 21
 22	grc721 "gno.land/p/g1gn6t0q9wenwhdda47rkrpfd63kcxjvyp7eqwku/grc721v2"
 23	"gno.land/p/nt/avl/v0"
 24	"gno.land/p/nt/ufmt/v0"
 25)
 26
 27const (
 28	CollectionName   = "GnoDrobe Collection"
 29	CollectionSymbol = "GDG"
 30
 31	// MaxSupply — общий тираж коллекции (жёсткий потолок минтов).
 32	MaxSupply = 10000
 33
 34	// MaxMintsPerWallet — сколько NFT максимум на один кошелёк.
 35	MaxMintsPerWallet = 10
 36
 37	// RoyaltyPct — роялти с вторичных продаж, 1%.
 38	RoyaltyPct = 1
 39
 40	// collectionOwner — владелец коллекции, получает платежи за минт.
 41	// = Harry (деплойер). Менять только owner-функцией SetOwner.
 42	collectionOwner = "g19sh6ww9g6ukzhndg4d4zkx25l6nq782f9pfuap"
 43
 44	// realmRelPath — путь реалма (для рендера).
 45	realmRelPath = "/r/g19sh6ww9g6ukzhndg4d4zkx25l6nq782f9pfuap/gnowardrobe/v2"
 46)
 47
 48// Part — одна часть гнома (шапка или борода) с ценой в GNOT.
 49type Part struct {
 50	Name  string
 51	Price int64 // цена в GNOT (не ugnot)
 52}
 53
 54// hats/beards — каталог ассетов GnoDrobe с ценами из assets-manifest.json.
 55// Индекс = ID, как в манифесте фронтенда. Порядок менять нельзя —
 56// сломает соответствие с UI.
 57var hats = []Part{
 58	{"Cardboard Crown", 2},
 59	{"Cosmic", 4},
 60	{"Crystal", 6},
 61	{"Denim", 2},
 62	{"Golden", 4},
 63	{"Grassy", 6},
 64	{"Jelly", 2},
 65	{"Leather", 4},
 66	{"Metallic", 6},
 67	{"Noodle", 2},
 68	{"Plank", 4},
 69	{"Spruce", 6},
 70	{"Straw", 2},
 71	{"Volcanic", 4},
 72}
 73
 74var beards = []Part{
 75	{"Bone", 4},
 76	{"Cloudy", 6},
 77	{"Coin", 2},
 78	{"Cosmic", 4},
 79	{"Crystal", 6},
 80	{"Leafy", 2},
 81	{"Lightning", 4},
 82	{"Metallic", 6},
 83	{"Mushroom", 2},
 84	{"Piped", 4},
 85}
 86
 87var (
 88	nft     *grc721.MetadataNFT
 89	owner   address = collectionOwner
 90	mintingOpen      = true
 91	mintsByWallet    = avl.NewTree() // addr -> int64 (сколько наминтил)
 92)
 93
 94func init(cur realm) {
 95	nft = grc721.NewNFTWithMetadata(0, cur, CollectionName, CollectionSymbol)
 96}
 97
 98// ---- Owner / конфиг ----
 99
100func Owner() address { return owner }
101
102func SetOwner(cur realm, newOwner address) {
103	if callerAddress() != owner {
104		panic("only the collection owner can change the owner")
105	}
106	if newOwner == "" {
107		panic("owner cannot be empty")
108	}
109	owner = newOwner
110}
111
112func MintingOpen() bool { return mintingOpen }
113
114func SetMintingOpen(cur realm, open bool) {
115	if callerAddress() != owner {
116		panic("only the collection owner can change minting")
117	}
118	mintingOpen = open
119}
120
121// WithdrawFunds отправляет amount ugnot с баланса реалма владельцу.
122func WithdrawFunds(cur realm, amountUgnot int64) {
123	if callerAddress() != owner {
124		panic("only the collection owner can withdraw")
125	}
126	if amountUgnot <= 0 {
127		panic("amount must be positive")
128	}
129	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
130	b.SendCoins(cur.Address(), owner, chain.Coins{{Denom: "ugnot", Amount: amountUgnot}})
131}
132
133// ---- Каталог ----
134
135func HatCount() int   { return len(hats) }
136func BeardCount() int { return len(beards) }
137
138func HatName(id int) string {
139	if id < 0 || id >= len(hats) {
140		return ""
141	}
142	return hats[id].Name
143}
144
145func BeardName(id int) string {
146	if id < 0 || id >= len(beards) {
147		return ""
148	}
149	return beards[id].Name
150}
151
152func HatPriceUgnot(id int) int64 {
153	if id < 0 || id >= len(hats) {
154		return 0
155	}
156	return hats[id].Price * 1_000_000
157}
158
159func BeardPriceUgnot(id int) int64 {
160	if id < 0 || id >= len(beards) {
161		return 0
162	}
163	return beards[id].Price * 1_000_000
164}
165
166// ---- Минт ----
167
168// Mint чеканит NFT гнома из hatID+beardID вызывающему.
169// К вызову нужно приложить ровно (HatPriceUgnot(hatID)+BeardPriceUgnot(beardID)).
170func Mint(cur realm, hatID, beardID int) string {
171	caller := callerAddress()
172
173	if !mintingOpen {
174		panic("minting is closed")
175	}
176	if nft.TokenCount() >= MaxSupply {
177		panic("max supply reached")
178	}
179	if maxPerWalletReached(caller) {
180		panic("wallet mint limit reached")
181	}
182	if hatID < 0 || hatID >= len(hats) {
183		panic("invalid hat id")
184	}
185	if beardID < 0 || beardID >= len(beards) {
186		panic("invalid beard id")
187	}
188
189	priceUgnot := HatPriceUgnot(hatID) + BeardPriceUgnot(beardID)
190
191	// Оплата: приложенные ugnot должны ровно совпадать с ценой.
192	if priceUgnot > 0 {
193		runtime.AssertOriginCall()
194		if !unsaferealm.PreviousRealm().IsUserCall() {
195			panic("payment verification requires a direct user call")
196		}
197		got := unsaferealm.OriginSend().AmountOf("ugnot")
198		if got != priceUgnot {
199			panic(ufmt.Sprintf(
200				"payment required: exactly %d ugnot, got %d",
201				priceUgnot, got,
202			))
203		}
204		// Сразу пересылаем владельцу.
205		b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
206		b.SendCoins(cur.Address(), owner, chain.Coins{{Denom: "ugnot", Amount: got}})
207	}
208
209	tid := grc721.TokenID(strconv.FormatInt(nft.TokenCount()+1, 10))
210	checkErr(nft.Mint(caller, tid))
211	checkErr(nft.SetTokenMetadata(caller, tid, grc721.Metadata{
212		Name:        ufmt.Sprintf("%s #%s", CollectionName, tid.String()),
213		Description: "A gnome from the GnoDrobe Collection, assembled from hat + beard on-chain traits.",
214		Attributes: []grc721.Trait{
215			{TraitType: "Hat", Value: hats[hatID].Name},
216			{TraitType: "Beard", Value: beards[beardID].Name},
217			{TraitType: "Royalty", Value: "1%"},
218		},
219	}))
220	incMintCount(caller)
221
222	chain.Emit(
223		"GnoDrobeMinted",
224		"tokenId", tid.String(),
225		"hat", hats[hatID].Name,
226		"beard", beards[beardID].Name,
227		"to", caller.String(),
228	)
229
230	return tid.String()
231}
232
233// ---- View-функции для фронтенда ----
234
235func Name() string  { return nft.Name() }
236func Symbol() string { return nft.Symbol() }
237func TotalMinted() int64 { return nft.TokenCount() }
238
239// GetMaxSupply — общий тираж (константа MaxSupply).
240func GetMaxSupply() int64 { return MaxSupply }
241
242func TokenURI(tid grc721.TokenID) (string, error) { return nft.TokenURI(tid) }
243func OwnerOf(tid grc721.TokenID) (address, error) { return nft.OwnerOf(tid) }
244func BalanceOf(addr address) (int64, error)       { return nft.BalanceOf(addr) }
245func TokenMetadata(tid grc721.TokenID) (grc721.Metadata, error) {
246	return nft.TokenMetadata(tid)
247}
248
249// TokensOf возвращает список tokenID, принадлежащих addr (по возрастанию).
250// Реализовано через обход всех токенов — для тестнета норм
251// (макс 10k токенов, qeval читает без газа).
252func TokensOf(addr address) []string {
253	out := []string{}
254	for i := int64(1); i <= nft.TokenCount(); i++ {
255		tid := grc721.TokenID(strconv.FormatInt(i, 10))
256		o, err := nft.OwnerOf(tid)
257		if err == nil && o == addr {
258			out = append(out, tid.String())
259		}
260	}
261	return out
262}
263
264// ---- Рендер ----
265
266func Render(path string) string {
267	var b strings.Builder
268	b.WriteString("# GnoDrobe Collection\n\n")
269	b.WriteString(ufmt.Sprintf("Symbol: %s\n", CollectionSymbol))
270	b.WriteString(ufmt.Sprintf("Minted: %d / %d\n", nft.TokenCount(), MaxSupply))
271	b.WriteString(ufmt.Sprintf("Owner: %s\n", owner.String()))
272	b.WriteString(ufmt.Sprintf("Royalty: %d%%\n", RoyaltyPct))
273	b.WriteString(ufmt.Sprintf("Mint price: %d ugnot (hat) + %d ugnot (beard) per part\n\n",
274		hats[0].Price*1_000_000, beards[0].Price*1_000_000))
275
276	b.WriteString("## Parts\n\n")
277	b.WriteString("### Hats\n")
278	for i, h := range hats {
279		b.WriteString(ufmt.Sprintf("%d. %s — %d GNOT\n", i, h.Name, h.Price))
280	}
281	b.WriteString("\n### Beards\n")
282	for i, bd := range beards {
283		b.WriteString(ufmt.Sprintf("%d. %s — %d GNOT\n", i, bd.Name, bd.Price))
284	}
285	b.WriteString("\n## Tokens\n")
286	for i := int64(1); i <= nft.TokenCount(); i++ {
287		tid := grc721.TokenID(strconv.FormatInt(i, 10))
288		o, err := nft.OwnerOf(tid)
289		if err != nil {
290			continue
291		}
292		md, err := nft.TokenMetadata(tid)
293		if err != nil {
294			continue
295		}
296		hat := ""
297		beard := ""
298		for _, t := range md.Attributes {
299			if t.TraitType == "Hat" {
300				hat = t.Value
301			}
302			if t.TraitType == "Beard" {
303				beard = t.Value
304			}
305		}
306		b.WriteString(ufmt.Sprintf("- #%s: %s + %s, owner %s\n",
307			tid.String(), hat, beard, o.String()))
308	}
309	return b.String()
310}
311
312// ---- Хелперы ----
313
314func callerAddress() address {
315	return unsaferealm.PreviousRealm().Address()
316}
317
318func maxPerWalletReached(addr address) bool {
319	v := mintsByWallet.Get(addr.String())
320	if v == nil {
321		return false
322	}
323	return v.(int64) >= MaxMintsPerWallet
324}
325
326func incMintCount(addr address) {
327	v := mintsByWallet.Get(addr.String())
328	if v == nil {
329		mintsByWallet.Set(addr.String(), int64(1))
330		return
331	}
332	mintsByWallet.Set(addr.String(), v.(int64)+1)
333}
334
335func checkErr(err error) {
336	if err != nil {
337		panic(err)
338	}
339}