Search Apps Documentation Source Content File Folder Download Copy Actions Download

market.gno

5.09 Kb · 175 lines
  1package gnogle_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	recordSale(collID, tokenID, l.seller, buyer, l.price, "buy")
 88
 89	chain.Emit("Buy", "collection", collID, "tokenId", tokenID,
 90		"buyer", buyer.String(), "seller", l.seller.String(),
 91		"price", strconv.FormatInt(l.price, 10))
 92}
 93
 94// CancelListing delists a token and returns it from escrow to the seller.
 95func CancelListing(cur realm, collID, tokenID string) {
 96	assertUserCall(cur)
 97	coll := mustGetCollection(collID)
 98	caller := cur.Previous().Address()
 99
100	l, ok := getListing(collID, tokenID)
101	if !ok {
102		panic("token is not listed")
103	}
104	if caller != l.seller {
105		panic("only the seller can cancel the listing")
106	}
107
108	if err := coll.nft.TransferFrom(cur.Address(), cur.Address(), l.seller, grc721.TokenID(tokenID)); err != nil {
109		panic(err)
110	}
111	listings.Remove(listingKey(collID, tokenID))
112
113	chain.Emit("Cancel", "collection", collID, "tokenId", tokenID, "seller", l.seller.String())
114}
115
116// UpdatePrice changes the asking price of an active listing. Seller only.
117func UpdatePrice(cur realm, collID, tokenID string, newPrice int64) {
118	assertUserCall(cur)
119	if newPrice <= 0 {
120		panic("price must be positive")
121	}
122	caller := cur.Previous().Address()
123
124	l, ok := getListing(collID, tokenID)
125	if !ok {
126		panic("token is not listed")
127	}
128	if caller != l.seller {
129		panic("only the seller can update the price")
130	}
131	l.price = newPrice
132
133	chain.Emit("UpdatePrice", "collection", collID, "tokenId", tokenID,
134		"price", strconv.FormatInt(newPrice, 10))
135}
136
137// --- read-only views (callable via vm/qeval) --------------------------------
138
139// OwnerOf returns the on-chain owner of a token, or the zero address if the
140// token does not exist. A listed token is owned by the realm (escrow).
141func OwnerOf(collID, tokenID string) address {
142	coll, ok := collections.Get(collID)
143	if !ok {
144		return zeroAddr
145	}
146	owner, err := coll.(*Collection).nft.OwnerOf(grc721.TokenID(tokenID))
147	if err != nil {
148		return zeroAddr
149	}
150	return owner
151}
152
153// GetListing returns the seller, price and whether the token is currently listed.
154func GetListing(collID, tokenID string) (seller address, price int64, listed bool) {
155	l, ok := getListing(collID, tokenID)
156	if !ok {
157		return zeroAddr, 0, false
158	}
159	return l.seller, l.price, true
160}
161
162// TokenURI returns the metadata URI for a token (baseURI + tokenID).
163func TokenURI(collID, tokenID string) string {
164	coll, ok := collections.Get(collID)
165	if !ok {
166		return ""
167	}
168	return coll.(*Collection).baseURI + tokenID
169}
170
171// CollectionInfo returns the public configuration of a collection.
172func CollectionInfo(id string) (name, symbol string, creator address, mintPrice, maxSupply, minted, royaltyBps int64) {
173	coll := mustGetCollection(id)
174	return coll.name, coll.symbol, coll.creator, coll.mintPrice, coll.maxSupply, coll.minted, coll.royaltyBps
175}