Search Apps Documentation Source Content File Folder Download Copy Actions Download

gnopensea.gno

12.09 Kb ยท 510 lines
  1package gnopendao9
  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
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 own listing (seller only)
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	// Only seller can cancel their own listing
216	// Admins/DAO use ProposeForceCancelListing instead
217	if listing.Seller != caller {
218		panic("Only the seller can cancel this listing. DAO members can create a ProposeForceCancelListing proposal.")
219	}
220
221	if !listing.Active {
222		panic("This listing is already inactive")
223	}
224
225	listing.Active = false
226	listings.Set(strconv.Itoa(listingId), listing)
227}
228
229
230// UpdatePrice - Update listing price
231func UpdatePrice(_ realm, listingId int, newPrice int64) {
232	caller := runtime.PreviousRealm().Address()
233
234	if newPrice <= 0 {
235		panic("Price must be positive")
236	}
237
238	listing := getListing(listingId)
239	if listing == nil {
240		panic("Listing not found")
241	}
242
243	if listing.Seller != caller {
244		panic("Only the seller can modify this listing")
245	}
246
247	if !listing.Active {
248		panic("This listing is no longer active")
249	}
250
251	listing.Price = newPrice
252	listings.Set(strconv.Itoa(listingId), listing)
253}
254
255// ============= READ FUNCTIONS =============
256
257func GetListing(listingId int) (int, string, int64, string, bool, int64) {
258	listing := getListing(listingId)
259	if listing == nil {
260		return 0, "", 0, "", false, 0
261	}
262
263	return listing.ListingId,
264		listing.TokenId.String(),
265		listing.Price,
266		listing.Seller.String(),
267		listing.Active,
268		listing.ListedAt
269}
270
271func GetSale(saleId int) (int, string, string, string, int64, int64, int64, string, int64) {
272	value, exists := sales.Get(strconv.Itoa(saleId))
273	if !exists {
274		return 0, "", "", "", 0, 0, 0, "", 0
275	}
276
277	sale := value.(*Sale)
278	return sale.ListingId,
279		sale.TokenId,
280		sale.Buyer.String(),
281		sale.Seller.String(),
282		sale.Price,
283		sale.MarketplaceFee,
284		sale.RoyaltyFee,
285		sale.RoyaltyReceiver.String(),
286		sale.SoldAt
287}
288
289func GetActiveListingsCount() int {
290	count := 0
291	listings.Iterate("", "", func(key string, value interface{}) bool {
292		listing := value.(*Listing)
293		if listing.Active {
294			count++
295		}
296		return false
297	})
298	return count
299}
300
301func GetTotalSales() int {
302	return sales.Size()
303}
304
305func GetTotalVolume() int64 {
306	var total int64 = 0
307	sales.Iterate("", "", func(key string, value interface{}) bool {
308		sale := value.(*Sale)
309		total += sale.Price
310		return false
311	})
312	return total
313}
314
315func GetTotalRoyaltiesPaid() int64 {
316	var total int64 = 0
317	sales.Iterate("", "", func(key string, value interface{}) bool {
318		sale := value.(*Sale)
319		total += sale.RoyaltyFee
320		return false
321	})
322	return total
323}
324
325func GetMarketplaceFee() int64 {
326	return marketplaceFee
327}
328
329func GetMarketplaceAddress() address {
330	return runtime.CurrentRealm().Address()
331}
332
333// GetRoyaltyBreakdown - Calculate distribution for a listing
334func GetRoyaltyBreakdown(listingId int) (int64, int64, int64, address) {
335	listing := getListing(listingId)
336	if listing == nil {
337		return 0, 0, 0, ""
338	}
339
340	// Calculate marketplace fee
341	marketplaceFeeAmount := (listing.Price * marketplaceFee) / 10000
342
343	// Calculate royalties if supported
344	nftInstance := listing.NFTGetter()
345	var royaltyAmount int64 = 0
346	var royaltyReceiver address
347
348	if royaltyNFT, ok := nftInstance.(grc721.IGRC2981); ok {
349		addr, amount, err := royaltyNFT.RoyaltyInfo(listing.TokenId, listing.Price)
350		if err == nil {
351			royaltyAmount = amount
352			royaltyReceiver = addr
353		}
354	}
355
356	sellerAmount := listing.Price - marketplaceFeeAmount - royaltyAmount
357
358	return sellerAmount, marketplaceFeeAmount, royaltyAmount, royaltyReceiver
359}
360
361func GetBalance() int64 {
362	bnkr := banker.NewBanker(banker.BankerTypeRealmSend)
363	realmAddr := runtime.CurrentRealm().Address()
364	balance := bnkr.GetCoins(realmAddr)
365	return balance.AmountOf("ugnot")
366}
367
368// ============= READ FUNCTIONS FOR FRONTEND =============
369
370// GetAllListings - Return all active listings
371func GetAllListings() string {
372	var listingsData []string
373
374	listings.Iterate("", "", func(key string, value interface{}) bool {
375		listing := value.(*Listing)
376		if listing.Active {
377			// Format: listingId|nftAddress|tokenId|price|seller
378			listingData := ufmt.Sprintf("%d|%s|%s|%d|%s",
379				listing.ListingId,
380				listing.NFTRealmAddress.String(),
381				listing.TokenId.String(),
382				listing.Price,
383				listing.Seller.String(),
384			)
385			listingsData = append(listingsData, listingData)
386		}
387		return false
388	})
389
390	if len(listingsData) == 0 {
391		return "No active listings"
392	}
393
394	return strings.Join(listingsData, "\n")
395}
396
397// GetListingDetails - Get detailed info about a listing
398func GetListingDetails(listingId int) string {
399	listing := getListing(listingId)
400	if listing == nil {
401		return "Listing not found"
402	}
403
404	nftInstance := listing.NFTGetter()
405
406	// Get NFT metadata if available
407	var name, uri string
408	if metaNFT, ok := nftInstance.(grc721.IGRC721Metadata); ok {
409		name = metaNFT.Name()
410		if tokenURI, err := metaNFT.TokenURI(listing.TokenId); err == nil {
411			uri = tokenURI
412		}
413	}
414
415	// Format JSON-like response
416	output := ufmt.Sprintf(`{
417  "listingId": %d,
418  "tokenId": "%s",
419  "price": %d,
420  "seller": "%s",
421  "active": %t,
422  "name": "%s",
423  "uri": "%s",
424  "listedAt": %d
425}`,
426		listing.ListingId,
427		listing.TokenId.String(),
428		listing.Price,
429		listing.Seller.String(),
430		listing.Active,
431		name,
432		uri,
433		listing.ListedAt,
434	)
435
436	return output
437}
438
439// GetUserListings - Get all listings by a specific seller
440func GetUserListings(sellerAddr address) string {
441	var userListings []string
442
443	listings.Iterate("", "", func(key string, value interface{}) bool {
444		listing := value.(*Listing)
445		if listing.Seller == sellerAddr && listing.Active {
446			listingData := ufmt.Sprintf("%d|%s|%d",
447				listing.ListingId,
448				listing.TokenId.String(),
449				listing.Price,
450			)
451			userListings = append(userListings, listingData)
452		}
453		return false
454	})
455
456	if len(userListings) == 0 {
457		return "No active listings"
458	}
459
460	return strings.Join(userListings, "\n")
461}
462
463// GetMarketplaceStats - Get overall marketplace statistics
464func GetMarketplaceStats() string {
465	activeCount := GetActiveListingsCount()
466	totalSales := GetTotalSales()
467	totalVolume := GetTotalVolume()
468
469	output := ufmt.Sprintf(`{
470  "activeListings": %d,
471  "totalSales": %d,
472  "totalVolume": %d,
473  "marketplaceFee": %d
474}`,
475		activeCount,
476		totalSales,
477		totalVolume,
478		marketplaceFee,
479	)
480
481	return output
482}
483
484// ============= HELPERS =============
485
486func getListing(listingId int) *Listing {
487	value, exists := listings.Get(strconv.Itoa(listingId))
488	if !exists {
489		return nil
490	}
491	return value.(*Listing)
492}
493
494func formatPrice(ugnot int64) string {
495	gnot := float64(ugnot) / 1000000.0
496	return ufmt.Sprintf("%.2f GNOT", gnot)
497}
498
499func formatFee(basisPoints int64) string {
500	percent := float64(basisPoints) / 100.0
501	return ufmt.Sprintf("%.2f%%", percent)
502}
503
504func formatPercentage(value, total int64) string {
505	if total == 0 {
506		return "0%"
507	}
508	percent := (float64(value) / float64(total)) * 100.0
509	return ufmt.Sprintf("%.1f%%", percent)
510}