gnopensea.gno
14.04 Kb ยท 565 lines
1package gnopensea11
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.OriginCaller()
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// BuyNFTDebug - Version debug de BuyNFT
218func BuyNFTDebug(_ realm, listingId int) string {
219 output := "=== BuyNFT Debug ===\n\n"
220
221 // Step 1
222 caller := runtime.OriginCaller()
223 output += ufmt.Sprintf("1. Caller: %s\n", caller.String())
224
225 // Step 2
226 sent := banker.OriginSend()
227 amount := sent.AmountOf("ugnot")
228 output += ufmt.Sprintf("2. Amount sent: %d ugnot\n", amount)
229
230 // Step 3
231 listing := getListing(listingId)
232 if listing == nil {
233 output += "3. ERROR: Listing not found\n"
234 return output
235 }
236 output += ufmt.Sprintf("3. Listing found, price: %d\n", listing.Price)
237 output += ufmt.Sprintf(" Seller: %s\n", listing.Seller.String())
238 output += ufmt.Sprintf(" Active: %t\n", listing.Active)
239
240 // Step 4
241 if !listing.Active {
242 output += "4. ERROR: Listing not active\n"
243 return output
244 }
245 output += "4. Listing is active\n"
246
247 // Step 5
248 if amount < listing.Price {
249 output += ufmt.Sprintf("5. ERROR: Insufficient amount (sent: %d, need: %d)\n", amount, listing.Price)
250 return output
251 }
252 output += "5. Payment verified\n"
253
254 // Step 6
255 if caller == listing.Seller {
256 output += "6. ERROR: Cannot buy your own NFT\n"
257 return output
258 }
259 output += "6. Buyer is not seller\n"
260
261 // Step 7
262 nftInstance := listing.NFTGetter()
263 output += "7. NFT instance retrieved\n"
264
265 // Step 8
266 currentOwner, err := nftInstance.OwnerOf(listing.TokenId)
267 if err != nil {
268 output += ufmt.Sprintf("8. ERROR: %s\n", err.Error())
269 return output
270 }
271 output += ufmt.Sprintf("8. Current owner: %s\n", currentOwner.String())
272
273 // Step 9
274 if currentOwner != listing.Seller {
275 output += "9. ERROR: Seller no longer owns NFT\n"
276 return output
277 }
278 output += "9. Seller still owns NFT\n"
279
280 output += "\nโ
All checks passed - ready to execute transfer\n"
281
282 return output
283}
284
285// UpdatePrice - Update listing price
286func UpdatePrice(_ realm, listingId int, newPrice int64) {
287 caller := runtime.PreviousRealm().Address()
288
289 if newPrice <= 0 {
290 panic("Price must be positive")
291 }
292
293 listing := getListing(listingId)
294 if listing == nil {
295 panic("Listing not found")
296 }
297
298 if listing.Seller != caller {
299 panic("Only the seller can modify this listing")
300 }
301
302 if !listing.Active {
303 panic("This listing is no longer active")
304 }
305
306 listing.Price = newPrice
307 listings.Set(strconv.Itoa(listingId), listing)
308}
309
310// ============= READ FUNCTIONS =============
311
312func GetListing(listingId int) (int, string, int64, string, bool, int64) {
313 listing := getListing(listingId)
314 if listing == nil {
315 return 0, "", 0, "", false, 0
316 }
317
318 return listing.ListingId,
319 listing.TokenId.String(),
320 listing.Price,
321 listing.Seller.String(),
322 listing.Active,
323 listing.ListedAt
324}
325
326func GetSale(saleId int) (int, string, string, string, int64, int64, int64, string, int64) {
327 value, exists := sales.Get(strconv.Itoa(saleId))
328 if !exists {
329 return 0, "", "", "", 0, 0, 0, "", 0
330 }
331
332 sale := value.(*Sale)
333 return sale.ListingId,
334 sale.TokenId,
335 sale.Buyer.String(),
336 sale.Seller.String(),
337 sale.Price,
338 sale.MarketplaceFee,
339 sale.RoyaltyFee,
340 sale.RoyaltyReceiver.String(),
341 sale.SoldAt
342}
343
344func GetActiveListingsCount() int {
345 count := 0
346 listings.Iterate("", "", func(key string, value interface{}) bool {
347 listing := value.(*Listing)
348 if listing.Active {
349 count++
350 }
351 return false
352 })
353 return count
354}
355
356func GetTotalSales() int {
357 return sales.Size()
358}
359
360func GetTotalVolume() int64 {
361 var total int64 = 0
362 sales.Iterate("", "", func(key string, value interface{}) bool {
363 sale := value.(*Sale)
364 total += sale.Price
365 return false
366 })
367 return total
368}
369
370func GetTotalRoyaltiesPaid() int64 {
371 var total int64 = 0
372 sales.Iterate("", "", func(key string, value interface{}) bool {
373 sale := value.(*Sale)
374 total += sale.RoyaltyFee
375 return false
376 })
377 return total
378}
379
380func GetMarketplaceFee() int64 {
381 return marketplaceFee
382}
383
384func GetMarketplaceAddress() address {
385 return runtime.CurrentRealm().Address()
386}
387
388// GetRoyaltyBreakdown - Calculate distribution for a listing
389func GetRoyaltyBreakdown(listingId int) (int64, int64, int64, address) {
390 listing := getListing(listingId)
391 if listing == nil {
392 return 0, 0, 0, ""
393 }
394
395 // Calculate marketplace fee
396 marketplaceFeeAmount := (listing.Price * marketplaceFee) / 10000
397
398 // Calculate royalties if supported
399 nftInstance := listing.NFTGetter()
400 var royaltyAmount int64 = 0
401 var royaltyReceiver address
402
403 if royaltyNFT, ok := nftInstance.(grc721.IGRC2981); ok {
404 addr, amount, err := royaltyNFT.RoyaltyInfo(listing.TokenId, listing.Price)
405 if err == nil {
406 royaltyAmount = amount
407 royaltyReceiver = addr
408 }
409 }
410
411 sellerAmount := listing.Price - marketplaceFeeAmount - royaltyAmount
412
413 return sellerAmount, marketplaceFeeAmount, royaltyAmount, royaltyReceiver
414}
415
416func GetBalance() int64 {
417 bnkr := banker.NewBanker(banker.BankerTypeRealmSend)
418 realmAddr := runtime.CurrentRealm().Address()
419 balance := bnkr.GetCoins(realmAddr)
420 return balance.AmountOf("ugnot")
421}
422
423// ============= READ FUNCTIONS FOR FRONTEND =============
424
425// GetAllListings - Return all active listings
426func GetAllListings() string {
427 var listingsData []string
428
429 listings.Iterate("", "", func(key string, value interface{}) bool {
430 listing := value.(*Listing)
431 if listing.Active {
432 // Format: listingId|nftAddress|tokenId|price|seller
433 listingData := ufmt.Sprintf("%d|%s|%s|%d|%s",
434 listing.ListingId,
435 listing.NFTRealmAddress.String(),
436 listing.TokenId.String(),
437 listing.Price,
438 listing.Seller.String(),
439 )
440 listingsData = append(listingsData, listingData)
441 }
442 return false
443 })
444
445 if len(listingsData) == 0 {
446 return "No active listings"
447 }
448
449 return strings.Join(listingsData, "\n")
450}
451
452// GetListingDetails - Get detailed info about a listing
453func GetListingDetails(listingId int) string {
454 listing := getListing(listingId)
455 if listing == nil {
456 return "Listing not found"
457 }
458
459 nftInstance := listing.NFTGetter()
460
461 // Get NFT metadata if available
462 var name, uri string
463 if metaNFT, ok := nftInstance.(grc721.IGRC721Metadata); ok {
464 name = metaNFT.Name()
465 if tokenURI, err := metaNFT.TokenURI(listing.TokenId); err == nil {
466 uri = tokenURI
467 }
468 }
469
470 // Format JSON-like response
471 output := ufmt.Sprintf(`{
472 "listingId": %d,
473 "tokenId": "%s",
474 "price": %d,
475 "seller": "%s",
476 "active": %t,
477 "name": "%s",
478 "uri": "%s",
479 "listedAt": %d
480}`,
481 listing.ListingId,
482 listing.TokenId.String(),
483 listing.Price,
484 listing.Seller.String(),
485 listing.Active,
486 name,
487 uri,
488 listing.ListedAt,
489 )
490
491 return output
492}
493
494// GetUserListings - Get all listings by a specific seller
495func GetUserListings(sellerAddr address) string {
496 var userListings []string
497
498 listings.Iterate("", "", func(key string, value interface{}) bool {
499 listing := value.(*Listing)
500 if listing.Seller == sellerAddr && listing.Active {
501 listingData := ufmt.Sprintf("%d|%s|%d",
502 listing.ListingId,
503 listing.TokenId.String(),
504 listing.Price,
505 )
506 userListings = append(userListings, listingData)
507 }
508 return false
509 })
510
511 if len(userListings) == 0 {
512 return "No active listings"
513 }
514
515 return strings.Join(userListings, "\n")
516}
517
518// GetMarketplaceStats - Get overall marketplace statistics
519func GetMarketplaceStats() string {
520 activeCount := GetActiveListingsCount()
521 totalSales := GetTotalSales()
522 totalVolume := GetTotalVolume()
523
524 output := ufmt.Sprintf(`{
525 "activeListings": %d,
526 "totalSales": %d,
527 "totalVolume": %d,
528 "marketplaceFee": %d
529}`,
530 activeCount,
531 totalSales,
532 totalVolume,
533 marketplaceFee,
534 )
535
536 return output
537}
538
539// ============= HELPERS =============
540
541func getListing(listingId int) *Listing {
542 value, exists := listings.Get(strconv.Itoa(listingId))
543 if !exists {
544 return nil
545 }
546 return value.(*Listing)
547}
548
549func formatPrice(ugnot int64) string {
550 gnot := float64(ugnot) / 1000000.0
551 return ufmt.Sprintf("%.2f GNOT", gnot)
552}
553
554func formatFee(basisPoints int64) string {
555 percent := float64(basisPoints) / 100.0
556 return ufmt.Sprintf("%.2f%%", percent)
557}
558
559func formatPercentage(value, total int64) string {
560 if total == 0 {
561 return "0%"
562 }
563 percent := (float64(value) / float64(total)) * 100.0
564 return ufmt.Sprintf("%.1f%%", percent)
565}