Smart Contract

SerializeMetadata

A.1e4aa0b87d10b141.SerializeMetadata

Valid From

85,982,017

Deployed

1w ago
Feb 16, 2026, 08:14:21 PM UTC

Dependents

0 imports
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