Search Apps Documentation Source Content File Folder Download Copy Actions Download

market.gno

5.03 Kb · 174 lines
  1package nftmarket
  2
  3import (
  4	"chain"
  5	"strconv"
  6
  7	"gno.land/p/g18wk4a80cr7dqa25vfka2yug5n3pd50udled6y3/grc721"
  8	"gno.land/p/nt/ufmt/v0"
  9)
 10
 11// List puts a token the caller owns up for sale at a fixed price (in ugnot).
 12// The NFT is escrowed in the realm until the listing is bought or cancelled.
 13func List(cur realm, collID, tokenID string, price int64) {
 14	assertUserCall(cur)
 15	if price <= 0 {
 16		panic("price must be positive")
 17	}
 18	coll := mustGetCollection(collID)
 19	seller := cur.Previous().Address()
 20	tid := grc721.TokenID(tokenID)
 21
 22	owner, err := coll.nft.OwnerOf(tid)
 23	if err != nil {
 24		panic(err)
 25	}
 26	if owner != seller {
 27		panic("only the token owner can list it")
 28	}
 29	if _, exists := getListing(collID, tokenID); exists {
 30		panic("token is already listed")
 31	}
 32
 33	// Escrow: move the NFT into the realm's custody.
 34	if err := coll.nft.TransferFrom(seller, seller, cur.Address(), tid); err != nil {
 35		panic(err)
 36	}
 37
 38	listings.Set(listingKey(collID, tokenID), &Listing{
 39		collID:    collID,
 40		tokenID:   tokenID,
 41		seller:    seller,
 42		price:     price,
 43		createdAt: now(),
 44	})
 45
 46	chain.Emit("List", "collection", collID, "tokenId", tokenID,
 47		"seller", seller.String(), "price", strconv.FormatInt(price, 10))
 48}
 49
 50// Buy purchases a listed token. The caller must attach exactly the listing
 51// price in ugnot. The price is split: royalty -> collection creator,
 52// fee -> platform, remainder -> seller. The NFT is transferred to the buyer.
 53func Buy(cur realm, collID, tokenID string) {
 54	assertUserCall(cur)
 55	coll := mustGetCollection(collID)
 56	buyer := cur.Previous().Address()
 57	tid := grc721.TokenID(tokenID)
 58
 59	l, ok := getListing(collID, tokenID)
 60	if !ok {
 61		panic("token is not listed for sale")
 62	}
 63	if buyer == l.seller {
 64		panic("cannot buy your own listing")
 65	}
 66
 67	paid := receivedUgnot()
 68	if paid != l.price {
 69		panic(ufmt.Sprintf("must send exactly %d ugnot to buy (sent %d)", l.price, paid))
 70	}
 71
 72	// Split the proceeds. royaltyBps (<=5000) + feeBps (<=1000) < 10000, so the
 73	// seller always nets a positive amount.
 74	royalty := l.price * coll.royaltyBps / bpsDenominator
 75	fee := l.price * feeBps / bpsDenominator
 76	sellerAmt := l.price - royalty - fee
 77
 78	// Hand the NFT to the buyer and drop the listing before paying out.
 79	if err := coll.nft.TransferFrom(cur.Address(), cur.Address(), buyer, tid); err != nil {
 80		panic(err)
 81	}
 82	listings.Remove(listingKey(collID, tokenID))
 83
 84	feePot += fee
 85	payout(cur, coll.creator, royalty)
 86	payout(cur, l.seller, sellerAmt)
 87
 88	chain.Emit("Buy", "collection", collID, "tokenId", tokenID,
 89		"buyer", buyer.String(), "seller", l.seller.String(),
 90		"price", strconv.FormatInt(l.price, 10))
 91}
 92
 93// CancelListing delists a token and returns it from escrow to the seller.
 94func CancelListing(cur realm, collID, tokenID string) {
 95	assertUserCall(cur)
 96	coll := mustGetCollection(collID)
 97	caller := cur.Previous().Address()
 98
 99	l, ok := getListing(collID, tokenID)
100	if !ok {
101		panic("token is not listed")
102	}
103	if caller != l.seller {
104		panic("only the seller can cancel the listing")
105	}
106
107	if err := coll.nft.TransferFrom(cur.Address(), cur.Address(), l.seller, grc721.TokenID(tokenID)); err != nil {
108		panic(err)
109	}
110	listings.Remove(listingKey(collID, tokenID))
111
112	chain.Emit("Cancel", "collection", collID, "tokenId", tokenID, "seller", l.seller.String())
113}
114
115// UpdatePrice changes the asking price of an active listing. Seller only.
116func UpdatePrice(cur realm, collID, tokenID string, newPrice int64) {
117	assertUserCall(cur)
118	if newPrice <= 0 {
119		panic("price must be positive")
120	}
121	caller := cur.Previous().Address()
122
123	l, ok := getListing(collID, tokenID)
124	if !ok {
125		panic("token is not listed")
126	}
127	if caller != l.seller {
128		panic("only the seller can update the price")
129	}
130	l.price = newPrice
131
132	chain.Emit("UpdatePrice", "collection", collID, "tokenId", tokenID,
133		"price", strconv.FormatInt(newPrice, 10))
134}
135
136// --- read-only views (callable via vm/qeval) --------------------------------
137
138// OwnerOf returns the on-chain owner of a token, or the zero address if the
139// token does not exist. A listed token is owned by the realm (escrow).
140func OwnerOf(collID, tokenID string) address {
141	coll, ok := collections.Get(collID)
142	if !ok {
143		return zeroAddr
144	}
145	owner, err := coll.(*Collection).nft.OwnerOf(grc721.TokenID(tokenID))
146	if err != nil {
147		return zeroAddr
148	}
149	return owner
150}
151
152// GetListing returns the seller, price and whether the token is currently listed.
153func GetListing(collID, tokenID string) (seller address, price int64, listed bool) {
154	l, ok := getListing(collID, tokenID)
155	if !ok {
156		return zeroAddr, 0, false
157	}
158	return l.seller, l.price, true
159}
160
161// TokenURI returns the metadata URI for a token (baseURI + tokenID).
162func TokenURI(collID, tokenID string) string {
163	coll, ok := collections.Get(collID)
164	if !ok {
165		return ""
166	}
167	return coll.(*Collection).baseURI + tokenID
168}
169
170// CollectionInfo returns the public configuration of a collection.
171func CollectionInfo(id string) (name, symbol string, creator address, mintPrice, maxSupply, minted, royaltyBps int64) {
172	coll := mustGetCollection(id)
173	return coll.name, coll.symbol, coll.creator, coll.mintPrice, coll.maxSupply, coll.minted, coll.royaltyBps
174}