Search Apps Documentation Source Content File Folder Download Copy Actions Download

gnopensea.gno

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