diff --git a/Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift b/Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift index f36b0d7..f9690a5 100644 --- a/Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift +++ b/Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift @@ -83,41 +83,64 @@ public class BoundWitnessBuilder { } return (bw, _payloads) } + + private static func filterUnderscoreKeys(_ jsonObject: Any) -> Any { + if let dictionary = jsonObject as? [String: Any] { + // Process dictionaries: filter keys, sort, and recurse + let filteredDictionary = dictionary + .filter { !$0.key.hasPrefix("_") } // Remove keys starting with "_" + .sorted { $0.key < $1.key } // Sort keys lexicographically + .reduce(into: [String: Any]()) { result, pair in + result[pair.key] = filterUnderscoreKeys(pair.value) // Recurse on values + } + return filteredDictionary + } else if let array = jsonObject as? [Any] { + // Process arrays: recursively process each element + return array.map { filterUnderscoreKeys($0) } + } else { + // Return primitives (String, Number, etc.) + return jsonObject + } + } static func hash(_ json: T) throws -> String { + if let bw = json as? BoundWitness { + return try hashWithoutUnderscores(bw) + } else { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + // Encode the object to JSON data + let data = try encoder.encode(json) + return data.sha256().toHex() + } + } + + // NOTE: Temporary fix until we have a custom JSON Serializer + // this method currently has issues with round tripping of floating + // point numbers as precision doesn't round trip + static private func hashWithoutUnderscores(_ json: T) throws -> String { + let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys - // Encode `self` to JSON data + // Encode the object to JSON data let data = try encoder.encode(json) - // Decode the JSON into a dictionary and filter keys - guard - let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) - as? [String: Any] - else { + // Decode the JSON into a dictionary, array, or primitive + guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { throw BoundWitnessBuilderError.encodingError } - let filteredJSON = jsonObject.filter { !$0.key.hasPrefix("_") } + // Recursively filter keys starting with "_" + let filteredJSON = filterUnderscoreKeys(jsonObject) - // Encode the filtered dictionary back into JSON data + // Encode the filtered JSON back to data let filteredData = try JSONSerialization.data( - withJSONObject: filteredJSON, options: [.sortedKeys]) - - // Convert the JSON data into a string - guard let jsonString = String(data: filteredData, encoding: .utf8) else { - throw BoundWitnessBuilderError.encodingError - } - + withJSONObject: filteredJSON, + options: [.sortedKeys] + ) // Hash the JSON string - let prefixesRemoved = try jsonString.sha256().toHex() - print(prefixesRemoved) - let withoutPrefixesRemoved = data.sha256().toHex() - print(withoutPrefixesRemoved) - if prefixesRemoved != withoutPrefixesRemoved { - print("Error") - } - return prefixesRemoved + return filteredData.sha256().toHex() } } diff --git a/Tests/XyoClientTests/Witness/Location/Generic/LocationPayloadTests.swift b/Tests/XyoClientTests/Witness/Location/Generic/LocationPayloadTests.swift index 3fc8f4c..377d70d 100644 --- a/Tests/XyoClientTests/Witness/Location/Generic/LocationPayloadTests.swift +++ b/Tests/XyoClientTests/Witness/Location/Generic/LocationPayloadTests.swift @@ -57,6 +57,8 @@ class LocationPayloadTests: XCTestCase { } """ XCTAssertEqual(jsonString, expectedJSON) + let hash = try BoundWitnessBuilder.hash(payload) + XCTAssertEqual(hash, "d9c4af75352658783d1b9b86d647a57d29de611261de8a3fc27019dfc3ef0dc9") } func testLocationPayloadEncodingHandlesNilValues() throws {