Skip to content

Commit

Permalink
Merge branch 'main' into gio/update-fee-estimate
Browse files Browse the repository at this point in the history
  • Loading branch information
sisyphusSmiling committed Oct 25, 2024
2 parents 8814053 + d346978 commit acab319
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 118 deletions.
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

0 comments on commit acab319

Please sign in to comment.