market.gno
5.53 Kb · 180 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_market
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_nft"
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.
139func requireOwnerAndApproval(cur realm, collID, tokenID string, owner address) {
140 if nft.OwnerOf(collID, tokenID) != owner {
141 panic("you are not the current owner of this token")
142 }
143 if !nft.IsApprovedForAll(owner, cur.Address()) {
144 panic("approve the marketplace first: call gnogle_nft.SetApprovalForAll(<market>, true)")
145 }
146}
147
148func assertUserCall(cur realm) {
149 if !cur.Previous().IsUserCall() {
150 panic("only direct user calls are accepted")
151 }
152}
153
154func receivedUgnot() int64 {
155 sent := unsafe.OriginSend()
156 for _, c := range sent {
157 if c.Denom != ugnot {
158 panic("only ugnot is accepted")
159 }
160 }
161 return sent.AmountOf(ugnot)
162}
163
164func payout(cur realm, to address, amount int64) {
165 if amount <= 0 {
166 return
167 }
168 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
169 b.SendCoins(cur.Address(), to, chain.Coins{chain.NewCoin(ugnot, amount)})
170}
171
172func key(collID, tokenID string) string { return collID + "/" + tokenID }
173
174func getListing(collID, tokenID string) (*Listing, bool) {
175 v, ok := listings.Get(key(collID, tokenID))
176 if !ok {
177 return nil, false
178 }
179 return v.(*Listing), true
180}