gnopensea.gno
11.99 Kb ยท 497 lines
1package gnopensea9
2
3import (
4 "strconv"
5
6 "chain"
7 "chain/runtime"
8 "chain/banker"
9 "strings"
10
11 "gno.land/p/nt/avl"
12 "gno.land/p/demo/tokens/grc721"
13 "gno.land/p/nt/ufmt"
14 "gno.land/r/pierre115/nftregistry8"
15)
16
17
18var (
19 listings avl.Tree
20 sales avl.Tree
21 nextListingId = 1
22 nextSaleId = 1
23
24 marketplaceFee = int64(250) // 250 basis points = 2.5%
25 marketplaceAddr = runtime.CurrentRealm().Address()
26 admin = address("g1e7r5eglptfejknps3xv2kxv6pc8wvd3wwtw6tg")
27)
28
29// CreateListing - List an NFT for sale using the global NFT Registry
30func CreateListing(_ realm, nftRealmAddress address, tokenId grc721.TokenID, price int64) int {
31 caller := runtime.OriginCaller()
32
33 if price <= 0 || price > 99999999999 {
34 panic("Price not valid")
35 }
36
37 // Check if collection is registered in the GLOBAL NFT Registry
38 if !nftregistry8.IsRegistered(nftRealmAddress) {
39 panic("NFT collection not registered in the global NFT Registry. The collection must call nftregistry.RegisterCollection() first.")
40 }
41
42 // Get NFT getter from the global registry
43 getter, exists := nftregistry8.GetNFTGetter(nftRealmAddress)
44 if !exists {
45 panic("Failed to get NFT getter from registry")
46 }
47
48 nftInstance := getter()
49
50 // Verify caller is the owner
51 owner, err := nftInstance.OwnerOf(tokenId)
52 if err != nil {
53 panic("Token not found: " + err.Error())
54 }
55
56 if owner != caller {
57 panic("You are not the owner of this NFT")
58 }
59
60 // Verify approvals
61 marketplaceAddr := runtime.CurrentRealm().Address()
62 approvedAddr, err := nftInstance.GetApproved(tokenId)
63 isApprovedForToken := (err == nil && approvedAddr == marketplaceAddr)
64 isApprovedForAll := nftInstance.IsApprovedForAll(owner, marketplaceAddr)
65
66 if !isApprovedForToken && !isApprovedForAll {
67 panic("You must first approve the marketplace.\nUse Approve() or SetApprovalForAll()")
68 }
69
70 // Create listing
71 listing := &Listing{
72 ListingId: nextListingId,
73 NFTGetter: getter,
74 TokenId: tokenId,
75 Seller: caller,
76 Price: price,
77 Active: true,
78 ListedAt: runtime.ChainHeight(),
79 NFTRealmAddress: nftRealmAddress,
80 }
81
82 listings.Set(strconv.Itoa(nextListingId), listing)
83 nextListingId++
84
85 return listing.ListingId
86}
87
88// BuyNFT - Purchase a listed NFT with automatic royalty calculation
89func BuyNFT(_ realm, listingId int) {
90 caller := runtime.PreviousRealm().Address()
91 sent := banker.OriginSend()
92
93 // Get listing
94 listing := getListing(listingId)
95 if listing == nil {
96 panic("Listing not found")
97 }
98
99 if !listing.Active {
100 panic("This listing is no longer active")
101 }
102
103 // Verify payment
104 amount := sent.AmountOf("ugnot")
105 if amount < listing.Price {
106 panic(ufmt.Sprintf("Insufficient amount. Price: %d ugnot", listing.Price))
107 }
108
109 // Get NFT instance
110 nftInstance := listing.NFTGetter()
111
112 // Verify seller still owns the NFT
113 currentOwner, err := nftInstance.OwnerOf(listing.TokenId)
114 if err != nil {
115 panic("NFT not found: " + err.Error())
116 }
117 if currentOwner != listing.Seller {
118 panic("Seller no longer owns this NFT")
119 }
120
121 // Calculate royalties
122 var royaltyAmount int64 = 0
123 var royaltyReceiver address
124
125 // Check if NFT supports royalties
126 if royaltyNFT, ok := nftInstance.(grc721.IGRC2981); ok {
127 addr, amount, err := royaltyNFT.RoyaltyInfo(listing.TokenId, listing.Price)
128 if err == nil {
129 royaltyAmount = amount
130 royaltyReceiver = addr
131 }
132 }
133
134 // Calculate marketplace fee
135 marketplaceFeeAmount := (listing.Price * marketplaceFee) / 10000
136
137 // Calculate what seller receives
138 sellerAmount := listing.Price - marketplaceFeeAmount - royaltyAmount
139
140 if sellerAmount < 0 {
141 panic("Error: fees + royalties exceed sale price")
142 }
143
144 // Distribute payments
145 bnkr := banker.NewBanker(banker.BankerTypeRealmSend)
146 realmAddr := runtime.CurrentRealm().Address()
147
148 // 1. Pay seller
149 if sellerAmount > 0 {
150 sellerCoins := chain.Coins{chain.Coin{"ugnot", sellerAmount}}
151 bnkr.SendCoins(realmAddr, listing.Seller, sellerCoins)
152 }
153
154 // 2. Pay royalties to creator
155 if royaltyAmount > 0 && royaltyReceiver != "" {
156 royaltyCoins := chain.Coins{chain.Coin{"ugnot", royaltyAmount}}
157 bnkr.SendCoins(realmAddr, royaltyReceiver, royaltyCoins)
158 }
159
160 // 3. TODO : Marketplace fees remain in contract (withdrawal by admin)
161
162 // 4. Refund excess
163 if amount > listing.Price {
164 excess := amount - listing.Price
165 excessCoins := chain.Coins{chain.Coin{"ugnot", excess}}
166 bnkr.SendCoins(realmAddr, caller, excessCoins)
167 }
168
169 // Transfer NFT
170 err = nftInstance.TransferFrom(listing.Seller, caller, listing.TokenId)
171 if err != nil {
172 panic("Transfer error: " + err.Error())
173 }
174
175 // Record sale
176 sale := &Sale{
177 ListingId: listingId,
178 TokenId: listing.TokenId.String(),
179 Buyer: caller,
180 Seller: listing.Seller,
181 Price: listing.Price,
182 MarketplaceFee: marketplaceFeeAmount,
183 RoyaltyFee: royaltyAmount,
184 RoyaltyReceiver: royaltyReceiver,
185 SoldAt: runtime.ChainHeight(),
186 }
187
188 sales.Set(strconv.Itoa(nextSaleId), sale)
189 nextSaleId++
190
191 // Deactivate listing
192 listing.Active = false
193 listings.Set(strconv.Itoa(listingId), listing)
194}
195
196// CancelListing - Cancel a listing
197func CancelListing(_realm, listingId int) {
198 caller := runtime.PreviousRealm().Address()
199
200 listing := getListing(listingId)
201 if listing == nil {
202 panic("Listing not found")
203 }
204
205 if listing.Seller != caller {
206 panic("Only the seller can cancel this listing")
207 }
208
209 if !listing.Active {
210 panic("This listing is already inactive")
211 }
212
213 listing.Active = false
214 listings.Set(strconv.Itoa(listingId), listing)
215}
216
217// UpdatePrice - Update listing price
218func UpdatePrice(_ realm, listingId int, newPrice int64) {
219 caller := runtime.PreviousRealm().Address()
220
221 if newPrice <= 0 {
222 panic("Price must be positive")
223 }
224
225 listing := getListing(listingId)
226 if listing == nil {
227 panic("Listing not found")
228 }
229
230 if listing.Seller != caller {
231 panic("Only the seller can modify this listing")
232 }
233
234 if !listing.Active {
235 panic("This listing is no longer active")
236 }
237
238 listing.Price = newPrice
239 listings.Set(strconv.Itoa(listingId), listing)
240}
241
242// ============= READ FUNCTIONS =============
243
244func GetListing(listingId int) (int, string, int64, string, bool, int64) {
245 listing := getListing(listingId)
246 if listing == nil {
247 return 0, "", 0, "", false, 0
248 }
249
250 return listing.ListingId,
251 listing.TokenId.String(),
252 listing.Price,
253 listing.Seller.String(),
254 listing.Active,
255 listing.ListedAt
256}
257
258func GetSale(saleId int) (int, string, string, string, int64, int64, int64, string, int64) {
259 value, exists := sales.Get(strconv.Itoa(saleId))
260 if !exists {
261 return 0, "", "", "", 0, 0, 0, "", 0
262 }
263
264 sale := value.(*Sale)
265 return sale.ListingId,
266 sale.TokenId,
267 sale.Buyer.String(),
268 sale.Seller.String(),
269 sale.Price,
270 sale.MarketplaceFee,
271 sale.RoyaltyFee,
272 sale.RoyaltyReceiver.String(),
273 sale.SoldAt
274}
275
276func GetActiveListingsCount() int {
277 count := 0
278 listings.Iterate("", "", func(key string, value interface{}) bool {
279 listing := value.(*Listing)
280 if listing.Active {
281 count++
282 }
283 return false
284 })
285 return count
286}
287
288func GetTotalSales() int {
289 return sales.Size()
290}
291
292func GetTotalVolume() int64 {
293 var total int64 = 0
294 sales.Iterate("", "", func(key string, value interface{}) bool {
295 sale := value.(*Sale)
296 total += sale.Price
297 return false
298 })
299 return total
300}
301
302func GetTotalRoyaltiesPaid() int64 {
303 var total int64 = 0
304 sales.Iterate("", "", func(key string, value interface{}) bool {
305 sale := value.(*Sale)
306 total += sale.RoyaltyFee
307 return false
308 })
309 return total
310}
311
312func GetMarketplaceFee() int64 {
313 return marketplaceFee
314}
315
316func GetMarketplaceAddress() address {
317 return runtime.CurrentRealm().Address()
318}
319
320// GetRoyaltyBreakdown - Calculate distribution for a listing
321func GetRoyaltyBreakdown(listingId int) (int64, int64, int64, address) {
322 listing := getListing(listingId)
323 if listing == nil {
324 return 0, 0, 0, ""
325 }
326
327 // Calculate marketplace fee
328 marketplaceFeeAmount := (listing.Price * marketplaceFee) / 10000
329
330 // Calculate royalties if supported
331 nftInstance := listing.NFTGetter()
332 var royaltyAmount int64 = 0
333 var royaltyReceiver address
334
335 if royaltyNFT, ok := nftInstance.(grc721.IGRC2981); ok {
336 addr, amount, err := royaltyNFT.RoyaltyInfo(listing.TokenId, listing.Price)
337 if err == nil {
338 royaltyAmount = amount
339 royaltyReceiver = addr
340 }
341 }
342
343 sellerAmount := listing.Price - marketplaceFeeAmount - royaltyAmount
344
345 return sellerAmount, marketplaceFeeAmount, royaltyAmount, royaltyReceiver
346}
347
348func GetBalance() int64 {
349 bnkr := banker.NewBanker(banker.BankerTypeRealmSend)
350 realmAddr := runtime.CurrentRealm().Address()
351 balance := bnkr.GetCoins(realmAddr)
352 return balance.AmountOf("ugnot")
353}
354
355// ============= READ FUNCTIONS FOR FRONTEND =============
356
357// GetAllListings - Return all active listings
358func GetAllListings() string {
359 var listingsData []string
360
361 listings.Iterate("", "", func(key string, value interface{}) bool {
362 listing := value.(*Listing)
363 if listing.Active {
364 // Format: listingId|nftAddress|tokenId|price|seller
365 listingData := ufmt.Sprintf("%d|%s|%s|%d|%s",
366 listing.ListingId,
367 listing.NFTRealmAddress.String(),
368 listing.TokenId.String(),
369 listing.Price,
370 listing.Seller.String(),
371 )
372 listingsData = append(listingsData, listingData)
373 }
374 return false
375 })
376
377 if len(listingsData) == 0 {
378 return "No active listings"
379 }
380
381 return strings.Join(listingsData, "\n")
382}
383
384// GetListingDetails - Get detailed info about a listing
385func GetListingDetails(listingId int) string {
386 listing := getListing(listingId)
387 if listing == nil {
388 return "Listing not found"
389 }
390
391 nftInstance := listing.NFTGetter()
392
393 // Get NFT metadata if available
394 var name, uri string
395 if metaNFT, ok := nftInstance.(grc721.IGRC721Metadata); ok {
396 name = metaNFT.Name()
397 if tokenURI, err := metaNFT.TokenURI(listing.TokenId); err == nil {
398 uri = tokenURI
399 }
400 }
401
402 // Format JSON-like response
403 output := ufmt.Sprintf(`{
404 "listingId": %d,
405 "tokenId": "%s",
406 "price": %d,
407 "seller": "%s",
408 "active": %t,
409 "name": "%s",
410 "uri": "%s",
411 "listedAt": %d
412}`,
413 listing.ListingId,
414 listing.TokenId.String(),
415 listing.Price,
416 listing.Seller.String(),
417 listing.Active,
418 name,
419 uri,
420 listing.ListedAt,
421 )
422
423 return output
424}
425
426// GetUserListings - Get all listings by a specific seller
427func GetUserListings(sellerAddr address) string {
428 var userListings []string
429
430 listings.Iterate("", "", func(key string, value interface{}) bool {
431 listing := value.(*Listing)
432 if listing.Seller == sellerAddr && listing.Active {
433 listingData := ufmt.Sprintf("%d|%s|%d",
434 listing.ListingId,
435 listing.TokenId.String(),
436 listing.Price,
437 )
438 userListings = append(userListings, listingData)
439 }
440 return false
441 })
442
443 if len(userListings) == 0 {
444 return "No active listings"
445 }
446
447 return strings.Join(userListings, "\n")
448}
449
450// GetMarketplaceStats - Get overall marketplace statistics
451func GetMarketplaceStats() string {
452 activeCount := GetActiveListingsCount()
453 totalSales := GetTotalSales()
454 totalVolume := GetTotalVolume()
455
456 output := ufmt.Sprintf(`{
457 "activeListings": %d,
458 "totalSales": %d,
459 "totalVolume": %d,
460 "marketplaceFee": %d
461}`,
462 activeCount,
463 totalSales,
464 totalVolume,
465 marketplaceFee,
466 )
467
468 return output
469}
470
471// ============= HELPERS =============
472
473func getListing(listingId int) *Listing {
474 value, exists := listings.Get(strconv.Itoa(listingId))
475 if !exists {
476 return nil
477 }
478 return value.(*Listing)
479}
480
481func formatPrice(ugnot int64) string {
482 gnot := float64(ugnot) / 1000000.0
483 return ufmt.Sprintf("%.2f GNOT", gnot)
484}
485
486func formatFee(basisPoints int64) string {
487 percent := float64(basisPoints) / 100.0
488 return ufmt.Sprintf("%.2f%%", percent)
489}
490
491func formatPercentage(value, total int64) string {
492 if total == 0 {
493 return "0%"
494 }
495 percent := (float64(value) / float64(total)) * 100.0
496 return ufmt.Sprintf("%.1f%%", percent)
497}