Skip to content

Commit

Permalink
fix NFT serialization, add test cases & add contract URI serializatio…
Browse files Browse the repository at this point in the history
…n on deployERC721
  • Loading branch information
sisyphusSmiling committed Mar 20, 2024
1 parent f9795b5 commit 192674b
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 29 deletions.
25 changes: 19 additions & 6 deletions cadence/contracts/bridge/FlowEVMBridge.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
if let metadata = token.resolveView(Type<CrossVMNFT.EVMBridgedMetadata>()) as! CrossVMNFT.EVMBridgedMetadata? {
uri = metadata.uri.uri()
} else {
// Otherwise, serialize the NFT using OpenSea Metadata strategy
// Otherwise, serialize the NFT
uri = SerializeNFT.serializeNFTMetadataAsURI(&token as &{NonFungibleToken.NFT})
}

Expand Down Expand Up @@ -416,13 +416,26 @@ contract FlowEVMBridge : IFlowEVMNFTBridge {
// Borrow the ViewResolver to attempt to resolve the EVMBridgedMetadata view
let viewResolver = getAccount(cadenceAddress).contracts.borrow<&{ViewResolver}>(name: name)!
var contractURI = ""
if let bridgedMetadata = viewResolver.resolveContractView(
// Try to resolve the EVMBridgedMetadata
let bridgedMetadata = viewResolver.resolveContractView(
resourceType: forNFTType,
viewType: Type<CrossVMNFT.EVMBridgedMetadata>()
) as! CrossVMNFT.EVMBridgedMetadata? {
name = bridgedMetadata.name
symbol = bridgedMetadata.symbol
contractURI = bridgedMetadata.uri.uri()
) as! CrossVMNFT.EVMBridgedMetadata?
// Default to project-defined URI if available
if bridgedMetadata != nil {
name = bridgedMetadata!.name
symbol = bridgedMetadata!.symbol
contractURI = bridgedMetadata!.uri.uri()
} else {
// Otherwise, serialize collection-level NFTCollectionDisplay
if let collectionDisplay = viewResolver.resolveContractView(
resourceType: forNFTType,
viewType: Type<MetadataViews.NFTCollectionDisplay>()
) as! MetadataViews.NFTCollectionDisplay? {
name = collectionDisplay.name
let serializedDisplay = SerializeNFT.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)!
contractURI = "data:application/json;ascii,{".concat(serializedDisplay).concat("}")
}
}

// Call to the factory contract to deploy an ERC721
Expand Down
44 changes: 23 additions & 21 deletions cadence/contracts/utils/SerializeNFT.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ access(all) contract SerializeNFT {
// Serialize the display values from the NFT's Display & NFTCollectionDisplay views
let nftDisplay = nft.resolveView(Type<MetadataViews.Display>()) as! MetadataViews.Display?
let collectionDisplay = nft.resolveView(Type<MetadataViews.NFTCollectionDisplay>()) as! MetadataViews.NFTCollectionDisplay?
let display = self.serializeNFTDisplay(nftDisplay: nftDisplay, collectionDisplay: collectionDisplay)
let display = self.serializeFromDisplays(nftDisplay: nftDisplay, collectionDisplay: collectionDisplay)

// Get the Traits view from the NFT, returning early if no traits are found
let traits = nft.resolveView(Type<MetadataViews.Traits>()) as! MetadataViews.Traits?
Expand All @@ -41,29 +41,32 @@ access(all) contract SerializeNFT {
return ""
}
// Init the data format prefix & concatenate the serialized display & attributes
var serializedMetadata= "data:application/json;ascii,{"
var serializedMetadata = "data:application/json;ascii,{"
if display != nil {
serializedMetadata = serializedMetadata.concat(display!)
}
if display != nil && attributes != nil {
serializedMetadata = serializedMetadata.concat(", ")
}
if attributes != nil {
serializedMetadata = serializedMetadata.concat(attributes!)
serializedMetadata = serializedMetadata.concat(attributes)
}
return serializedMetadata.concat("}")
}

/// Serializes the display & collection display views of a given NFT as a JSON compatible string
/// Serializes the display & collection display views of a given NFT as a JSON compatible string. If nftDisplay is
/// present, the value is returned as token-level metadata. If nftDisplay is nil and collectionDisplay is present,
/// the value is returned as contract-level metadata. If both values are nil, nil is returned.
///
/// @param nftDisplay: The NFT's Display view from which values `name`, `description`, and `thumbnail` are serialized
/// @param collectionDisplay: The NFT's NFTCollectionDisplay view from which the `externalURL` is serialized
///
/// @returns: A JSON compatible string containing the serialized display & collection display views as:
/// \"name\": \"<display.name>\", \"description\": \"<display.description>\", \"image\": \"<display.thumbnail.uri()>\", \"external_url\": \"<nftCollectionDisplay.externalURL.url>\",
/// @returns: A JSON compatible string containing the serialized display & collection display views as either:
/// \"name\": \"<nftDisplay.name>\", \"description\": \"<nftDisplay.description>\", \"image\": \"<nftDisplay.thumbnail.uri()>\", \"external_url\": \"<collectionDisplay.externalURL.url>\",
/// \"name\": \"<collectionDisplay.name>\", \"description\": \"<collectionDisplay.description>\", \"image\": \"<collectionDisplay.squareImage.file.uri()>\", \"external_link\": \"<collectionDisplay.externalURL.url>\",
///
access(all)
fun serializeNFTDisplay(nftDisplay: MetadataViews.Display?, collectionDisplay: MetadataViews.NFTCollectionDisplay?, ): String? {
fun serializeFromDisplays(nftDisplay: MetadataViews.Display?, collectionDisplay: MetadataViews.NFTCollectionDisplay?): String? {
// Return early if both values are nil
if nftDisplay == nil && collectionDisplay == nil {
return nil
Expand All @@ -74,33 +77,32 @@ access(all) contract SerializeNFT {
let description = "\"description\": "
let image = "\"image\": "
let externalURL = "\"external_url\": "
let externalLink = "\"external_link\": "
var serializedResult = ""

// Append results from the Display view to the serialized JSON compatible string
// Append results from the token-level Display view to the serialized JSON compatible string
if nftDisplay != nil {
serializedResult = serializedResult
.concat(name).concat(Serialize.tryToJSONString(nftDisplay!.name)!).concat(", ")
.concat(description).concat(Serialize.tryToJSONString(nftDisplay!.description)!).concat(", ")
.concat(image).concat(Serialize.tryToJSONString(nftDisplay!.thumbnail.uri())!)
// Return here if collectionDisplay is not present
if collectionDisplay == nil {
return serializedResult
// Append the `externa_url` value from NFTCollectionDisplay view if present
if collectionDisplay != nil {
return serializedResult.concat(", ")
.concat(externalURL).concat(Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!)
}
}

// Append a comma if both Display & NFTCollection Display views are present
if nftDisplay != nil {
serializedResult = serializedResult.concat(", ")
} else {
// Otherwise, append the name & description fields from the NFTCollectionDisplay view, foregoing image
serializedResult = serializedResult
.concat(name).concat(Serialize.tryToJSONString(collectionDisplay!.name)!).concat(", ")
.concat(description).concat(Serialize.tryToJSONString(collectionDisplay!.description)!).concat(", ")
if collectionDisplay == nil {
return serializedResult
}

// Without token-level view, serialize as contract-level metadata
return serializedResult
.concat(externalURL)
.concat(Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!)
.concat(name).concat(Serialize.tryToJSONString(collectionDisplay!.name)!).concat(", ")
.concat(description).concat(Serialize.tryToJSONString(collectionDisplay!.description)!).concat(", ")
.concat(image).concat(Serialize.tryToJSONString(collectionDisplay!.squareImage.file.uri())!).concat(", ")
.concat(externalLink).concat(Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!)
}

/// Serializes given Traits view as a JSON compatible string. If a given Trait is not serializable, it is skipped
Expand Down
70 changes: 68 additions & 2 deletions cadence/tests/serialize_nft_tests.cdc
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Test
import BlockchainHelpers

import "MetadataViews"

import "Serialize"
import "SerializeNFT"

access(all) let admin = Test.getAccount(0x0000000000000007)
access(all) let alice = Test.createAccount()
Expand Down Expand Up @@ -107,6 +110,69 @@ fun testSerializeNFTSucceeds() {

let serializedMetadata = serializeMetadataResult.returnValue! as! String

// Test.assertEqual(true, serializedMetadata == expectedPrefix.concat(altSuffix1) || serializedMetadata == expectedPrefix.concat(altSuffix2))
Test.assertEqual(serializedMetadata, expectedPrefix.concat(altSuffix1))
Test.assertEqual(true, serializedMetadata == expectedPrefix.concat(altSuffix1) || serializedMetadata == expectedPrefix.concat(altSuffix2))
}

// Returns nil when no displays are provided
access(all)
fun testSerializeNilDisplaysReturnsNil() {
let serializedResult = SerializeNFT.serializeFromDisplays(nftDisplay: nil, collectionDisplay: nil)
Test.assertEqual(nil, serializedResult)
}

// Given just token-level Display, serialize as tokenURI format
access(all)
fun testSerializeNFTDisplaySucceeds() {
let display = MetadataViews.Display(
name: "NAME",
description: "NFT Description",
thumbnail: MetadataViews.HTTPFile(url: "https://flow.com/examplenft.jpg"),
)

let expected = "\"name\": \"NAME\", \"description\": \"NFT Description\", \"image\": \"https://flow.com/examplenft.jpg\""

let serializedResult = SerializeNFT.serializeFromDisplays(nftDisplay: display, collectionDisplay: nil)
Test.assertEqual(expected, serializedResult!)
}

// Given just token-level Display, serialize as contractURI format
access(all)
fun testSerializeNFTCollectionDisplaySucceeds() {
let collectionDisplay = MetadataViews.NFTCollectionDisplay(
name: "NAME",
description: "NFT Description",
externalURL: MetadataViews.ExternalURL("https://flow.com"),
squareImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://flow.com/square_image.jpg"), mediaType: "image"),
bannerImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://flow.com/square_image.jpg"), mediaType: "image"),
socials: {}
)

let expected = "\"name\": \"NAME\", \"description\": \"NFT Description\", \"image\": \"https://flow.com/square_image.jpg\", \"external_link\": \"https://flow.com\""

let serializedResult = SerializeNFT.serializeFromDisplays(nftDisplay: nil, collectionDisplay: collectionDisplay)
Test.assertEqual(expected, serializedResult!)
}

// Given bol token- & contract-level Displays, serialize as tokenURI format
access(all)
fun testSerializeBothDisplaysSucceeds() {
let nftDisplay = MetadataViews.Display(
name: "NAME",
description: "NFT Description",
thumbnail: MetadataViews.HTTPFile(url: "https://flow.com/examplenft.jpg"),
)

let collectionDisplay = MetadataViews.NFTCollectionDisplay(
name: "NAME",
description: "NFT Description",
externalURL: MetadataViews.ExternalURL("https://flow.com"),
squareImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://flow.com/square_image.jpg"), mediaType: "image"),
bannerImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://flow.com/square_image.jpg"), mediaType: "image"),
socials: {}
)

let expected = "\"name\": \"NAME\", \"description\": \"NFT Description\", \"image\": \"https://flow.com/examplenft.jpg\", \"external_url\": \"https://flow.com\""

let serializedResult = SerializeNFT.serializeFromDisplays(nftDisplay: nftDisplay, collectionDisplay: collectionDisplay)
Test.assertEqual(expected, serializedResult!)
}

0 comments on commit 192674b

Please sign in to comment.