Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize serialization #132

Merged
merged 7 commits into from
Oct 25, 2024
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
90 changes: 37 additions & 53 deletions cadence/contracts/utils/Serialize.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "NonFungibleToken"
/// This contract is a utility for serializing primitive types, arrays, and common metadata mapping formats to JSON
/// compatible strings. Also included are interfaces enabling custom serialization for structs and resources.
///
/// Special thanks to @austinkline for the idea and initial implementation.
/// Special thanks to @austinkline for the idea and initial implementation & @bjartek + @bluesign for optimizations.
///
access(all)
contract Serialize {
Expand All @@ -27,59 +27,59 @@ contract Serialize {
case Type<Never?>():
return "\"nil\""
case Type<String>():
return "\"".concat(value as! String).concat("\"")
return String.join(["\"", value as! String, "\"" ], separator: "")
case Type<String?>():
return "\"".concat(value as? String ?? "nil").concat("\"")
return String.join(["\"", value as? String ?? "nil", "\"" ], separator: "")
case Type<Character>():
return "\"".concat((value as! Character).toString()).concat("\"")
return String.join(["\"", (value as! Character).toString(), "\"" ], separator: "")
case Type<Bool>():
return "\"".concat(value as! Bool ? "true" : "false").concat("\"")
return String.join(["\"", value as! Bool ? "true" : "false", "\"" ], separator: "")
case Type<Address>():
return "\"".concat((value as! Address).toString()).concat("\"")
return String.join(["\"", (value as! Address).toString(), "\"" ], separator: "")
case Type<Address?>():
return "\"".concat((value as? Address)?.toString() ?? "nil").concat("\"")
return String.join(["\"", (value as? Address)?.toString() ?? "nil", "\"" ], separator: "")
case Type<Int8>():
return "\"".concat((value as! Int8).toString()).concat("\"")
return String.join(["\"", (value as! Int8).toString(), "\"" ], separator: "")
case Type<Int16>():
return "\"".concat((value as! Int16).toString()).concat("\"")
return String.join(["\"", (value as! Int16).toString(), "\"" ], separator: "")
case Type<Int32>():
return "\"".concat((value as! Int32).toString()).concat("\"")
return String.join(["\"", (value as! Int32).toString(), "\"" ], separator: "")
case Type<Int64>():
return "\"".concat((value as! Int64).toString()).concat("\"")
return String.join(["\"", (value as! Int64).toString(), "\"" ], separator: "")
case Type<Int128>():
return "\"".concat((value as! Int128).toString()).concat("\"")
return String.join(["\"", (value as! Int128).toString(), "\"" ], separator: "")
case Type<Int256>():
return "\"".concat((value as! Int256).toString()).concat("\"")
return String.join(["\"", (value as! Int256).toString(), "\"" ], separator: "")
case Type<Int>():
return "\"".concat((value as! Int).toString()).concat("\"")
return String.join(["\"", (value as! Int).toString(), "\"" ], separator: "")
case Type<UInt8>():
return "\"".concat((value as! UInt8).toString()).concat("\"")
return String.join(["\"", (value as! UInt8).toString(), "\"" ], separator: "")
case Type<UInt16>():
return "\"".concat((value as! UInt16).toString()).concat("\"")
return String.join(["\"", (value as! UInt16).toString(), "\"" ], separator: "")
case Type<UInt32>():
return "\"".concat((value as! UInt32).toString()).concat("\"")
return String.join(["\"", (value as! UInt32).toString(), "\"" ], separator: "")
case Type<UInt64>():
return "\"".concat((value as! UInt64).toString()).concat("\"")
return String.join(["\"", (value as! UInt64).toString(), "\"" ], separator: "")
case Type<UInt128>():
return "\"".concat((value as! UInt128).toString()).concat("\"")
return String.join(["\"", (value as! UInt128).toString(), "\"" ], separator: "")
case Type<UInt256>():
return "\"".concat((value as! UInt256).toString()).concat("\"")
return String.join(["\"", (value as! UInt256).toString(), "\"" ], separator: "")
case Type<UInt>():
return "\"".concat((value as! UInt).toString()).concat("\"")
return String.join(["\"", (value as! UInt).toString(), "\"" ], separator: "")
case Type<Word8>():
return "\"".concat((value as! Word8).toString()).concat("\"")
return String.join(["\"", (value as! Word8).toString(), "\"" ], separator: "")
case Type<Word16>():
return "\"".concat((value as! Word16).toString()).concat("\"")
return String.join(["\"", (value as! Word16).toString(), "\"" ], separator: "")
case Type<Word32>():
return "\"".concat((value as! Word32).toString()).concat("\"")
return String.join(["\"", (value as! Word32).toString(), "\"" ], separator: "")
case Type<Word64>():
return "\"".concat((value as! Word64).toString()).concat("\"")
return String.join(["\"", (value as! Word64).toString(), "\"" ], separator: "")
case Type<Word128>():
return "\"".concat((value as! Word128).toString()).concat("\"")
return String.join(["\"", (value as! Word128).toString(), "\"" ], separator: "")
case Type<Word256>():
return "\"".concat((value as! Word256).toString()).concat("\"")
return String.join(["\"", (value as! Word256).toString(), "\"" ], separator: "")
case Type<UFix64>():
return "\"".concat((value as! UFix64).toString()).concat("\"")
return String.join(["\"", (value as! UFix64).toString(), "\"" ], separator: "")
default:
return nil
}
Expand All @@ -89,24 +89,15 @@ contract Serialize {
///
access(all)
fun arrayToJSONString(_ arr: [AnyStruct]): String? {
var serializedArr = "["
let arrLength = arr.length
for i, element in arr {
let parts: [String]= []
for element in arr {
let serializedElement = self.tryToJSONString(element)
if serializedElement == nil {
if i == arrLength - 1 && serializedArr.length > 1 && serializedArr[serializedArr.length - 2] == "," {
// Remove trailing comma as this element could not be serialized
serializedArr = serializedArr.slice(from: 0, upTo: serializedArr.length - 2)
}
continue
}
serializedArr = serializedArr.concat(serializedElement!)
// Add a comma if there are more elements to serialize
if i < arr.length - 1 {
serializedArr = serializedArr.concat(", ")
}
parts.append(serializedElement!)
}
return serializedArr.concat("]")
return "[".concat(String.join(parts, separator: ", ")).concat("]")
}

/// Returns a serialized representation of the given String-indexed mapping or nil if the value is not serializable.
Expand All @@ -120,22 +111,15 @@ contract Serialize {
dict.remove(key: k)
}
}
var serializedDict = "{"
let dictLength = dict.length
for i, key in dict.keys {
let parts: [String] = []
for key in dict.keys {
let serializedValue = self.tryToJSONString(dict[key]!)
if serializedValue == nil {
if i == dictLength - 1 && serializedDict.length > 1 && serializedDict[serializedDict.length - 2] == "," {
// Remove trailing comma as this element could not be serialized
serializedDict = serializedDict.slice(from: 0, upTo: serializedDict.length - 2)
}
continue
}
serializedDict = serializedDict.concat(self.tryToJSONString(key)!).concat(": ").concat(serializedValue!)
if i < dict.length - 1 {
serializedDict = serializedDict.concat(", ")
}
let serialializedKeyValue = String.join([self.tryToJSONString(key)!, serializedValue!], separator: ": ")
parts.append(serialializedKeyValue)
}
return serializedDict.concat("}")
return "{".concat(String.join(parts, separator: ", ")).concat("}")
}
}
121 changes: 67 additions & 54 deletions cadence/contracts/utils/SerializeMetadata.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import "Serialize"
/// This contract defines methods for serializing NFT metadata as a JSON compatible string, according to the common
/// OpenSea metadata format. NFTs and metadata views can be serialized by reference via contract methods.
///
/// Special thanks to @austinkline for the idea and initial implementation & @bjartek + @bluesign for optimizations.
///
access(all) contract SerializeMetadata {

/// Serializes the metadata (as a JSON compatible String) for a given NFT according to formats expected by EVM
Expand All @@ -31,31 +33,28 @@ access(all) contract SerializeMetadata {
// 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?
// Serialize the display & collection display views - nil if both views are nil
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?
let attributes = self.serializeNFTTraitsAsAttributes(traits ?? MetadataViews.Traits([]))

// Return an empty string if nothing is serializable
if display == nil && attributes == nil {
// Return an empty string if all views are nil
if display == nil && traits == nil {
return ""
}
// Init the data format prefix & concatenate the serialized display & attributes
var serializedMetadata = "data:application/json;utf8,{"
let parts: [String] = ["data:application/json;utf8,{"]
if display != nil {
serializedMetadata = serializedMetadata.concat(display!)
}
if display != nil && attributes != nil {
serializedMetadata = serializedMetadata.concat(", ")
}
if attributes != nil {
serializedMetadata = serializedMetadata.concat(attributes)
parts.appendAll([display!, ", "]) // Include display if present & separate with a comma
}
return serializedMetadata.concat("}")
parts.appendAll([attributes, "}"]) // Include attributes & close the JSON object
return String.join(parts, separator: "")
}

/// Serializes the display & collection display views of a given NFT as a JSON compatible string. If nftDisplay is
/// 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.
///
Expand All @@ -80,73 +79,85 @@ access(all) contract SerializeMetadata {
let externalURL = "\"external_url\": "
let externalLink = "\"external_link\": "
var serializedResult = ""
let parts: [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())!)
// Append the `externa_url` value from NFTCollectionDisplay view if present
parts.appendAll([
name, Serialize.tryToJSONString(nftDisplay!.name)!, ", ",
description, Serialize.tryToJSONString(nftDisplay!.description)!, ", ",
image, Serialize.tryToJSONString(nftDisplay!.thumbnail.uri())!
])
// Append the `external_url` value from NFTCollectionDisplay view if present
if collectionDisplay != nil {
return serializedResult.concat(", ")
.concat(externalURL).concat(Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!)
parts.appendAll([", ", externalURL, Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!])
return String.join(parts, separator: "")
}
}

if collectionDisplay == nil {
return serializedResult
return String.join(parts, separator: "")
}

// Without token-level view, serialize as contract-level metadata
return serializedResult
.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)!)
parts.appendAll([
name, Serialize.tryToJSONString(collectionDisplay!.name)!, ", ",
description, Serialize.tryToJSONString(collectionDisplay!.description)!, ", ",
image, Serialize.tryToJSONString(collectionDisplay!.squareImage.file.uri())!, ", ",
externalLink, Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!
])
return String.join(parts, separator: "")
}

