Search Apps Documentation Source Content File Folder Download Copy Actions Download

nft.gno

5.62 Kb · 207 lines
  1// Package gnogle_nft is the NFT layer of the GNOGLE marketplace: a stable,
  2// upgrade-independent realm that stores every collection and token (GRC721).
  3//
  4// The marketplace LOGIC lives in a SEPARATE realm (gnogle_market). It never
  5// holds tokens — instead an owner grants it operator approval (SetApprovalForAll)
  6// and the market moves tokens by calling TransferFrom here. Because the tokens
  7// live in THIS realm, the market can be redeployed/upgraded any number of times
  8// without affecting a single NFT.
  9package gnogle_nft
 10
 11import (
 12	"chain"
 13	"chain/banker"
 14	"chain/runtime"
 15	"chain/runtime/unsafe"
 16	"strconv"
 17
 18	"gno.land/p/g18wk4a80cr7dqa25vfka2yug5n3pd50udled6y3/grc721"
 19	"gno.land/p/nt/avl/v0"
 20	"gno.land/p/nt/ufmt/v0"
 21)
 22
 23const (
 24	ugnot          = "ugnot"
 25	bpsDenominator = 10000
 26	maxRoyaltyBps  = 5000
 27	maxSlugLen     = 40
 28)
 29
 30var zeroAddr address
 31
 32// Collection is one NFT collection. The concrete *grc721.BasicNFT is held in an
 33// unexported field, so only this realm can mutate token ownership.
 34type Collection struct {
 35	id         string
 36	name       string
 37	symbol     string
 38	creator    address
 39	baseURI    string
 40	mintPrice  int64
 41	maxSupply  int64
 42	royaltyBps int64
 43	minted     int64
 44	burned     int64
 45	sealed     bool
 46	verified   bool
 47	createdAt  int64
 48	nft        *grc721.BasicNFT
 49}
 50
 51var (
 52	collections avl.Tree // id -> *Collection
 53	collOrder   []string
 54	admin       address
 55)
 56
 57// CreateCollection registers a new collection owned by the caller.
 58func CreateCollection(cur realm, id, name, symbol, baseURI string, mintPrice, maxSupply, royaltyBps int64) string {
 59	assertUserCall(cur)
 60	creator := cur.Previous().Address()
 61	if !validSlug(id) {
 62		panic("collection id must be 1-40 chars of [a-z0-9-]")
 63	}
 64	if _, ok := collections.Get(id); ok {
 65		panic("collection id already taken: " + id)
 66	}
 67	if mintPrice < 0 || maxSupply < 0 {
 68		panic("mintPrice/maxSupply cannot be negative")
 69	}
 70	if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {
 71		panic(ufmt.Sprintf("royaltyBps must be 0..%d", maxRoyaltyBps))
 72	}
 73	nft := grc721.NewBasicNFT(0, cur, name, symbol)
 74	collections.Set(id, &Collection{
 75		id: id, name: name, symbol: symbol, creator: creator, baseURI: baseURI,
 76		mintPrice: mintPrice, maxSupply: maxSupply, royaltyBps: royaltyBps,
 77		createdAt: runtime.ChainHeight(), nft: nft,
 78	})
 79	collOrder = append(collOrder, id)
 80	chain.Emit("CreateCollection", "id", id, "creator", creator.String())
 81	return id
 82}
 83
 84// Mint mints the next token of a collection to the caller, who must attach the
 85// mint price in ugnot (paid to the creator).
 86func Mint(cur realm, collID string) string {
 87	assertUserCall(cur)
 88	coll := mustGetCollection(collID)
 89	minter := cur.Previous().Address()
 90	paid := receivedUgnot()
 91	if paid != coll.mintPrice {
 92		panic(ufmt.Sprintf("must send exactly %d ugnot to mint (sent %d)", coll.mintPrice, paid))
 93	}
 94	tokenID := mintOne(coll, minter)
 95	payCreator(cur, coll.creator, paid)
 96	chain.Emit("Mint", "collection", collID, "tokenId", tokenID, "minter", minter.String())
 97	return tokenID
 98}
 99
100// MintBatch mints count (1..20) tokens to the caller, charging count × mint price.
101func MintBatch(cur realm, collID string, count int64) string {
102	assertUserCall(cur)
103	if count <= 0 || count > 20 {
104		panic("count must be between 1 and 20")
105	}
106	coll := mustGetCollection(collID)
107	minter := cur.Previous().Address()
108	paid := receivedUgnot()
109	if paid != coll.mintPrice*count {
110		panic(ufmt.Sprintf("must send exactly %d ugnot for %d mints", coll.mintPrice*count, count))
111	}
112	first := ""
113	for i := int64(0); i < count; i++ {
114		if tid := mintOne(coll, minter); first == "" {
115			first = tid
116		}
117	}
118	payCreator(cur, coll.creator, paid)
119	chain.Emit("MintBatch", "collection", collID, "count", strconv.FormatInt(count, 10), "minter", minter.String())
120	return first
121}
122
123func mintOne(coll *Collection, minter address) string {
124	if coll.maxSupply > 0 && coll.minted >= coll.maxSupply {
125		panic("collection is sold out")
126	}
127	tokenID := strconv.FormatInt(coll.minted+1, 10)
128	if err := coll.nft.Mint(minter, grc721.TokenID(tokenID)); err != nil {
129		panic(err)
130	}
131	coll.minted++
132	return tokenID
133}
134
135// --- admin (verified badge only) -------------------------------------------
136
137// ClaimAdmin assigns the admin (one-time). Admin can only flag collections verified.
138func ClaimAdmin(cur realm) {
139	if admin != zeroAddr {
140		panic("admin already claimed")
141	}
142	admin = cur.Previous().Address()
143	chain.Emit("ClaimAdmin", "admin", admin.String())
144}
145
146func assertAdmin(cur realm) {
147	if admin == zeroAddr {
148		panic("admin not set; call ClaimAdmin first")
149	}
150	if cur.Previous().Address() != admin {
151		panic("unauthorized: admin only")
152	}
153}
154
155// --- helpers ---------------------------------------------------------------
156
157func assertUserCall(cur realm) {
158	if !cur.Previous().IsUserCall() {
159		panic("only direct user calls are accepted")
160	}
161}
162
163func receivedUgnot() int64 {
164	sent := unsafe.OriginSend()
165	for _, c := range sent {
166		if c.Denom != ugnot {
167			panic("only ugnot is accepted")
168		}
169	}
170	return sent.AmountOf(ugnot)
171}
172
173func payCreator(cur realm, to address, amount int64) {
174	if amount <= 0 {
175		return
176	}
177	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
178	b.SendCoins(cur.Address(), to, chain.Coins{chain.NewCoin(ugnot, amount)})
179}
180
181func mustGetCollection(id string) *Collection {
182	v, ok := collections.Get(id)
183	if !ok {
184		panic("collection not found: " + id)
185	}
186	return v.(*Collection)
187}
188
189func getCollection(id string) (*Collection, bool) {
190	v, ok := collections.Get(id)
191	if !ok {
192		return nil, false
193	}
194	return v.(*Collection), true
195}
196
197func validSlug(s string) bool {
198	if len(s) == 0 || len(s) > maxSlugLen {
199		return false
200	}
201	for _, c := range s {
202		if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
203			return false
204		}
205	}
206	return true
207}