From eb4a9cfe0bd065c9decf0b069c2f1c009db5b0c9 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:45:05 -0500 Subject: [PATCH] update serialization utils & tests --- .gitignore | 5 + .../utils/SerializationInterfaces.cdc | 30 +++ cadence/contracts/utils/Serialize.cdc | 141 +++++++------ cadence/contracts/utils/SerializeNFT.cdc | 185 ++++++++++++++++++ cadence/scripts/serialize/serialize_nft.cdc | 20 ++ .../serialize_nft_from_open_sea_strategy.cdc | 21 ++ cadence/tests/Serialize_tests.cdc | 109 ++++++++--- cadence/tests/serialize_nft_tests.cdc | 130 ++++++++++++ flow.json | 25 ++- 9 files changed, 564 insertions(+), 102 deletions(-) create mode 100644 cadence/contracts/utils/SerializationInterfaces.cdc create mode 100644 cadence/contracts/utils/SerializeNFT.cdc create mode 100644 cadence/scripts/serialize/serialize_nft.cdc create mode 100644 cadence/scripts/serialize/serialize_nft_from_open_sea_strategy.cdc create mode 100644 cadence/tests/serialize_nft_tests.cdc diff --git a/.gitignore b/.gitignore index 6a3b4a5c..0a5466db 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,9 @@ docs/ # Dotenv file .env +# flow-evm-gateway db/ files db/ + +# Cadence test framework coverage +coverage.json +coverage.lcov diff --git a/cadence/contracts/utils/SerializationInterfaces.cdc b/cadence/contracts/utils/SerializationInterfaces.cdc new file mode 100644 index 00000000..667982a5 --- /dev/null +++ b/cadence/contracts/utils/SerializationInterfaces.cdc @@ -0,0 +1,30 @@ +/// The contract defines an interface for serialization strategies that can be used to serialize the struct or resource +/// according to a specific format. +/// +access(all) contract SerializationInterfaces { + + /// A SerializationStrategy takes a reference to a SerializableResource or SerializableStruct and returns a + /// serialized representation of it. The strategy is responsible for determining the structure of the serialized + /// representation and the format of the serialized data. + /// + access(all) + struct interface SerializationStrategy { + /// Returns the types supported by the implementing strategy + /// + access(all) view fun getSupportedTypes(): [Type] { + return [] + } + + /// Returns serialized representation of the given resource according to the format of the implementing strategy + /// + access(all) fun serializeResource(_ r: &AnyResource): String? { + return nil + } + + /// Returns serialized representation of the given struct according to the format of the implementing strategy + /// + access(all) fun serializeStruct(_ s: AnyStruct): String? { + return nil + } + } +} diff --git a/cadence/contracts/utils/Serialize.cdc b/cadence/contracts/utils/Serialize.cdc index 52c6c1d1..9c73f430 100644 --- a/cadence/contracts/utils/Serialize.cdc +++ b/cadence/contracts/utils/Serialize.cdc @@ -1,3 +1,9 @@ +import "ViewResolver" +import "MetadataViews" +import "NonFungibleToken" + +import "SerializationInterfaces" + /// 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. /// @@ -6,129 +12,115 @@ access(all) contract Serialize { - /// Defines the interface for a struct that returns a serialized representation of itself + /// A basic serialization strategy that supports serializing resources and structs to JSON-compatible strings. /// access(all) - struct interface SerializableStruct { - access(all) fun serialize(): String - } - - /// Defines the interface for a resource that returns a serialized representation of itself - /// - access(all) - resource interface SerializableResource { - access(all) fun serialize(): String + struct JSONStringStrategy : SerializationInterfaces.SerializationStrategy { + /// Returns the types this stategy will attempt to serialize + /// + access(all) view fun getSupportedTypes(): [Type] { + return [Type<@AnyResource>(), Type()] + } + /// Returns the resource serialized on its identifier as an escaped JSON string + /// + access(all) fun serializeResource(_ r: &AnyResource): String? { + return Serialize.tryToJSONString(r.getType().identifier) + } + /// Returns the an escaped JSON string of the provided struct, calling through to Serialize.tryToJSONString + /// with the provided value + /// + access(all) fun serializeStruct(_ s: AnyStruct): String? { + return Serialize.tryToJSONString(s) + } } /// Method that returns a serialized representation of the given value or nil if the value is not serializable /// access(all) - fun tryToString(_ value: AnyStruct): String? { - // Call serialize on the value if available - if value.getType().isSubtype(of: Type<{SerializableStruct}>()) { - return (value as! {SerializableStruct}).serialize() - } + fun tryToJSONString(_ value: AnyStruct): String? { // Recursively serialize array & return if value.getType().isSubtype(of: Type<[AnyStruct]>()) { - return self.arrayToString(value as! [AnyStruct]) + return self.arrayToJSONString(value as! [AnyStruct]) } // Recursively serialize map & return if value.getType().isSubtype(of: Type<{String: AnyStruct}>()) { - return self.dictToString(dict: value as! {String: AnyStruct}, excludedNames: nil) + return self.dictToJSONString(dict: value as! {String: AnyStruct}, excludedNames: nil) } - // Handle primitive types & their respective optionals + // Handle primitive types & optionals switch value.getType() { case Type(): - return "nil" + return "\"nil\"" case Type(): - return value as! String + return "\"".concat(value as! String).concat("\"") case Type(): - return value as? String ?? "nil" + return "\"".concat(value as? String ?? "nil").concat("\"") case Type(): - return (value as! Character).toString() - case Type(): - return (value as? Character)?.toString() ?? "nil" + return "\"".concat((value as! Character).toString()).concat("\"") case Type(): - return self.boolToString(value as! Bool) - case Type(): - if value as? Bool == nil { - return "nil" - } - return self.boolToString(value as! Bool) + return "\"".concat(value as! Bool ? "true" : "false").concat("\"") case Type
(): - return (value as! Address).toString() + return "\"".concat((value as! Address).toString()).concat("\"") case Type(): - return (value as? Address)?.toString() ?? "nil" + return "\"".concat((value as? Address)?.toString() ?? "nil").concat("\"") case Type(): - return (value as! Int8).toString() + return "\"".concat((value as! Int8).toString()).concat("\"") case Type(): - return (value as! Int16).toString() + return "\"".concat((value as! Int16).toString()).concat("\"") case Type(): - return (value as! Int32).toString() + return "\"".concat((value as! Int32).toString()).concat("\"") case Type(): - return (value as! Int64).toString() + return "\"".concat((value as! Int64).toString()).concat("\"") case Type(): - return (value as! Int128).toString() + return "\"".concat((value as! Int128).toString()).concat("\"") case Type(): - return (value as! Int256).toString() + return "\"".concat((value as! Int256).toString()).concat("\"") case Type(): - return (value as! Int).toString() + return "\"".concat((value as! Int).toString()).concat("\"") case Type(): - return (value as! UInt8).toString() + return "\"".concat((value as! UInt8).toString()).concat("\"") case Type(): - return (value as! UInt16).toString() + return "\"".concat((value as! UInt16).toString()).concat("\"") case Type(): - return (value as! UInt32).toString() + return "\"".concat((value as! UInt32).toString()).concat("\"") case Type(): - return (value as! UInt64).toString() + return "\"".concat((value as! UInt64).toString()).concat("\"") case Type(): - return (value as! UInt128).toString() + return "\"".concat((value as! UInt128).toString()).concat("\"") case Type(): - return (value as! UInt256).toString() + return "\"".concat((value as! UInt256).toString()).concat("\"") case Type(): - return (value as! UInt).toString() + return "\"".concat((value as! UInt).toString()).concat("\"") case Type(): - return (value as! Word8).toString() + return "\"".concat((value as! Word8).toString()).concat("\"") case Type(): - return (value as! Word16).toString() + return "\"".concat((value as! Word16).toString()).concat("\"") case Type(): - return (value as! Word32).toString() + return "\"".concat((value as! Word32).toString()).concat("\"") case Type(): - return (value as! Word64).toString() + return "\"".concat((value as! Word64).toString()).concat("\"") case Type(): - return (value as! Word128).toString() + return "\"".concat((value as! Word128).toString()).concat("\"") case Type(): - return (value as! Word256).toString() + return "\"".concat((value as! Word256).toString()).concat("\"") case Type(): - return (value as! UFix64).toString() + return "\"".concat((value as! UFix64).toString()).concat("\"") default: return nil } - } - access(all) - fun tryToJSONString(_ value: AnyStruct): String? { - return "\"".concat(self.tryToString(value) ?? "nil").concat("\"") - } - - /// Method that returns a serialized representation of a provided boolean - /// - access(all) - fun boolToString(_ value: Bool): String { - return value ? "true" : "false" } - /// Method that returns a serialized representation of the given array or nil if the value is not serializable + /// Returns a serialized representation of the given array or nil if the value is not serializable /// access(all) - fun arrayToString(_ arr: [AnyStruct]): String? { + fun arrayToJSONString(_ arr: [AnyStruct]): String? { var serializedArr = "[" for i, element in arr { - let serializedElement = self.tryToString(element) + let serializedElement = self.tryToJSONString(element) if serializedElement == nil { return nil } - serializedArr = serializedArr.concat("\"").concat(serializedElement!).concat("\"") + serializedArr = serializedArr.concat(serializedElement!) if i < arr.length - 1 { serializedArr = serializedArr.concat(", ") } @@ -136,12 +128,12 @@ contract Serialize { return serializedArr.concat("]") } - /// Method that returns a serialized representation of the given String-indexed mapping or nil if the value is not - /// serializable. The interface here is largely the same as as the `MetadataViews.dictToTraits` method, though here + /// Returns a serialized representation of the given String-indexed mapping or nil if the value is not serializable. + /// The interface here is largely the same as as the `MetadataViews.dictToTraits` method, though here /// a JSON-compatible String is returned instead of a `Traits` array. /// access(all) - fun dictToString(dict: {String: AnyStruct}, excludedNames: [String]?): String? { + fun dictToJSONString(dict: {String: AnyStruct}, excludedNames: [String]?): String? { if excludedNames != nil { for k in excludedNames! { dict.remove(key: k) @@ -149,16 +141,15 @@ contract Serialize { } var serializedDict = "{" for i, key in dict.keys { - let serializedValue = self.tryToString(dict[key]!) + let serializedValue = self.tryToJSONString(dict[key]!) if serializedValue == nil { return nil } - serializedDict = serializedDict.concat("\"").concat(key).concat("\": \"").concat(serializedValue!).concat("\"}") + serializedDict = serializedDict.concat(self.tryToJSONString(key)!).concat(": ").concat(serializedValue!) if i < dict.length - 1 { serializedDict = serializedDict.concat(", ") } } - serializedDict.concat("}") - return serializedDict + return serializedDict.concat("}") } } diff --git a/cadence/contracts/utils/SerializeNFT.cdc b/cadence/contracts/utils/SerializeNFT.cdc new file mode 100644 index 00000000..4b36a643 --- /dev/null +++ b/cadence/contracts/utils/SerializeNFT.cdc @@ -0,0 +1,185 @@ +import "ViewResolver" +import "MetadataViews" +import "NonFungibleToken" + +import "SerializationInterfaces" +import "Serialize" + +/// This contract defines methods for serializing NFT metadata as a JSON compatible string, according to the common +/// OpenSea metadata format. NFTs can be serialized by reference via contract methods or via the +/// OpenSeaMetadataSerializationStrategy struct. +/// +access(all) contract SerializeNFT { + + /// This struct will serialize NFT metadata as a JSON-compatible URI according to the OpenSea metadata standard + /// + access(all) + struct OpenSeaMetadataSerializationStrategy : SerializationInterfaces.SerializationStrategy { + /// Returns the types this strategy is intended to serialize + /// + access(all) view fun getSupportedTypes(): [Type] { + return [ + Type<@{NonFungibleToken.NFT}>(), + Type(), + Type(), + Type() + ] + } + + /// Serializes the given NFT (as &AnyResource) as a JSON compatible string in the format of an + /// OpenSea-compatible metadata URI. If the given resource is not an NFT, this method returns nil. + /// + /// Reference: https://docs.opensea.io/docs/metadata-standards + /// + access(all) fun serializeResource(_ r: &AnyResource): String? { + if r.getType().isSubtype(of: Type<@{NonFungibleToken.NFT}>()) { + let nft = r as! &{NonFungibleToken.NFT} + return SerializeNFT.serializeNFTMetadataAsURI(nft) + } + return nil + } + + /// Serializes the given struct as a JSON compatible string in the format that conforms with overlapping values + /// expected by the OpenSea metadata standard. If the given struct is not a Display, NFTCollectionDisplay, or + /// Traits view, this method returns nil. + /// + access(all) fun serializeStruct(_ s: AnyStruct): String? { + switch s.getType() { + case Type(): + let view = s as! MetadataViews.NFTCollectionDisplay + return SerializeNFT.serializeNFTDisplay(nftDisplay: nil, collectionDisplay: view) + case Type(): + let view = s as! MetadataViews.Display + return SerializeNFT.serializeNFTDisplay(nftDisplay: view, collectionDisplay: nil) + case Type(): + let view = s as! MetadataViews.Traits + return SerializeNFT.serializeNFTTraitsAsAttributes(view) + default: + return nil + + } + } + } + + /// Serializes the metadata (as a JSON compatible String) for a given NFT according to formats expected by EVM + /// platforms like OpenSea. If you are a project owner seeking to expose custom traits on bridged NFTs and your + /// Trait.value is not natively serializable, you can implement a custom serialization method with the + /// `{SerializableStruct}` interface's `serialize` method. + /// + /// Reference: https://docs.opensea.io/docs/metadata-standards + /// + /// + /// @returns: A JSON compatible string containing the serialized display & collection display views as: + /// `{ + /// \"name\": \"\", + /// \"description\": \"\", + /// \"image\": \"\", + /// \"external_url\": \"\", + /// \"attributes\": [{\"trait_type\": \"\", \"value\": \"\"}, {...}] + /// }` + access(all) + fun serializeNFTMetadataAsURI(_ nft: &{NonFungibleToken.NFT}): String { + // Serialize the display values from the NFT's Display & NFTCollectionDisplay views + let nftDisplay = nft.resolveView(Type()) as! MetadataViews.Display? + let collectionDisplay = nft.resolveView(Type()) as! MetadataViews.NFTCollectionDisplay? + let display = self.serializeNFTDisplay(nftDisplay: nftDisplay, collectionDisplay: collectionDisplay) + + // Get the Traits view from the NFT, returning early if no traits are found + let traits = nft.resolveView(Type()) as! MetadataViews.Traits? + let attributes = self.serializeNFTTraitsAsAttributes(traits ?? MetadataViews.Traits([])) + + // Return an empty string if nothing is serializable + if display == nil && attributes == nil { + return "" + } + // Init the data format prefix & concatenate the serialized display & attributes + 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!) + } + return serializedMetadata.concat("}") + } + + /// Serializes the display & collection display views of a given NFT as a JSON compatible string + /// + /// @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\": \"\", \"description\": \"\", \"image\": \"\", \"external_url\": \"\", + /// + access(all) + fun serializeNFTDisplay(nftDisplay: MetadataViews.Display?, collectionDisplay: MetadataViews.NFTCollectionDisplay?): String? { + // Return early if both values are nil + if nftDisplay == nil && collectionDisplay == nil { + return nil + } + + // Initialize JSON fields + let name = "\"name\": " + let description = "\"description\": " + let image = "\"image\": " + let externalURL = "\"external_url\": " + var serializedResult = "" + + // Append results from the 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 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(", ") + } + + return serializedResult + .concat(externalURL) + .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 + /// 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: + /// `\"attributes\": [{\"trait_type\": \"\", \"value\": \"\"}, {...}]` + /// + access(all) + fun serializeNFTTraitsAsAttributes(_ traits: MetadataViews.Traits): String { + // Serialize each trait as an attribute, building the serialized JSON compatible string + var serializedResult = "\"attributes\": [" + for i, trait in traits!.traits { + let value = Serialize.tryToJSONString(trait.value) + if value == nil { + continue + } + serializedResult = serializedResult.concat("{") + .concat("\"trait_type\": ").concat(Serialize.tryToJSONString(trait.name)!) + .concat(", \"value\": ").concat(value!) + .concat("}") + if i < traits!.traits.length - 1 { + serializedResult = serializedResult.concat(",") + } + } + return serializedResult.concat("]") + } +} diff --git a/cadence/scripts/serialize/serialize_nft.cdc b/cadence/scripts/serialize/serialize_nft.cdc new file mode 100644 index 00000000..3cb72baa --- /dev/null +++ b/cadence/scripts/serialize/serialize_nft.cdc @@ -0,0 +1,20 @@ +import "ViewResolver" +import "MetadataViews" +import "NonFungibleToken" + +import "SerializeNFT" + +access(all) +fun main(address: Address, storagePathIdentifier: String, id: UInt64): String? { + let storagePath = StoragePath(identifier: storagePathIdentifier) + ?? panic("Could not construct StoragePath from identifier") + if let collection = getAuthAccount(address).storage + .borrow<&{NonFungibleToken.Collection}>( + from: storagePath + ) { + if let nft = collection.borrowNFT(id) { + return SerializeNFT.serializeNFTMetadataAsURI(nft) + } + } + return nil +} diff --git a/cadence/scripts/serialize/serialize_nft_from_open_sea_strategy.cdc b/cadence/scripts/serialize/serialize_nft_from_open_sea_strategy.cdc new file mode 100644 index 00000000..95b8d1e2 --- /dev/null +++ b/cadence/scripts/serialize/serialize_nft_from_open_sea_strategy.cdc @@ -0,0 +1,21 @@ +import "ViewResolver" +import "MetadataViews" +import "NonFungibleToken" + +import "SerializeNFT" + +access(all) +fun main(address: Address, storagePathIdentifier: String, id: UInt64): String? { + let storagePath = StoragePath(identifier: storagePathIdentifier) + ?? panic("Could not construct StoragePath from identifier") + if let collection = getAuthAccount(address).storage + .borrow<&{NonFungibleToken.Collection}>( + from: storagePath + ) { + if let nft = collection.borrowNFT(id) { + let strategy = SerializeNFT.OpenSeaMetadataSerializationStrategy() + return strategy.serializeResource(nft) + } + } + return nil +} diff --git a/cadence/tests/Serialize_tests.cdc b/cadence/tests/Serialize_tests.cdc index 3a7ad0f5..cdfd30fb 100644 --- a/cadence/tests/Serialize_tests.cdc +++ b/cadence/tests/Serialize_tests.cdc @@ -1,14 +1,67 @@ import Test import BlockchainHelpers +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" + import "Serialize" +import "SerializationInterfaces" access(all) -let serializeAccount = Test.getAccount(0x0000000000000007) +let admin = Test.getAccount(0x0000000000000007) +access(all) +let alice = Test.createAccount() + +// access(all) +// let testSerializableStructOutput = "{\"trait_type\": \"Name\", \"value\": \"TestSerializableStruct\"}" + +// access(all) +// struct TestSerializableStruct : SerializationInterfaces.SerializableStruct { +// access(all) +// fun serialize(): String { +// return testSerializableStructOutput +// } +// } access(all) fun setup() { var err = Test.deployContract( + name: "ViewResolver", + path: "../contracts/standards/ViewResolver.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "Burner", + path: "../contracts/standards/Burner.cdc", + arguments: [] + ) + err = Test.deployContract( + name: "FungibleToken", + path: "../contracts/standards/FungibleToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "NonFungibleToken", + path: "../contracts/standards/NonFungibleToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MetadataViews", + path: "../contracts/standards/MetadataViews.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SerializationInterfaces", + path: "../contracts/utils/SerializationInterfaces.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( name: "Serialize", path: "../contracts/utils/Serialize.cdc", arguments: [] @@ -184,37 +237,43 @@ fun testBoolTryToJSONStringSucceeds() { Test.assertEqual(expectedFalse, actualFalse!) } -access(all) -fun testBoolToStringSucceeds() { - let t: Bool = true - let f: Bool = false - - let expectedTrue = "true" - let expectedFalse = "false" - - var actualTrue = Serialize.boolToString(t) - var actualFalse = Serialize.boolToString(f) - - Test.assertEqual(expectedTrue, actualTrue) - Test.assertEqual(expectedFalse, actualFalse) -} - access(all) fun testArrayToJSONStringSucceeds() { let arr: [AnyStruct] = [ - 127, - 255, - "Hello, World!", - "c", - Address(0x0000000000000007), - UFix64.max, - true - ] + 127, + 255, + "Hello, World!", + "c", + Address(0x0000000000000007), + UFix64.max, + true + ] let expected = "[\"127\", \"255\", \"Hello, World!\", \"c\", \"0x0000000000000007\", \"184467440737.09551615\", \"true\"]" - var actual = Serialize.arrayToString(arr) + var actual = Serialize.arrayToJSONString(arr) Test.assertEqual(expected, actual!) } +access(all) +fun testDictToJSONStringSucceeds() { + let dict: {String: AnyStruct} = { + "bool": true, + "arr": [ 127, "Hello, World!" ] + } + + // Mapping values can be indexed in arbitrary order, so we need to check for all possible outputs + var expectedOne: String = "{\"bool\": \"true\", \"arr\": [\"127\", \"Hello, World!\"]}" + var expectedTwo: String = "{\"arr\": [\"127\", \"Hello, World!\"], \"bool\": \"true\"}" + + var actual: String? = Serialize.dictToJSONString(dict: dict, excludedNames: nil) + Test.assertEqual(true, expectedOne == actual! || expectedTwo == actual!) + + actual = Serialize.tryToJSONString(dict) + Test.assertEqual(true, expectedOne == actual! || expectedTwo == actual!) + + actual = Serialize.dictToJSONString(dict: dict, excludedNames: ["bool"]) + expectedOne = "{\"arr\": [\"127\", \"Hello, World!\"]}" + Test.assertEqual(true, expectedOne == actual!) +} diff --git a/cadence/tests/serialize_nft_tests.cdc b/cadence/tests/serialize_nft_tests.cdc new file mode 100644 index 00000000..a9291169 --- /dev/null +++ b/cadence/tests/serialize_nft_tests.cdc @@ -0,0 +1,130 @@ +import Test +import BlockchainHelpers + +import "NonFungibleToken" +import "ViewResolver" +import "MetadataViews" + +import "Serialize" +import "SerializationInterfaces" + +access(all) +let admin = Test.getAccount(0x0000000000000007) +access(all) +let alice = Test.createAccount() + +access(all) +fun setup() { + var err = Test.deployContract( + name: "ViewResolver", + path: "../contracts/standards/ViewResolver.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "Burner", + path: "../contracts/standards/Burner.cdc", + arguments: [] + ) + err = Test.deployContract( + name: "FungibleToken", + path: "../contracts/standards/FungibleToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "NonFungibleToken", + path: "../contracts/standards/NonFungibleToken.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "MetadataViews", + path: "../contracts/standards/MetadataViews.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "ExampleNFT", + path: "../contracts/example-assets/ExampleNFT.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SerializationInterfaces", + path: "../contracts/utils/SerializationInterfaces.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "Serialize", + path: "../contracts/utils/Serialize.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + err = Test.deployContract( + name: "SerializeNFT", + path: "../contracts/utils/SerializeNFT.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) +} + +access(all) +fun testSerializeNFTSucceeds() { + let setupResult = executeTransaction( + "../transactions/example-assets/setup_collection.cdc", + [], + alice + ) + Test.expect(setupResult, Test.beSucceeded()) + + let mintResult = executeTransaction( + "../transactions/example-assets/mint_nft.cdc", + [alice.address, "ExampleNFT", "Example NFT Collection", "https://flow.com/examplenft.jpg", [], [], []], + admin + ) + Test.expect(mintResult, Test.beSucceeded()) + + let expectedPrefix = "data:application/json;ascii,{\"name\": \"ExampleNFT\", \"description\": \"Example NFT Collection\", \"image\": \"https://flow.com/examplenft.jpg\", \"external_url\": \"https://example-nft.onflow.org\", " + let altSuffix1 = "\"attributes\": [{\"trait_type\": \"mintedBlock\", \"value\": \"54\"},{\"trait_type\": \"foo\", \"value\": \"nil\"}]}" + let altSuffix2 = "\"attributes\": [{\"trait_type\": \"foo\", \"value\": \"nil\"}]}, {\"trait_type\": \"mintedBlock\", \"value\": \"54\"}" + + let idsResult = executeScript( + "../scripts/nft/get_ids.cdc", + [alice.address, "cadenceExampleNFTCollection"] + ) + Test.expect(idsResult, Test.beSucceeded()) + let ids = idsResult.returnValue! as! [UInt64] + + let serializeMetadataResult = executeScript( + "../scripts/serialize/serialize_nft.cdc", + [alice.address, "cadenceExampleNFTCollection", ids[0]] + ) + Test.expect(serializeMetadataResult, Test.beSucceeded()) + + let serializedMetadata = serializeMetadataResult.returnValue! as! String + + Test.assertEqual(true, serializedMetadata == expectedPrefix.concat(altSuffix1) || serializedMetadata == expectedPrefix.concat(altSuffix2)) + // Test.assertEqual(serializedMetadata, expectedPrefix.concat(altSuffix1)) +} + +access(all) +fun testOpenSeaMetadataSerializationStrategySucceeds() { + let expectedPrefix = "data:application/json;ascii,{\"name\": \"ExampleNFT\", \"description\": \"Example NFT Collection\", \"image\": \"https://flow.com/examplenft.jpg\", \"external_url\": \"https://example-nft.onflow.org\", " + let altSuffix1 = "\"attributes\": [{\"trait_type\": \"mintedBlock\", \"value\": \"54\"},{\"trait_type\": \"foo\", \"value\": \"nil\"}]}" + let altSuffix2 = "\"attributes\": [{\"trait_type\": \"foo\", \"value\": \"nil\"}]}, {\"trait_type\": \"mintedBlock\", \"value\": \"54\"}" + + let idsResult = executeScript( + "../scripts/nft/get_ids.cdc", + [alice.address, "cadenceExampleNFTCollection"] + ) + Test.expect(idsResult, Test.beSucceeded()) + let ids = idsResult.returnValue! as! [UInt64] + + let serializeMetadataResult = executeScript( + "../scripts/serialize/serialize_nft_from_open_sea_strategy.cdc", + [alice.address, "cadenceExampleNFTCollection", ids[0]] + ) + Test.expect(serializeMetadataResult, Test.beSucceeded()) +} diff --git a/flow.json b/flow.json index ff11de22..9b42a063 100644 --- a/flow.json +++ b/flow.json @@ -16,7 +16,8 @@ "source": "./cadence/contracts/standards/Burner.cdc", "aliases": { "emulator": "ee82856bf20e2aa6", - "previewnet": "b6763b4399a888c8" + "previewnet": "b6763b4399a888c8", + "testing": "0000000000000007" } }, "CrossVMNFT": { @@ -41,7 +42,8 @@ "ExampleNFT": { "source": "./cadence/contracts/example-assets/ExampleNFT.cdc", "aliases": { - "emulator": "179b6b1cb6755e31" + "emulator": "179b6b1cb6755e31", + "testing": "0000000000000007" } }, "FlowEVMBridge": { @@ -89,6 +91,7 @@ "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", "previewnet": "a0225e7000ac82a9", + "testing": "0000000000000007", "testnet": "9a0766d93b6608b7" } }, @@ -98,6 +101,7 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "f233dcee88fe0abe", "previewnet": "a0225e7000ac82a9", + "testing": "0000000000000007", "testnet": "9a0766d93b6608b7" } }, @@ -125,6 +129,7 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "previewnet": "b6763b4399a888c8", + "testing": "0000000000000007", "testnet": "631e88ae7f1d7c20" } }, @@ -134,6 +139,7 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "previewnet": "b6763b4399a888c8", + "testing": "0000000000000007", "testnet": "631e88ae7f1d7c20" } }, @@ -150,6 +156,20 @@ "testing": "0000000000000007" } }, + "SerializeNFT": { + "source": "./cadence/contracts/utils/SerializeNFT.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, + "SerializationInterfaces": { + "source": "./cadence/contracts/utils/SerializationInterfaces.cdc", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "testing": "0000000000000007" + } + }, "StringUtils": { "source": "./cadence/contracts/utils/StringUtils.cdc", "aliases": { @@ -162,6 +182,7 @@ "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", "previewnet": "b6763b4399a888c8", + "testing": "0000000000000007", "testnet": "631e88ae7f1d7c20" } }