Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}