Search Apps Documentation Source Content File Folder Download Copy Actions Download

gnopensea.gno

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