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}