Search Apps Documentation Source Content File Folder Download Copy Actions Download

market.gno

5.88 Kb · 186 lines
  1// Package gnogle_market is the upgradeable marketplace layer of GNOGLE. It holds
  2// listings/auctions/offers/sales/fees but NEVER holds NFTs — tokens stay in
  3// their owners' wallets in the gnogle_nft realm. The market settles trades by
  4// calling gnogle_nft.TransferFrom as an approved operator. Because no NFT lives
  5// here, this realm can be redeployed/upgraded freely without affecting any NFT.
  6package gnogle_market2
  7
  8import (
  9	"chain"
 10	"chain/banker"
 11	"chain/runtime"
 12	"chain/runtime/unsafe"
 13	"strconv"
 14
 15	"gno.land/p/nt/avl/v0"
 16	"gno.land/p/nt/ufmt/v0"
 17	nft "gno.land/r/g18wk4a80cr7dqa25vfka2yug5n3pd50udled6y3/gnogle_nft2"
 18)
 19
 20const (
 21	ugnot          = "ugnot"
 22	bpsDenominator = 10000
 23	maxFeeBps      = 1000
 24)
 25
 26var zeroAddr address
 27
 28// Listing is a fixed-price sale. The NFT is NOT escrowed; it stays with the
 29// seller, who has approved this market as operator.
 30type Listing struct {
 31	collID    string
 32	tokenID   string
 33	seller    address
 34	price     int64
 35	createdAt int64
 36}
 37
 38var (
 39	listings avl.Tree // "collID/tokenID" -> *Listing
 40	admin    address
 41	feeBps   int64 = 250 // 2.5%
 42	feePot   int64
 43)
 44
 45// List puts a token the caller owns up for sale. The caller must first approve
 46// this market on gnogle_nft (SetApprovalForAll); the NFT stays in their wallet.
 47func List(cur realm, collID, tokenID string, price int64) {
 48	assertUserCall(cur)
 49	if price <= 0 {
 50		panic("price must be positive")
 51	}
 52	seller := cur.Previous().Address()
 53	requireOwnerAndApproval(cur, collID, tokenID, seller)
 54	if _, ok := getListing(collID, tokenID); ok {
 55		panic("token is already listed")
 56	}
 57	if _, ok := getAuction(collID, tokenID); ok {
 58		panic("token is in an auction")
 59	}
 60	listings.Set(key(collID, tokenID), &Listing{collID, tokenID, seller, price, runtime.ChainHeight()})
 61	chain.Emit("List", "collection", collID, "tokenId", tokenID, "seller", seller.String(), "price", strconv.FormatInt(price, 10))
 62}
 63
 64// Buy purchases a listed token. The caller attaches exactly the price in ugnot.
 65func Buy(cur realm, collID, tokenID string) {
 66	assertUserCall(cur)
 67	buyer := cur.Previous().Address()
 68	l, ok := getListing(collID, tokenID)
 69	if !ok {
 70		panic("token is not listed for sale")
 71	}
 72	if buyer == l.seller {
 73		panic("cannot buy your own listing")
 74	}
 75	paid := receivedUgnot()
 76	if paid != l.price {
 77		panic(ufmt.Sprintf("must send exactly %d ugnot to buy (sent %d)", l.price, paid))
 78	}
 79	if nft.OwnerOf(collID, tokenID) != l.seller {
 80		listings.Remove(key(collID, tokenID))
 81		panic("listing is stale: the seller no longer owns this token")
 82	}
 83	nft.TransferFrom(cross(cur), collID, l.seller, buyer, tokenID)
 84	listings.Remove(key(collID, tokenID))
 85	settle(cur, collID, tokenID, l.seller, buyer, l.price, "buy")
 86	chain.Emit("Buy", "collection", collID, "tokenId", tokenID, "buyer", buyer.String(), "seller", l.seller.String(), "price", strconv.FormatInt(l.price, 10))
 87}
 88
 89// CancelListing delists a token (no NFT moves — it never left the seller).
 90func CancelListing(cur realm, collID, tokenID string) {
 91	assertUserCall(cur)
 92	caller := cur.Previous().Address()
 93	l, ok := getListing(collID, tokenID)
 94	if !ok {
 95		panic("token is not listed")
 96	}
 97	if caller != l.seller {
 98		panic("only the seller can cancel the listing")
 99	}
100	listings.Remove(key(collID, tokenID))
101	chain.Emit("Cancel", "collection", collID, "tokenId", tokenID)
102}
103
104// UpdatePrice changes a listing's price. Seller only.
105func UpdatePrice(cur realm, collID, tokenID string, newPrice int64) {
106	assertUserCall(cur)
107	if newPrice <= 0 {
108		panic("price must be positive")
109	}
110	caller := cur.Previous().Address()
111	l, ok := getListing(collID, tokenID)
112	if !ok {
113		panic("token is not listed")
114	}
115	if caller != l.seller {
116		panic("only the seller can update the price")
117	}
118	l.price = newPrice
119	chain.Emit("UpdatePrice", "collection", collID, "tokenId", tokenID, "price", strconv.FormatInt(newPrice, 10))
120}
121
122// --- shared helpers ---------------------------------------------------------
123
124// settle splits price into royalty (creator) + fee (platform) + seller, pays
125// them out, and records the sale.
126func settle(cur realm, collID, tokenID string, seller, buyer address, price int64, kind string) {
127	creator, royaltyBps := nft.CollectionRoyalty(collID)
128	royalty := price * royaltyBps / bpsDenominator
129	fee := price * feeBps / bpsDenominator
130	sellerAmt := price - royalty - fee
131	feePot += fee
132	payout(cur, creator, royalty)
133	payout(cur, seller, sellerAmt)
134	recordSale(collID, tokenID, seller, buyer, price, kind)
135}
136
137// requireOwnerAndApproval verifies the caller owns the token AND has approved
138// this market as operator on gnogle_nft.
139// marketApproved reports whether this market may move (collID,tokenID) — either
140// the per-token approval points at us, or the owner approved us for all.
141func marketApproved(cur realm, collID, tokenID string, owner address) bool {
142	return nft.GetApproved(collID, tokenID) == cur.Address() || nft.IsApprovedForAll(owner, cur.Address())
143}
144
145func requireOwnerAndApproval(cur realm, collID, tokenID string, owner address) {
146	if nft.OwnerOf(collID, tokenID) != owner {
147		panic("you are not the current owner of this token")
148	}
149	if !marketApproved(cur, collID, tokenID, owner) {
150		panic("approve this token to the marketplace first: call gnogle_nft.Approve(collection, tokenId, <market>)")
151	}
152}
153
154func assertUserCall(cur realm) {
155	if !cur.Previous().IsUserCall() {
156		panic("only direct user calls are accepted")
157	}
158}
159
160func receivedUgnot() int64 {
161	sent := unsafe.OriginSend()
162	for _, c := range sent {
163		if c.Denom != ugnot {
164			panic("only ugnot is accepted")
165		}
166	}
167	return sent.AmountOf(ugnot)
168}
169
170func payout(cur realm, to address, amount int64) {
171	if amount <= 0 {
172		return
173	}
174	b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
175	b.SendCoins(cur.Address(), to, chain.Coins{chain.NewCoin(ugnot, amount)})
176}
177
178func key(collID, tokenID string) string { return collID + "/" + tokenID }
179
180func getListing(collID, tokenID string) (*Listing, bool) {
181	v, ok := listings.Get(key(collID, tokenID))
182	if !ok {
183		return nil, false
184	}
185	return v.(*Listing), true
186}