From 262cd43218aa237fcb97e1fe93c7e3d02caa1f1f Mon Sep 17 00:00:00 2001 From: Tang Bo Hao Date: Tue, 22 Oct 2024 16:25:38 +0800 Subject: [PATCH 01/12] feat: add a new transaction for creating new account with coa - Mainly from the usage of CEXs --- .../evm/create_new_account_with_coa.cdc | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 cadence/transactions/evm/create_new_account_with_coa.cdc diff --git a/cadence/transactions/evm/create_new_account_with_coa.cdc b/cadence/transactions/evm/create_new_account_with_coa.cdc new file mode 100644 index 00000000..512898bb --- /dev/null +++ b/cadence/transactions/evm/create_new_account_with_coa.cdc @@ -0,0 +1,64 @@ +import Crypto + +import "EVM" + +/// Creates a new Flow Address and EVM account with a Cadence Owned Account (COA) stored in the account's storage. +/// +transaction( + key: String, // key to be used for the account + signatureAlgorithm: UInt8, // signature algorithm to be used for the account + hashAlgorithm: UInt8, // hash algorithm to be used for the account + weight: UFix64, // weight to be used for the account +) { + let auth: auth(Storage, Keys, Capabilities) &Account + + prepare(signer: auth(Storage, Keys, Capabilities) &Account) { + pre { + signatureAlgorithm >= 1 && signatureAlgorithm <= 3: + "Cannot add Key: Must provide a signature algorithm raw value that corresponds to " + .concat("one of the available signature algorithms for Flow keys.") + .concat("You provided ").concat(signatureAlgorithm.toString()) + .concat(" but the options are either 1 (ECDSA_P256), 2 (ECDSA_secp256k1), or 3 (BLS_BLS12_381).") + hashAlgorithm >= 1 && hashAlgorithm <= 6: + "Cannot add Key: Must provide a hash algorithm raw value that corresponds to " + .concat("one of of the available hash algorithms for Flow keys.") + .concat("You provided ").concat(hashAlgorithm.toString()) + .concat(" but the options are 1 (SHA2_256), 2 (SHA2_384), 3 (SHA3_256), ") + .concat("4 (SHA3_384), 5 (KMAC128_BLS_BLS12_381), or 6 (KECCAK_256).") + weight <= 1000.0: + "Cannot add Key: The key weight must be between 0 and 1000." + .concat(" You provided ").concat(weight.toString()).concat(" which is invalid.") + } + + self.auth = signer + } + + execute { + // Create a new public key + let publicKey = PublicKey( + publicKey: key.decodeHex(), + signatureAlgorithm: SignatureAlgorithm(rawValue: signatureAlgorithm)! + ) + + // Create a new account + let account = Account(payer: self.auth) + + // Add the public key to the account + account.keys.add( + publicKey: publicKey, + hashAlgorithm: HashAlgorithm(rawValue: hashAlgorithm)!, + weight: weight + ) + + // Create a new COA + let coa <- EVM.createCadenceOwnedAccount() + + // Save the COA to the new account + let storagePath = StoragePath(identifier: "evm")! + let publicPath = PublicPath(identifier: "evm")! + account.storage.save<@EVM.CadenceOwnedAccount>(<-coa, to: storagePath) + let addressableCap = account.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath) + account.capabilities.unpublish(publicPath) + account.capabilities.publish(addressableCap, at: publicPath) + } +} From 3b438360276eaa9eee38d277970b4c5def991ff4 Mon Sep 17 00:00:00 2001 From: Tang Bo Hao Date: Tue, 22 Oct 2024 16:32:24 +0800 Subject: [PATCH 02/12] chore: tab fix --- .../evm/create_new_account_with_coa.cdc | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cadence/transactions/evm/create_new_account_with_coa.cdc b/cadence/transactions/evm/create_new_account_with_coa.cdc index 512898bb..8127c349 100644 --- a/cadence/transactions/evm/create_new_account_with_coa.cdc +++ b/cadence/transactions/evm/create_new_account_with_coa.cdc @@ -13,38 +13,38 @@ transaction( let auth: auth(Storage, Keys, Capabilities) &Account prepare(signer: auth(Storage, Keys, Capabilities) &Account) { - pre { - signatureAlgorithm >= 1 && signatureAlgorithm <= 3: + pre { + signatureAlgorithm >= 1 && signatureAlgorithm <= 3: "Cannot add Key: Must provide a signature algorithm raw value that corresponds to " .concat("one of the available signature algorithms for Flow keys.") .concat("You provided ").concat(signatureAlgorithm.toString()) .concat(" but the options are either 1 (ECDSA_P256), 2 (ECDSA_secp256k1), or 3 (BLS_BLS12_381).") - hashAlgorithm >= 1 && hashAlgorithm <= 6: + hashAlgorithm >= 1 && hashAlgorithm <= 6: "Cannot add Key: Must provide a hash algorithm raw value that corresponds to " .concat("one of of the available hash algorithms for Flow keys.") .concat("You provided ").concat(hashAlgorithm.toString()) .concat(" but the options are 1 (SHA2_256), 2 (SHA2_384), 3 (SHA3_256), ") .concat("4 (SHA3_384), 5 (KMAC128_BLS_BLS12_381), or 6 (KECCAK_256).") - weight <= 1000.0: + weight <= 1000.0: "Cannot add Key: The key weight must be between 0 and 1000." .concat(" You provided ").concat(weight.toString()).concat(" which is invalid.") - } + } self.auth = signer } execute { // Create a new public key - let publicKey = PublicKey( - publicKey: key.decodeHex(), - signatureAlgorithm: SignatureAlgorithm(rawValue: signatureAlgorithm)! - ) + let publicKey = PublicKey( + publicKey: key.decodeHex(), + signatureAlgorithm: SignatureAlgorithm(rawValue: signatureAlgorithm)! + ) // Create a new account - let account = Account(payer: self.auth) + let account = Account(payer: self.auth) // Add the public key to the account - account.keys.add( + account.keys.add( publicKey: publicKey, hashAlgorithm: HashAlgorithm(rawValue: hashAlgorithm)!, weight: weight From ea8630cac216ff63cecf0fd4d526cf9fbdb65981 Mon Sep 17 00:00:00 2001 From: Tang Bo Hao Date: Wed, 23 Oct 2024 10:54:56 +0800 Subject: [PATCH 03/12] feat: hard code weight to 1000.0 and limit the algorithms --- .../evm/create_new_account_with_coa.cdc | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/cadence/transactions/evm/create_new_account_with_coa.cdc b/cadence/transactions/evm/create_new_account_with_coa.cdc index 8127c349..264e0b11 100644 --- a/cadence/transactions/evm/create_new_account_with_coa.cdc +++ b/cadence/transactions/evm/create_new_account_with_coa.cdc @@ -2,32 +2,28 @@ import Crypto import "EVM" -/// Creates a new Flow Address and EVM account with a Cadence Owned Account (COA) stored in the account's storage. +/// Creates a new Flow Address with a single full-weight key and its EVM account, which is +/// a Cadence Owned Account (COA) stored in the account's storage. /// transaction( key: String, // key to be used for the account signatureAlgorithm: UInt8, // signature algorithm to be used for the account hashAlgorithm: UInt8, // hash algorithm to be used for the account - weight: UFix64, // weight to be used for the account ) { let auth: auth(Storage, Keys, Capabilities) &Account prepare(signer: auth(Storage, Keys, Capabilities) &Account) { pre { - signatureAlgorithm >= 1 && signatureAlgorithm <= 3: + signatureAlgorithm == 1 || signatureAlgorithm == 2: "Cannot add Key: Must provide a signature algorithm raw value that corresponds to " .concat("one of the available signature algorithms for Flow keys.") .concat("You provided ").concat(signatureAlgorithm.toString()) - .concat(" but the options are either 1 (ECDSA_P256), 2 (ECDSA_secp256k1), or 3 (BLS_BLS12_381).") - hashAlgorithm >= 1 && hashAlgorithm <= 6: + .concat(" but the options are either 1 (ECDSA_P256), 2 (ECDSA_secp256k1).") + hashAlgorithm == 1 || hashAlgorithm == 3: "Cannot add Key: Must provide a hash algorithm raw value that corresponds to " .concat("one of of the available hash algorithms for Flow keys.") .concat("You provided ").concat(hashAlgorithm.toString()) - .concat(" but the options are 1 (SHA2_256), 2 (SHA2_384), 3 (SHA3_256), ") - .concat("4 (SHA3_384), 5 (KMAC128_BLS_BLS12_381), or 6 (KECCAK_256).") - weight <= 1000.0: - "Cannot add Key: The key weight must be between 0 and 1000." - .concat(" You provided ").concat(weight.toString()).concat(" which is invalid.") + .concat(" but the options are 1 (SHA2_256), 3 (SHA3_256), ") } self.auth = signer @@ -47,7 +43,7 @@ transaction( account.keys.add( publicKey: publicKey, hashAlgorithm: HashAlgorithm(rawValue: hashAlgorithm)!, - weight: weight + weight: 1000.0 ) // Create a new COA From ce1239a14575faf5f3bd728b0dde057af7195f25 Mon Sep 17 00:00:00 2001 From: Tang Bo Hao Date: Wed, 23 Oct 2024 10:57:26 +0800 Subject: [PATCH 04/12] chore: typo --- cadence/transactions/evm/create_new_account_with_coa.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/transactions/evm/create_new_account_with_coa.cdc b/cadence/transactions/evm/create_new_account_with_coa.cdc index 264e0b11..e7415138 100644 --- a/cadence/transactions/evm/create_new_account_with_coa.cdc +++ b/cadence/transactions/evm/create_new_account_with_coa.cdc @@ -23,7 +23,7 @@ transaction( "Cannot add Key: Must provide a hash algorithm raw value that corresponds to " .concat("one of of the available hash algorithms for Flow keys.") .concat("You provided ").concat(hashAlgorithm.toString()) - .concat(" but the options are 1 (SHA2_256), 3 (SHA3_256), ") + .concat(" but the options are 1 (SHA2_256), 3 (SHA3_256).") } self.auth = signer From 404a0ec6506f7c870bcb376139a586163505a46a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:28:57 -0600 Subject: [PATCH 05/12] remove previewnet from flow.json --- flow.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/flow.json b/flow.json index f39cf1d0..65384829 100644 --- a/flow.json +++ b/flow.json @@ -264,7 +264,6 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", - "previewnet": "b6763b4399a888c8", "testnet": "8c5303eaa26202d6" } }, @@ -274,7 +273,6 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "e467b9dd11fa00df", - "previewnet": "b6763b4399a888c8", "testnet": "8c5303eaa26202d6" } }, @@ -284,7 +282,6 @@ "aliases": { "emulator": "0ae53cb6e3f42a79", "mainnet": "1654653399040a61", - "previewnet": "4445e7ad11568276", "testnet": "7e60df042a9c0868" } }, @@ -294,7 +291,6 @@ "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", - "previewnet": "a0225e7000ac82a9", "testnet": "9a0766d93b6608b7" } }, @@ -304,7 +300,6 @@ "aliases": { "emulator": "ee82856bf20e2aa6", "mainnet": "f233dcee88fe0abe", - "previewnet": "a0225e7000ac82a9", "testnet": "9a0766d93b6608b7" } }, @@ -314,7 +309,6 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "previewnet": "b6763b4399a888c8", "testnet": "631e88ae7f1d7c20" } }, @@ -324,7 +318,6 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "previewnet": "b6763b4399a888c8", "testnet": "631e88ae7f1d7c20" } }, @@ -334,7 +327,6 @@ "aliases": { "emulator": "f8d6e0586b0a20c7", "mainnet": "1d7e57aa55817448", - "previewnet": "b6763b4399a888c8", "testnet": "631e88ae7f1d7c20" } } From 348012239adec69b42d78cc09b4c6a438074274b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:30:14 -0600 Subject: [PATCH 06/12] optimize SerializeMetadata --- cadence/contracts/utils/SerializeMetadata.cdc | 66 ++++++++++--------- cadence/tests/serialize_metadata_tests.cdc | 1 - 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/cadence/contracts/utils/SerializeMetadata.cdc b/cadence/contracts/utils/SerializeMetadata.cdc index 7a93695b..3f0a4d8d 100644 --- a/cadence/contracts/utils/SerializeMetadata.cdc +++ b/cadence/contracts/utils/SerializeMetadata.cdc @@ -42,20 +42,16 @@ access(all) contract SerializeMetadata { 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!) + parts.appendAll([display!, ", "]) // Include display if present & separate with a comma } - if display != nil && attributes != nil { - serializedMetadata = serializedMetadata.concat(", ") - } - if attributes != nil { - serializedMetadata = serializedMetadata.concat(attributes) - } - 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. /// @@ -80,30 +76,34 @@ 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 @@ -124,7 +124,7 @@ access(all) contract SerializeMetadata { 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) + serializedResult = serializedResult.slice(from: 0, upTo: serializedResult.length - 1) } continue } @@ -143,10 +143,10 @@ access(all) contract SerializeMetadata { return serializedResult.concat("]") } - /// 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: @@ -162,13 +162,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. diff --git a/cadence/tests/serialize_metadata_tests.cdc b/cadence/tests/serialize_metadata_tests.cdc index 40498d5c..16e0a9c5 100644 --- a/cadence/tests/serialize_metadata_tests.cdc +++ b/cadence/tests/serialize_metadata_tests.cdc @@ -74,7 +74,6 @@ fun testSerializeNFTSucceeds() { Test.expect(serializeMetadataResult, Test.beSucceeded()) let serializedMetadata = serializeMetadataResult.returnValue! as! String - Test.assertEqual(true, serializedMetadata == expectedPrefix.concat(altSuffix1) || serializedMetadata == expectedPrefix.concat(altSuffix2)) } From 7f552298b92dd67d1a3a06628e8d0f40e88aa88a Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:30:31 -0600 Subject: [PATCH 07/12] optimize Serialize --- cadence/contracts/utils/Serialize.cdc | 84 +++++++++++---------------- cadence/tests/serialize_tests.cdc | 1 + 2 files changed, 35 insertions(+), 50 deletions(-) diff --git a/cadence/contracts/utils/Serialize.cdc b/cadence/contracts/utils/Serialize.cdc index 0e2dc9ef..6c83f2ce 100644 --- a/cadence/contracts/utils/Serialize.cdc +++ b/cadence/contracts/utils/Serialize.cdc @@ -27,59 +27,59 @@ contract Serialize { case Type(): return "\"nil\"" case Type(): - return "\"".concat(value as! String).concat("\"") + return String.join(["\"", value as! String, "\"" ], separator: "") case Type(): - return "\"".concat(value as? String ?? "nil").concat("\"") + return String.join(["\"", value as? String ?? "nil", "\"" ], separator: "") case Type(): - return "\"".concat((value as! Character).toString()).concat("\"") + return String.join(["\"", (value as! Character).toString(), "\"" ], separator: "") case Type(): - return "\"".concat(value as! Bool ? "true" : "false").concat("\"") + return String.join(["\"", value as! Bool ? "true" : "false", "\"" ], separator: "") case Type
(): - return "\"".concat((value as! Address).toString()).concat("\"") + return String.join(["\"", (value as! Address).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as? Address)?.toString() ?? "nil").concat("\"") + return String.join(["\"", (value as? Address)?.toString() ?? "nil", "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int8).toString()).concat("\"") + return String.join(["\"", (value as! Int8).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int16).toString()).concat("\"") + return String.join(["\"", (value as! Int16).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int32).toString()).concat("\"") + return String.join(["\"", (value as! Int32).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int64).toString()).concat("\"") + return String.join(["\"", (value as! Int64).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int128).toString()).concat("\"") + return String.join(["\"", (value as! Int128).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int256).toString()).concat("\"") + return String.join(["\"", (value as! Int256).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Int).toString()).concat("\"") + return String.join(["\"", (value as! Int).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt8).toString()).concat("\"") + return String.join(["\"", (value as! UInt8).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt16).toString()).concat("\"") + return String.join(["\"", (value as! UInt16).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt32).toString()).concat("\"") + return String.join(["\"", (value as! UInt32).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt64).toString()).concat("\"") + return String.join(["\"", (value as! UInt64).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt128).toString()).concat("\"") + return String.join(["\"", (value as! UInt128).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt256).toString()).concat("\"") + return String.join(["\"", (value as! UInt256).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UInt).toString()).concat("\"") + return String.join(["\"", (value as! UInt).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word8).toString()).concat("\"") + return String.join(["\"", (value as! Word8).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word16).toString()).concat("\"") + return String.join(["\"", (value as! Word16).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word32).toString()).concat("\"") + return String.join(["\"", (value as! Word32).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word64).toString()).concat("\"") + return String.join(["\"", (value as! Word64).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word128).toString()).concat("\"") + return String.join(["\"", (value as! Word128).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! Word256).toString()).concat("\"") + return String.join(["\"", (value as! Word256).toString(), "\"" ], separator: "") case Type(): - return "\"".concat((value as! UFix64).toString()).concat("\"") + return String.join(["\"", (value as! UFix64).toString(), "\"" ], separator: "") default: return nil } @@ -89,24 +89,15 @@ contract Serialize { /// access(all) fun arrayToJSONString(_ arr: [AnyStruct]): String? { - var serializedArr = "[" - let arrLength = arr.length + let parts: [String]= [] for i, 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. @@ -120,22 +111,15 @@ contract Serialize { dict.remove(key: k) } } - var serializedDict = "{" - let dictLength = dict.length + let parts: [String] = [] for i, 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("}") } } diff --git a/cadence/tests/serialize_tests.cdc b/cadence/tests/serialize_tests.cdc index e472b907..b0c0b5ef 100644 --- a/cadence/tests/serialize_tests.cdc +++ b/cadence/tests/serialize_tests.cdc @@ -239,6 +239,7 @@ fun testDictToJSONStringSucceeds() { var expectedTwo: String = "{\"arr\": [\"127\", \"Hello, World!\"], \"bool\": \"true\"}" var actual: String? = Serialize.dictToJSONString(dict: dict, excludedNames: nil) + log(actual) Test.assertEqual(true, expectedOne == actual! || expectedTwo == actual!) actual = Serialize.tryToJSONString(dict) From 6f9f8624c07b526b1753754c68dd785547ef8443 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:50:03 -0600 Subject: [PATCH 08/12] fix serializeNFTMetadataAsURI conditional --- cadence/contracts/utils/SerializeMetadata.cdc | 5 +++-- cadence/tests/serialize_tests.cdc | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cadence/contracts/utils/SerializeMetadata.cdc b/cadence/contracts/utils/SerializeMetadata.cdc index 3f0a4d8d..d38c4045 100644 --- a/cadence/contracts/utils/SerializeMetadata.cdc +++ b/cadence/contracts/utils/SerializeMetadata.cdc @@ -31,14 +31,15 @@ access(all) contract SerializeMetadata { // 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? + // 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()) 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 diff --git a/cadence/tests/serialize_tests.cdc b/cadence/tests/serialize_tests.cdc index b0c0b5ef..e472b907 100644 --- a/cadence/tests/serialize_tests.cdc +++ b/cadence/tests/serialize_tests.cdc @@ -239,7 +239,6 @@ fun testDictToJSONStringSucceeds() { var expectedTwo: String = "{\"arr\": [\"127\", \"Hello, World!\"], \"bool\": \"true\"}" var actual: String? = Serialize.dictToJSONString(dict: dict, excludedNames: nil) - log(actual) Test.assertEqual(true, expectedOne == actual! || expectedTwo == actual!) actual = Serialize.tryToJSONString(dict) From 56cc52e85af6985debf85f2d07564f7e982525eb Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:18:00 -0600 Subject: [PATCH 09/12] optimize serializeNFTTraitsAsAttributes & add single trait serialization --- cadence/contracts/utils/Serialize.cdc | 4 +- cadence/contracts/utils/SerializeMetadata.cdc | 48 +++++++++++-------- cadence/tests/serialize_metadata_tests.cdc | 5 +- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/cadence/contracts/utils/Serialize.cdc b/cadence/contracts/utils/Serialize.cdc index 6c83f2ce..d8123728 100644 --- a/cadence/contracts/utils/Serialize.cdc +++ b/cadence/contracts/utils/Serialize.cdc @@ -90,7 +90,7 @@ contract Serialize { access(all) fun arrayToJSONString(_ arr: [AnyStruct]): String? { let parts: [String]= [] - for i, element in arr { + for element in arr { let serializedElement = self.tryToJSONString(element) if serializedElement == nil { continue @@ -112,7 +112,7 @@ contract Serialize { } } let parts: [String] = [] - for i, key in dict.keys { + for key in dict.keys { let serializedValue = self.tryToJSONString(dict[key]!) if serializedValue == nil { continue diff --git a/cadence/contracts/utils/SerializeMetadata.cdc b/cadence/contracts/utils/SerializeMetadata.cdc index d38c4045..0f299bec 100644 --- a/cadence/contracts/utils/SerializeMetadata.cdc +++ b/cadence/contracts/utils/SerializeMetadata.cdc @@ -112,36 +112,44 @@ access(all) contract SerializeMetadata { /// /// @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\": \"\", \"display_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\": [" + 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) - } + 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": "", "display_type": "", "value": ""}` + access(all) + fun serializeNFTTraitAsAttribute(_ trait: MetadataViews.Trait): String? { + let value = Serialize.tryToJSONString(trait.value) + if value == nil { + return nil } - return serializedResult.concat("]") + let parts: [String] = ["{"] + parts.appendAll( [ "\"trait_type\": ", Serialize.tryToJSONString(trait.name)! ] ) + if trait.displayType != nil { + parts.appendAll( [ ", \"display_type\": ", Serialize.tryToJSONString(trait.displayType)! ] ) + } + 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 diff --git a/cadence/tests/serialize_metadata_tests.cdc b/cadence/tests/serialize_metadata_tests.cdc index 16e0a9c5..f35ddb23 100644 --- a/cadence/tests/serialize_metadata_tests.cdc +++ b/cadence/tests/serialize_metadata_tests.cdc @@ -56,9 +56,10 @@ fun testSerializeNFTSucceeds() { mintedBlockHeight = heightResult.returnValue! as! UInt64 let heightString = mintedBlockHeight.toString() + // Cadence dictionaries are not ordered by insertion order, so we need to check for both possible orderings let expectedPrefix = "data:application/json;utf8,{\"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\": \"".concat(heightString).concat("\"},{\"trait_type\": \"foo\", \"value\": \"nil\"}]}") - let altSuffix2 = "\"attributes\": [{\"trait_type\": \"foo\", \"value\": \"nil\"}]}, {\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}") + let altSuffix1 = "\"attributes\": [{\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}, {\"trait_type\": \"foo\", \"value\": \"nil\"}]}") + let altSuffix2 = "\"attributes\": [{\"trait_type\": \"foo\", \"value\": \"nil\"}, {\"trait_type\": \"mintedBlock\", \"value\": \"".concat(heightString).concat("\"}]}") let idsResult = executeScript( "../scripts/nft/get_ids.cdc", From 7898bff3ac6bde05118c529abd4802cb9f034e8b Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:16:28 -0600 Subject: [PATCH 10/12] update attributions --- cadence/contracts/utils/Serialize.cdc | 2 +- cadence/contracts/utils/SerializeMetadata.cdc | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cadence/contracts/utils/Serialize.cdc b/cadence/contracts/utils/Serialize.cdc index d8123728..41c9892f 100644 --- a/cadence/contracts/utils/Serialize.cdc +++ b/cadence/contracts/utils/Serialize.cdc @@ -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 { diff --git a/cadence/contracts/utils/SerializeMetadata.cdc b/cadence/contracts/utils/SerializeMetadata.cdc index 0f299bec..965bf0ba 100644 --- a/cadence/contracts/utils/SerializeMetadata.cdc +++ b/cadence/contracts/utils/SerializeMetadata.cdc @@ -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 From 653fb06bd2729bdeecfe3716e92314bcb36d3429 Mon Sep 17 00:00:00 2001 From: "BT.Wood(Tang Bo Hao)" Date: Thu, 24 Oct 2024 10:51:13 +0800 Subject: [PATCH 11/12] Update cadence/transactions/evm/create_new_account_with_coa.cdc Co-authored-by: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> --- cadence/transactions/evm/create_new_account_with_coa.cdc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadence/transactions/evm/create_new_account_with_coa.cdc b/cadence/transactions/evm/create_new_account_with_coa.cdc index e7415138..65f31685 100644 --- a/cadence/transactions/evm/create_new_account_with_coa.cdc +++ b/cadence/transactions/evm/create_new_account_with_coa.cdc @@ -10,9 +10,9 @@ transaction( signatureAlgorithm: UInt8, // signature algorithm to be used for the account hashAlgorithm: UInt8, // hash algorithm to be used for the account ) { - let auth: auth(Storage, Keys, Capabilities) &Account + let auth: auth(BorrowValue) &Account - prepare(signer: auth(Storage, Keys, Capabilities) &Account) { + prepare(signer: auth(BorrowValue) &Account) { pre { signatureAlgorithm == 1 || signatureAlgorithm == 2: "Cannot add Key: Must provide a signature algorithm raw value that corresponds to " From fae3977a20c438ff0323b7fc40a66be36eaf3393 Mon Sep 17 00:00:00 2001 From: Giovanni Sanchez <108043524+sisyphusSmiling@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:08:57 -0600 Subject: [PATCH 12/12] remove unused value from SerializeMetadata --- cadence/contracts/utils/SerializeMetadata.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadence/contracts/utils/SerializeMetadata.cdc b/cadence/contracts/utils/SerializeMetadata.cdc index 965bf0ba..1ceca382 100644 --- a/cadence/contracts/utils/SerializeMetadata.cdc +++ b/cadence/contracts/utils/SerializeMetadata.cdc @@ -123,7 +123,7 @@ access(all) contract SerializeMetadata { // Serialize each trait as an attribute, building the serialized JSON compatible string let parts: [String] = [] let traitsLength = traits.traits.length - for i, trait in traits.traits { + for trait in traits.traits { let attribute = self.serializeNFTTraitAsAttribute(trait) if attribute == nil { continue