/// Serializes given Traits view as a JSON compatible string. If a given Trait is not serializable, it is skipped
/// and not included in the serialized result.
///
/// @param traits: The Traits view to be serialized
///
/// @returns: A JSON compatible string containing the serialized traits as:
/// @returns: A JSON compatible string containing the serialized traits as follows
/// (display_type omitted if trait.displayType == nil):
/// `\"attributes\": [{\"trait_type\": \"<trait.name>\", \"display_type\": \"<trait.displayType>\", \"value\": \"<trait.value>\"}, {...}]`
///
access(all)
fun serializeNFTTraitsAsAttributes(_ traits: MetadataViews.Traits): String {
// Serialize each trait as an attribute, building the serialized JSON compatible string
var serializedResult = "\"attributes\": ["
let parts: [String] = []
let traitsLength = traits.traits.length
for i, trait in traits.traits {
let value = Serialize.tryToJSONString(trait.value)
if value == nil {
// Remove trailing comma if last trait is not serializable
if i == traitsLength - 1 && serializedResult[serializedResult.length - 1] == "," {
serializedResult = serializedResult.slice(from: 0, upTo: serializedResult.length - 1)
}
for trait in traits.traits {
let attribute = self.serializeNFTTraitAsAttribute(trait)
if attribute == nil {
continue
}
serializedResult = serializedResult.concat("{")
.concat("\"trait_type\": ").concat(Serialize.tryToJSONString(trait.name)!)
if trait.displayType != nil {
serializedResult = serializedResult.concat(", \"display_type\": ")
.concat(Serialize.tryToJSONString(trait.displayType)!)
}
serializedResult = serializedResult.concat(", \"value\": ").concat(value!)
.concat("}")
if i < traits!.traits.length - 1 {
serializedResult = serializedResult.concat(",")
}
parts.append(attribute!)
}
// Join all serialized attributes with a comma separator, wrapping the result in square brackets under the
// `attributes` key
return "\"attributes\": [".concat(String.join(parts, separator: ", ")).concat("]")
}

/// Serializes a given Trait as an attribute in a JSON compatible format. If the trait's value is not serializable,
/// nil is returned.
/// The format of the serialized trait is as follows (display_type omitted if trait.displayType == nil):
/// `{"trait_type": "<trait.name>", "display_type": "<trait.displayType>", "value": "<trait.value>"}`
access(all)
fun serializeNFTTraitAsAttribute(_ trait: MetadataViews.Trait): String? {
let value = Serialize.tryToJSONString(trait.value)
if value == nil {
return nil
}
let parts: [String] = ["{"]
parts.appendAll( [ "\"trait_type\": ", Serialize.tryToJSONString(trait.name)! ] )
if trait.displayType != nil {
parts.appendAll( [ ", \"display_type\": ", Serialize.tryToJSONString(trait.displayType)! ] )
}
return serializedResult.concat("]")
parts.appendAll( [ ", \"value\": ", value! , "}" ] )
return String.join(parts, separator: "")
}

/// Serializes the FTDisplay view of a given fungible token as a JSON compatible data URL. The value is returned as
/// Serializes the FTDisplay view of a given fungible token as a JSON compatible data URL. The value is returned as
/// contract-level metadata.
///
/// @param ftDisplay: The tokens's FTDisplay view from which values `name`, `symbol`, `description`, and
/// @param ftDisplay: The tokens's FTDisplay view from which values `name`, `symbol`, `description`, and
/// `externaURL` are serialized
///
/// @returns: A JSON compatible data URL string containing the serialized view as:
Expand All @@ -162,13 +173,15 @@ access(all) contract SerializeMetadata {
let symbol = "\"symbol\": "
let description = "\"description\": "
let externalLink = "\"external_link\": "
let parts: [String] = ["data:application/json;utf8,{"]

return "data:application/json;utf8,{"
.concat(name).concat(Serialize.tryToJSONString(ftDisplay.name)!).concat(", ")
.concat(symbol).concat(Serialize.tryToJSONString(ftDisplay.symbol)!).concat(", ")
.concat(description).concat(Serialize.tryToJSONString(ftDisplay.description)!).concat(", ")
.concat(externalLink).concat(Serialize.tryToJSONString(ftDisplay.externalURL.url)!)
.concat("}")
parts.appendAll([
name, Serialize.tryToJSONString(ftDisplay.name)!, ", ",
symbol, Serialize.tryToJSONString(ftDisplay.symbol)!, ", ",
description, Serialize.tryToJSONString(ftDisplay.description)!, ", ",
externalLink, Serialize.tryToJSONString(ftDisplay.externalURL.url)!
])
return String.join(parts, separator: "")
}

/// Derives a symbol for use as an ERC20 or ERC721 symbol from a given string, presumably a Cadence contract name.
Expand Down
Loading
Loading