Smart Contract
SerializeMetadata
A.1e4aa0b87d10b141.SerializeMetadata
1import ViewResolver from 0x1d7e57aa55817448
2import MetadataViews from 0x1d7e57aa55817448
3import NonFungibleToken from 0x1d7e57aa55817448
4import FungibleTokenMetadataViews from 0xf233dcee88fe0abe
5
6import Serialize from 0x1e4aa0b87d10b141
7
8/// This contract defines methods for serializing NFT metadata as a JSON compatible string, according to the common
9/// OpenSea metadata format. NFTs and metadata views can be serialized by reference via contract methods.
10///
11/// Special thanks to @austinkline for the idea and initial implementation & @bjartek + @bluesign for optimizations.
12///
13access(all) contract SerializeMetadata {
14
15 /// Serializes the metadata (as a JSON compatible String) for a given NFT according to formats expected by EVM
16 /// platforms like OpenSea. If you are a project owner seeking to expose custom traits on bridged NFTs and your
17 /// Trait.value is not natively serializable, you can implement a custom serialization method with the
18 /// `{SerializableStruct}` interface's `serialize` method.
19 ///
20 /// Reference: https://docs.opensea.io/docs/metadata-standards
21 ///
22 ///
23 /// @returns: A JSON compatible data URL string containing the serialized display & collection display views as:
24 /// `data:application/json;utf8,{
25 /// \"name\": \"<display.name>\",
26 /// \"description\": \"<display.description>\",
27 /// \"image\": \"<display.thumbnail.uri()>\",
28 /// \"external_url\": \"<nftCollectionDisplay.externalURL.url>\",
29 /// \"attributes\": [{\"trait_type\": \"<trait.name>\", \"value\": \"<trait.value>\"}, {...}]
30 /// }`
31 access(all)
32 fun serializeNFTMetadataAsURI(_ nft: &{NonFungibleToken.NFT}): String {
33 // Serialize the display values from the NFT's Display & NFTCollectionDisplay views
34 let nftDisplay = nft.resolveView(Type<MetadataViews.Display>()) as! MetadataViews.Display?
35 let collectionDisplay = nft.resolveView(Type<MetadataViews.NFTCollectionDisplay>()) as! MetadataViews.NFTCollectionDisplay?
36 // Serialize the display & collection display views - nil if both views are nil
37 let display = self.serializeFromDisplays(nftDisplay: nftDisplay, collectionDisplay: collectionDisplay)
38
39 // Get the Traits view from the NFT, returning early if no traits are found
40 let traits = nft.resolveView(Type<MetadataViews.Traits>()) as! MetadataViews.Traits?
41 let attributes = self.serializeNFTTraitsAsAttributes(traits ?? MetadataViews.Traits([]))
42
43 // Return an empty string if all views are nil
44 if display == nil && traits == nil {
45 return ""
46 }
47 // Init the data format prefix & concatenate the serialized display & attributes
48 let parts: [String] = ["data:application/json;utf8,{"]
49 if display != nil {
50 parts.appendAll([display!, ", "]) // Include display if present & separate with a comma
51 }
52 parts.appendAll([attributes, "}"]) // Include attributes & close the JSON object
53
54 return String.join(parts, separator: "")
55 }
56
57 /// Serializes the display & collection display views of a given NFT as a JSON compatible string. If nftDisplay is
58 /// present, the value is returned as token-level metadata. If nftDisplay is nil and collectionDisplay is present,
59 /// the value is returned as contract-level metadata. If both values are nil, nil is returned.
60 ///
61 /// @param nftDisplay: The NFT's Display view from which values `name`, `description`, and `thumbnail` are serialized
62 /// @param collectionDisplay: The NFT's NFTCollectionDisplay view from which the `externalURL` is serialized
63 ///
64 /// @returns: A JSON compatible string containing the serialized display & collection display views as either:
65 /// \"name\": \"<nftDisplay.name>\", \"description\": \"<nftDisplay.description>\", \"image\": \"<nftDisplay.thumbnail.uri()>\", \"external_url\": \"<collectionDisplay.externalURL.url>\",
66 /// \"name\": \"<collectionDisplay.name>\", \"description\": \"<collectionDisplay.description>\", \"image\": \"<collectionDisplay.squareImage.file.uri()>\", \"external_link\": \"<collectionDisplay.externalURL.url>\",
67 ///
68 access(all)
69 fun serializeFromDisplays(nftDisplay: MetadataViews.Display?, collectionDisplay: MetadataViews.NFTCollectionDisplay?): String? {
70 // Return early if both values are nil
71 if nftDisplay == nil && collectionDisplay == nil {
72 return nil
73 }
74
75 // Initialize JSON fields
76 let name = "\"name\": "
77 let description = "\"description\": "
78 let image = "\"image\": "
79 let externalURL = "\"external_url\": "
80 let externalLink = "\"external_link\": "
81 var serializedResult = ""
82 let parts: [String] = []
83
84 // Append results from the token-level Display view to the serialized JSON compatible string
85 if nftDisplay != nil {
86 parts.appendAll([
87 name, Serialize.tryToJSONString(nftDisplay!.name)!, ", ",
88 description, Serialize.tryToJSONString(nftDisplay!.description)!, ", ",
89 image, Serialize.tryToJSONString(nftDisplay!.thumbnail.uri())!
90 ])
91 // Append the `external_url` value from NFTCollectionDisplay view if present
92 if collectionDisplay != nil {
93 parts.appendAll([", ", externalURL, Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!])
94 return String.join(parts, separator: "")
95 }
96 }
97
98 if collectionDisplay == nil {
99 return String.join(parts, separator: "")
100 }
101
102 // Without token-level view, serialize as contract-level metadata
103 parts.appendAll([
104 name, Serialize.tryToJSONString(collectionDisplay!.name)!, ", ",
105 description, Serialize.tryToJSONString(collectionDisplay!.description)!, ", ",
106 image, Serialize.tryToJSONString(collectionDisplay!.squareImage.file.uri())!, ", ",
107 externalLink, Serialize.tryToJSONString(collectionDisplay!.externalURL.url)!
108 ])
109 return String.join(parts, separator: "")
110 }
111
112 /// Serializes given Traits view as a JSON compatible string. If a given Trait is not serializable, it is skipped
113 /// and not included in the serialized result.
114 ///
115 /// @param traits: The Traits view to be serialized
116 ///
117 /// @returns: A JSON compatible string containing the serialized traits as follows
118 /// (display_type omitted if trait.displayType == nil):
119 /// `\"attributes\": [{\"trait_type\": \"<trait.name>\", \"display_type\": \"<trait.displayType>\", \"value\": \"<trait.value>\"}, {...}]`
120 ///
121 access(all)
122 fun serializeNFTTraitsAsAttributes(_ traits: MetadataViews.Traits): String {
123 // Serialize each trait as an attribute, building the serialized JSON compatible string
124 let parts: [String] = []
125 let traitsLength = traits.traits.length
126 for trait in traits.traits {
127 let attribute = self.serializeNFTTraitAsAttribute(trait)
128 if attribute == nil {
129 continue
130 }
131 parts.append(attribute!)
132 }
133 // Join all serialized attributes with a comma separator, wrapping the result in square brackets under the
134 // `attributes` key
135 return "\"attributes\": [".concat(String.join(parts, separator: ", ")).concat("]")
136 }
137
138 /// Serializes a given Trait as an attribute in a JSON compatible format. If the trait's value is not serializable,
139 /// nil is returned.
140 /// The format of the serialized trait is as follows (display_type omitted if trait.displayType == nil):
141 /// `{"trait_type": "<trait.name>", "display_type": "<trait.displayType>", "value": "<trait.value>"}`
142 access(all)
143 fun serializeNFTTraitAsAttribute(_ trait: MetadataViews.Trait): String? {
144 let value = Serialize.tryToJSONString(trait.value)
145 if value == nil {
146 return nil
147 }
148 let parts: [String] = ["{"]
149 parts.appendAll( [ "\"trait_type\": ", Serialize.tryToJSONString(trait.name)! ] )
150 if trait.displayType != nil {
151 parts.appendAll( [ ", \"display_type\": ", Serialize.tryToJSONString(trait.displayType)! ] )
152 }
153 parts.appendAll( [ ", \"value\": ", value! , "}" ] )
154 return String.join(parts, separator: "")
155 }
156
157 /// Serializes the FTDisplay view of a given fungible token as a JSON compatible data URL. The value is returned as
158 /// contract-level metadata.
159 ///
160 /// @param ftDisplay: The tokens's FTDisplay view from which values `name`, `symbol`, `description`, and
161 /// `externaURL` are serialized
162 ///
163 /// @returns: A JSON compatible data URL string containing the serialized view as:
164 /// `data:application/json;utf8,{
165 /// \"name\": \"<ftDisplay.name>\",
166 /// \"symbol\": \"<ftDisplay.symbol>\",
167 /// \"description\": \"<ftDisplay.description>\",
168 /// \"external_link\": \"<ftDisplay.externalURL.url>\",
169 /// }`
170 access(all)
171 fun serializeFTDisplay(_ ftDisplay: FungibleTokenMetadataViews.FTDisplay): String {
172 let name = "\"name\": "
173 let symbol = "\"symbol\": "
174 let description = "\"description\": "
175 let externalLink = "\"external_link\": "
176 let parts: [String] = ["data:application/json;utf8,{"]
177
178 parts.appendAll([
179 name, Serialize.tryToJSONString(ftDisplay.name)!, ", ",
180 symbol, Serialize.tryToJSONString(ftDisplay.symbol)!, ", ",
181 description, Serialize.tryToJSONString(ftDisplay.description)!, ", ",
182 externalLink, Serialize.tryToJSONString(ftDisplay.externalURL.url)!
183 ])
184 return String.join(parts, separator: "")
185 }
186
187 /// Derives a symbol for use as an ERC20 or ERC721 symbol from a given string, presumably a Cadence contract name.
188 /// Derivation is a process of slicing the first 4 characters of the string and converting them to uppercase.
189 ///
190 /// @param fromString: The string from which to derive a symbol
191 ///
192 /// @returns: A derived symbol for use as an ERC20 or ERC721 symbol based on the provided string, presumably a
193 /// Cadence contract name
194 ///
195 access(all) view fun deriveSymbol(fromString: String): String {
196 let defaultLen = 4
197 let len = fromString.length < defaultLen ? fromString.length : defaultLen
198 return self.toUpperAlphaNumerical(fromString, upTo: len)
199 }
200
201 /// Returns the uppercase alphanumeric version of a given string. If upTo is nil or exceeds the length of the string,
202 /// the entire string is converted to uppercase.
203 ///
204 /// @param str: The string to convert to uppercase
205 /// @param upTo: The maximum number of characters to convert to uppercase
206 ///
207 /// @returns: The uppercase version of the given string
208 ///
209 access(all) view fun toUpperAlphaNumerical(_ str: String, upTo: Int?): String {
210 let len = upTo ?? str.length
211 var upper: String = ""
212 for char in str {
213 if upper.length == len {
214 break
215 }
216 let bytes = char.utf8
217 if bytes.length != 1 {
218 continue
219 }
220 let byte = bytes[0]
221 if byte >= 97 && byte <= 122 {
222 // Convert lower case to upper case
223 let upperChar = String.fromUTF8([byte - UInt8(32)])!
224 upper = upper.concat(upperChar)
225 } else if byte >= 65 && byte <= 90 {
226 // Keep upper case
227 upper = upper.concat(char.toString())
228 } else if byte >= 48 && byte <= 57 {
229 // Keep numbers
230 upper = upper.concat(String.fromCharacters([char]))
231 } else {
232 // Skip non-alphanumeric characters
233 continue
234 }
235 }
236 return upper
237 }
238}
239