Search Apps Documentation Source Content File Folder Download Copy Actions Download

gnopensea.gno

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