gnopensea.gno
13.69 Kb ยท 576 lines
1package gnopendao3
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}