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}