Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}