Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions contracts/NFTStorefrontV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,22 @@ access(all) contract NFTStorefrontV2 {
/// hasListingBecomeGhosted
/// Tells whether listed NFT is present in provided capability.
/// If it returns `false` then it means listing becomes ghost or sold out.
///
/// DEPRECATED: The return value of this function is semantically inverted — it returns `true`
/// when the NFT is still present (i.e. NOT ghosted) and `false` when the NFT is absent
/// (i.e. IS ghosted). This is the opposite of what the function name implies. The function
/// is kept as-is to avoid breaking existing integrations that already compensate for the
/// inversion. Use `isGhostListing()` instead, which returns `true` when the listing is
/// ghosted and `false` when it is still valid.
access(all) view fun hasListingBecomeGhosted(): Bool

/// isGhostListing
/// Returns `true` if the listed NFT is no longer present in the seller's collection
/// (i.e. the listing is ghosted and cannot be purchased), and `false` if the NFT is
/// still available. This is the correctly-named replacement for `hasListingBecomeGhosted()`,
/// which has inverted return semantics.
access(all) view fun isGhostListing(): Bool

}


Expand Down Expand Up @@ -345,13 +359,28 @@ access(all) contract NFTStorefrontV2 {
/// hasListingBecomeGhosted
/// Tells whether listed NFT is present in provided capability.
/// If it returns `false` then it means listing becomes ghost or sold out.
///
/// DEPRECATED: The return value is semantically inverted relative to the function name.
/// This function returns `true` when the NFT is still present (not ghosted) and `false`
/// when the NFT is absent (ghosted). Use `isGhostListing()` instead.
access(all) view fun hasListingBecomeGhosted(): Bool {
if let providerRef = self.nftProviderCapability.borrow() {
return providerRef.borrowNFT(self.details.nftID) != nil
}
return false
}

/// isGhostListing
/// Returns `true` if the listed NFT is no longer present in the seller's collection
/// (i.e. the listing is ghosted and cannot be purchased), and `false` if the NFT is
/// still available. This is the correctly-named replacement for `hasListingBecomeGhosted()`.
access(all) view fun isGhostListing(): Bool {
if let providerRef = self.nftProviderCapability.borrow() {
return providerRef.borrowNFT(self.details.nftID) == nil
}
return true
}

/// purchase
/// Purchase the listing, buying the token.
/// This pays the beneficiaries and commission to the facilitator and returns extra token to the buyer.
Expand Down Expand Up @@ -863,13 +892,24 @@ access(all) contract NFTStorefrontV2 {
message: "NFTStorefrontV2.Storefront.cleanupGhostListings: Cannot cleanup listing with id \(listingResourceID) because it is already purchased!"
)
assert(
!listingRef.hasListingBecomeGhosted(),
listingRef.isGhostListing(),
message: "NFTStorefrontV2.Storefront.cleanupGhostListings: Cannot cleanup listing with id \(listingResourceID) because it is not a ghost listing!"
)
let listing <- self.listings.remove(key: listingResourceID)!
// Fetch duplicates before removing the primary from listedNFTs.
// getDuplicateListingIDs uses `contains(listingResourceID)` as a guard — if the primary
// is already absent from listedNFTs it returns [] and duplicates are never cleaned up.
let duplicateListings = self.getDuplicateListingIDs(nftType: details.nftType, nftID: details.nftID, listingID: listingResourceID)

// Let's force removal of the listing in this storefront for the NFT that is being ghosted.
// Now remove the ghost listing's own entry from listedNFTs.
// Every other removal path (removeListing, cleanupPurchasedListings, cleanup) does this
// for the primary listing; cleanupGhostListings was the only path that omitted it,
// leaving a dangling listedNFTs entry after the duplicate-cleanup loop.
self.removeDuplicateListing(
nftIdentifier: details.nftType.identifier,
nftID: details.nftID,
listingResourceID: listingResourceID
)
// Let's force removal of the listing in this storefront for the NFT that is being ghosted.
for listingID in duplicateListings {
self.cleanup(listingResourceID: listingID)
}
Expand Down
4 changes: 4 additions & 0 deletions contracts/utility/test/MaliciousStorefrontV2.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ access(all) contract MaliciousStorefrontV2 {
return self.storefrontCap.borrow()!.borrowListing(listingResourceID: self.listingResourceID)!.hasListingBecomeGhosted()
}

access(all) view fun isGhostListing(): Bool {
return self.storefrontCap.borrow()!.borrowListing(listingResourceID: self.listingResourceID)!.isGhostListing()
}

// purchase will return the "wrong" nft
access(all) fun purchase(
payment: @{FungibleToken.Vault},
Expand Down
26 changes: 22 additions & 4 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ resource interface ListingPublic {
): @{NonFungibleToken.NFT}
access(all) view fun getDetails(): ListingDetails
access(all) fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
access(all) fun hasListingBecomeGhosted(): Bool
access(all) fun hasListingBecomeGhosted(): Bool // DEPRECATED — see below
access(all) view fun isGhostListing(): Bool
}
```
An interface providing a useful public interface to a Listing.
Expand Down Expand Up @@ -187,13 +188,30 @@ If it returns `nil` then commission paid to the receiver by default.

---

**fun `hasListingBecomeGhosted()`**
**fun `isGhostListing()`**

```cadence
fun isGhostListing(): Bool
```
Returns `true` if the listing is ghosted — i.e. the underlying NFT is no longer present in the
seller's collection and the listing cannot be purchased. Returns `false` if the NFT is still
available. Use this function to check ghost state.

---

**fun `hasListingBecomeGhosted()` _(deprecated)_**

```cadence
fun hasListingBecomeGhosted(): Bool
```
Tells whether a listed NFT that was put up for sale is still available in the provided listing.
If it returns `true` then it means the listing is "ghosted" because there is no available nft to fulfill the listing.
**Deprecated.** The return value of this function is semantically inverted relative to its name:
it returns `true` when the NFT **is still present** (the listing is _not_ ghosted) and `false`
when the NFT **is absent** (the listing _is_ ghosted). This is the opposite of what the function
name implies.

The function is preserved to avoid breaking existing integrations that already compensate for
the inversion (e.g. by calling `!hasListingBecomeGhosted()`). New code should use
`isGhostListing()` instead.

---

Expand Down
12 changes: 6 additions & 6 deletions lib/go/contracts/internal/assets/assets.go

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions scripts/get_existing_listing_ids.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import "NFTStorefrontV2"

/// Returns all listing resource IDs tracked in `listedNFTs` for the given NFT type and ID.
/// This reads directly from the `listedNFTs` index (via `getExistingListingIDs`), which is
/// distinct from `getListingIDs()` — a ghost entry left behind by the Bug 1 fix would show
/// up here even after the listing has been removed from `self.listings`.
///
/// @param storefrontAddress Address of the account holding the storefront resource.
/// @param nftTypeIdentifier Fully-qualified type identifier of the NFT (e.g. "A.00…ExampleNFT.NFT").
/// @param nftID Resource ID of the NFT.
access(all) fun main(storefrontAddress: Address, nftTypeIdentifier: String, nftID: UInt64): [UInt64] {
let nftType = CompositeType(nftTypeIdentifier)
?? panic("Could not construct type from identifier: ".concat(nftTypeIdentifier))

return getAccount(storefrontAddress).capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>(
NFTStorefrontV2.StorefrontPublicPath
)?.getExistingListingIDs(nftType: nftType, nftID: nftID)
?? panic("Could not borrow public storefront from address")
}
5 changes: 5 additions & 0 deletions scripts/has_listing_become_ghosted.cdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import "NFTStorefrontV2"

/// DEPRECATED: This script calls `hasListingBecomeGhosted()`, whose return value is semantically
/// inverted — it returns `true` when the NFT is still present (NOT a ghost listing) and `false`
/// when the NFT is absent (IS a ghost listing). Use `is_ghost_listing.cdc` instead, which
/// calls `isGhostListing()` and returns `true` when the listing is ghosted.
///
/// This script tells whether the provided `listingID` under the provided `storefront` address
/// has a ghost listing.
access(all) fun main(storefrontAddress: Address, listingID: UInt64): Bool {
Expand Down
21 changes: 21 additions & 0 deletions scripts/is_ghost_listing.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import "NFTStorefrontV2"

/// Returns `true` if the listing with the given `listingID` under the given `storefrontAddress`
/// is a ghost listing — i.e. the underlying NFT is no longer present in the seller's collection
/// and the listing cannot be purchased. Returns `false` if the NFT is still available.
///
/// This script uses `isGhostListing()`, which has correct semantics. Prefer this over the
/// deprecated `has_listing_become_ghosted.cdc`, whose underlying function returns an inverted value.
///
/// @param storefrontAddress Address of the account holding the storefront resource.
/// @param listingID Resource ID of the listing to check.
access(all) fun main(storefrontAddress: Address, listingID: UInt64): Bool {
let storefrontPublicRef = getAccount(storefrontAddress).capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>(
NFTStorefrontV2.StorefrontPublicPath
) ?? panic("Given account does not have a storefront resource")

let listingRef = storefrontPublicRef.borrowListing(listingResourceID: listingID)
?? panic("Provided listingID doesn't exist under the given storefront address")

return listingRef.isGhostListing()
}
5 changes: 5 additions & 0 deletions scripts/read_all_unique_ghost_listings.cdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import "NFTStorefrontV2"

/// DEPRECATED: This script works correctly but relies on `hasListingBecomeGhosted()`, which has
/// inverted return semantics (returns `true` when NOT ghosted, `false` when ghosted). The script
/// compensates internally with `!`, but callers should migrate to `read_all_unique_ghost_listings_v2.cdc`,
/// which uses `isGhostListing()` and has clearer semantics.
///
/// This script provides the array of listing resource Id which got ghosted It automatically skips the duplicate listing
/// as duplicate listings would get automatically delete once the primary one.
///
Expand Down
42 changes: 42 additions & 0 deletions scripts/read_all_unique_ghost_listings_v2.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "NFTStorefrontV2"

/// Returns the listing resource IDs of all unique ghost listings under the given storefront.
/// A ghost listing is one where the underlying NFT is no longer present in the seller's collection.
/// Duplicate listings (those sharing the same NFT type and ID as another listing) are excluded,
/// since they are cleaned up automatically when the primary listing is removed.
///
/// This script uses `isGhostListing()`, which has correct semantics. Prefer this over the
/// deprecated `read_all_unique_ghost_listings.cdc`.
///
/// @param storefrontAddress Address of the account holding the storefront resource.
access(all) fun main(storefrontAddress: Address): [UInt64] {

var duplicateListings: [UInt64] = []
var ghostListings: [UInt64] = []

let storefrontPublicRef = getAccount(storefrontAddress).capabilities.borrow<&{NFTStorefrontV2.StorefrontPublic}>(
NFTStorefrontV2.StorefrontPublicPath
) ?? panic("Given account does not have a storefront resource")

let availableListingIds = storefrontPublicRef.getListingIDs()

for id in availableListingIds {
if !duplicateListings.contains(id) {
let listingRef = storefrontPublicRef.borrowListing(listingResourceID: id)!
if listingRef.isGhostListing() {
ghostListings.append(id)
let listingDetails = listingRef.getDetails()
let dupListings = storefrontPublicRef.getDuplicateListingIDs(
nftType: listingDetails.nftType,
nftID: listingDetails.nftID,
listingID: id
)
if dupListings.length > 0 {
duplicateListings.appendAll(dupListings)
}
}
}
}

return ghostListings
}
Loading
Loading