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