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}