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}