diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..2bcdce5 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,23 @@ +{ + "pins" : [ + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "cryptoswiftwrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/anquii/CryptoSwiftWrapper.git", + "state" : { + "revision" : "0d57e806de368b5d2364fabb7c789b48661ec90c", + "version" : "1.4.3" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..086cc4f --- /dev/null +++ b/Package.swift @@ -0,0 +1,40 @@ +// swift-tools-version:5.7 + +import PackageDescription + +let package = Package( + name: "Navcoin", + platforms: [ + .macOS(.v11), + .iOS(.v14) + ], + products: [ + .library( + name: "Navcoin", + targets: ["Navcoin"] + ) + ], + dependencies: [ + .package( + url: "https://github.com/attaswift/BigInt.git", + .upToNextMajor(from: "5.3.0") + ), + .package( + url: "https://github.com/anquii/CryptoSwiftWrapper.git", + .upToNextMajor(from: "1.4.3") + ) + ], + targets: [ + .target( + name: "Navcoin", + dependencies: [ + "BigInt", + "CryptoSwiftWrapper" + ] + ), + .testTarget( + name: "NavcoinTests", + dependencies: ["Navcoin"] + ) + ] +) diff --git a/README.md b/README.md index 106e619..6d1ff55 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Swift Package Manager compatible](https://img.shields.io/badge/SPM-compatible-orange)](#swift-package-manager) [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/anquii/Navcoin/blob/main/LICENSE) -An implementation to interact with [Navcoin](https://github.com/navcoin/navcoin) in Swift. +An implementation to interact with [Navcoin](https://navcoin.org)'s upcoming [PePoS](https://medium.com/nav-coin/announcing-pepos-a-privacy-enhanced-proof-of-stake-protocol-95c3149e8bd6) protocol in Swift. ## Platforms - macOS 11+ @@ -16,7 +16,7 @@ An implementation to interact with [Navcoin](https://github.com/navcoin/navcoin) Add the following line to your `Package.swift` file: ```swift -.package(url: "https://github.com/anquii/Navcoin.git", from: "1.0.0") +.package(url: "https://github.com/anquii/Navcoin.git", from: "0.1.0") ``` ...or integrate with Xcode via `File -> Swift Packages -> Add Package Dependency...` using the URL of the repository. @@ -24,6 +24,12 @@ Add the following line to your `Package.swift` file: ```swift import Navcoin + +let transactionSerializer = TransactionSerializer() +let data = transactionSerializer.data(transaction: transaction) + +let transactionDeserializer = TransactionDeserializer() +let transaction = transactionDeserializer.transaction(data: data) ``` ## License diff --git a/Sources/Navcoin/CompactSizeUInt.swift b/Sources/Navcoin/CompactSizeUInt.swift new file mode 100644 index 0000000..e356913 --- /dev/null +++ b/Sources/Navcoin/CompactSizeUInt.swift @@ -0,0 +1,76 @@ +import Foundation + +struct CompactSizeUInt { + let value: UInt + + init(_ value: UInt) { + self.value = value + } + + init(_ value: Int) { + guard value >= 0 else { + preconditionFailure() + } + self.value = UInt(value) + } + + init?(data: Data) { + guard !data.isEmpty, let firstByte = data.first else { + return nil + } + if firstByte <= 0xfc { + value = UInt(firstByte) + return + } + let startIndex = data.startIndex + 1 + var endIndex = startIndex + switch firstByte { + case 0xfd: + endIndex += 2 + case 0xfe: + endIndex += 4 + case 0xff: + endIndex += 8 + default: + preconditionFailure() + } + guard + data.indices.contains(startIndex), + data.indices.contains(endIndex - 1), + let unsignedInteger = UInt(data: data[startIndex.. Data { + let data = self.serialize() + guard data.count < requiredLength else { + return data + } + let leadingZeroesCount = requiredLength - data.count + let leadingZeroes = Data(repeating: 0, count: leadingZeroesCount) + return leadingZeroes + data + } +} diff --git a/Sources/Navcoin/Extensions/Data+Extension.swift b/Sources/Navcoin/Extensions/Data+Extension.swift new file mode 100644 index 0000000..39e9d71 --- /dev/null +++ b/Sources/Navcoin/Extensions/Data+Extension.swift @@ -0,0 +1,16 @@ +import Foundation +import CryptoSwift + +extension Data { + init(hex: String) { + self.init(Array(hex: hex)) + } + + var bytes: Array { + Array(self) + } + + func toHexString() -> String { + bytes.toHexString() + } +} diff --git a/Sources/Navcoin/Extensions/FixedWidthInteger+Extension.swift b/Sources/Navcoin/Extensions/FixedWidthInteger+Extension.swift new file mode 100644 index 0000000..2ded69b --- /dev/null +++ b/Sources/Navcoin/Extensions/FixedWidthInteger+Extension.swift @@ -0,0 +1,14 @@ +import Foundation + +extension FixedWidthInteger { + init?(data: Data) { + guard let value = Self(data.toHexString(), radix: 16) else { + return nil + } + self = value + } + + var bytes: [UInt8] { + withUnsafeBytes(of: self, Array.init) + } +} diff --git a/Sources/Navcoin/Extensions/Int+Extension.swift b/Sources/Navcoin/Extensions/Int+Extension.swift new file mode 100644 index 0000000..70b1ace --- /dev/null +++ b/Sources/Navcoin/Extensions/Int+Extension.swift @@ -0,0 +1,8 @@ +extension Int { + init?(unsignedInteger: UInt) { + guard unsignedInteger <= Int64.max else { + return nil + } + self = Int(unsignedInteger) + } +} diff --git a/Sources/Navcoin/Transaction/Outpoint.swift b/Sources/Navcoin/Transaction/Outpoint.swift new file mode 100644 index 0000000..980683d --- /dev/null +++ b/Sources/Navcoin/Transaction/Outpoint.swift @@ -0,0 +1,11 @@ +import BigInt + +public struct Outpoint { + public let id: BigUInt + public let index: UInt32 + + public init(id: BigUInt, index: UInt32) { + self.id = id + self.index = index + } +} diff --git a/Sources/Navcoin/Transaction/RangeProof.swift b/Sources/Navcoin/Transaction/RangeProof.swift new file mode 100644 index 0000000..15a33bb --- /dev/null +++ b/Sources/Navcoin/Transaction/RangeProof.swift @@ -0,0 +1,44 @@ +import Foundation + +public struct RangeProof { + public let Vs: [Data] + public let Ls: [Data] + public let Rs: [Data] + public let A: Data + public let S: Data + public let T1: Data + public let T2: Data + public let taux: Data + public let mu: Data + public let a: Data + public let b: Data + public let t: Data + + public init( + Vs: [Data], + Ls: [Data], + Rs: [Data], + A: Data, + S: Data, + T1: Data, + T2: Data, + taux: Data, + mu: Data, + a: Data, + b: Data, + t: Data + ) { + self.Vs = Vs + self.Ls = Ls + self.Rs = Rs + self.A = A + self.S = S + self.T1 = T1 + self.T2 = T2 + self.taux = taux + self.mu = mu + self.a = a + self.b = b + self.t = t + } +} diff --git a/Sources/Navcoin/Transaction/TokenIdentifier.swift b/Sources/Navcoin/Transaction/TokenIdentifier.swift new file mode 100644 index 0000000..63f007a --- /dev/null +++ b/Sources/Navcoin/Transaction/TokenIdentifier.swift @@ -0,0 +1,11 @@ +import BigInt + +public struct TokenIdentifier { + public let id: BigUInt + public let nftId: UInt64 + + public init(id: BigUInt, nftId: UInt64) { + self.id = id + self.nftId = nftId + } +} diff --git a/Sources/Navcoin/Transaction/Transaction.swift b/Sources/Navcoin/Transaction/Transaction.swift new file mode 100644 index 0000000..0322edb --- /dev/null +++ b/Sources/Navcoin/Transaction/Transaction.swift @@ -0,0 +1,26 @@ +import Foundation + +public struct Transaction { + public let version: Int32 + public let inputs: [TransactionInput]? + public let outputs: [TransactionOutput] + public let witnesses: [TransactionWitness]? + public let signature: Data? + public let lockTime: UInt32 + + public init( + version: Int32, + inputs: [TransactionInput]? = nil, + outputs: [TransactionOutput], + witnesses: [TransactionWitness]? = nil, + signature: Data? = nil, + lockTime: UInt32 + ) { + self.version = version + self.inputs = inputs + self.outputs = outputs + self.witnesses = witnesses + self.signature = signature + self.lockTime = lockTime + } +} diff --git a/Sources/Navcoin/Transaction/TransactionInput.swift b/Sources/Navcoin/Transaction/TransactionInput.swift new file mode 100644 index 0000000..cb33155 --- /dev/null +++ b/Sources/Navcoin/Transaction/TransactionInput.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct TransactionInput { + public let outpoint: Outpoint + public let scriptSignature: Data? + public let sequence: UInt32 + + public init(outpoint: Outpoint, scriptSignature: Data?, sequence: UInt32) { + self.outpoint = outpoint + self.scriptSignature = scriptSignature + self.sequence = sequence + } +} diff --git a/Sources/Navcoin/Transaction/TransactionOutput.swift b/Sources/Navcoin/Transaction/TransactionOutput.swift new file mode 100644 index 0000000..0aa0053 --- /dev/null +++ b/Sources/Navcoin/Transaction/TransactionOutput.swift @@ -0,0 +1,32 @@ +import Foundation + +public struct TransactionOutput { + public let value: Int64 + public let publicScriptKey: Data? + public let rangeProof: RangeProof? + public let publicBLSSpendKey: Data? + public let publicBLSBlindKey: Data? + public let publicBLSEphemeralKey: Data? + public let viewTag: UInt16? + public let tokenIdentifier: TokenIdentifier? + + public init( + value: Int64, + publicScriptKey: Data?, + rangeProof: RangeProof? = nil, + publicBLSSpendKey: Data? = nil, + publicBLSBlindKey: Data? = nil, + publicBLSEphemeralKey: Data? = nil, + viewTag: UInt16? = nil, + tokenIdentifier: TokenIdentifier? = nil + ) { + self.value = value + self.publicScriptKey = publicScriptKey + self.rangeProof = rangeProof + self.publicBLSSpendKey = publicBLSSpendKey + self.publicBLSBlindKey = publicBLSBlindKey + self.publicBLSEphemeralKey = publicBLSEphemeralKey + self.viewTag = viewTag + self.tokenIdentifier = tokenIdentifier + } +} diff --git a/Sources/Navcoin/Transaction/TransactionWitness.swift b/Sources/Navcoin/Transaction/TransactionWitness.swift new file mode 100644 index 0000000..7d49a96 --- /dev/null +++ b/Sources/Navcoin/Transaction/TransactionWitness.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct TransactionWitness { + public let stack: [Data] + + public init(stack: [Data]) { + self.stack = stack + } +} diff --git a/Sources/Navcoin/TransactionDeserializer/TransactionDeserializer.swift b/Sources/Navcoin/TransactionDeserializer/TransactionDeserializer.swift new file mode 100644 index 0000000..40bb27a --- /dev/null +++ b/Sources/Navcoin/TransactionDeserializer/TransactionDeserializer.swift @@ -0,0 +1,278 @@ +import Foundation +import BigInt + +public protocol TransactionDeserializing { + func transaction(data: Data) -> Transaction? +} + +public final class TransactionDeserializer: TransactionDeserializing { + public init() {} + + public func transaction(data: Data) -> Transaction? { + do { + return try transactionThrows(data: data) + } catch let error as ErrorDescribing { + print(error.description) + return nil + } catch { + preconditionFailure() + } + } +} + +extension TransactionDeserializer { + func transactionThrows(data: Data) throws -> Transaction { + /// Returns a data slice with `nextDataIndex` as the lower bound of the range, and with `nextByteCount` used to determine the upper bound + func dataSlice(nextByteCount: Int, byteOrderReversed: Bool = false) -> Data? { + let range = nextDataIndex..<(nextDataIndex + nextByteCount) + guard nextByteCount > 0, data.indices.contains(range.lowerBound), data.indices.contains(range.upperBound - 1) else { + return nil + } + nextDataIndex = range.upperBound + guard !byteOrderReversed else { + return Data(data[range].reversed()) + } + return data[range] + } + /// Sets the length of the next vector starting from `nextDataIndex`, and returns `true` if successful + func setNextVectorLength() -> Bool { + let range: Range + let rangeEndIndex = nextDataIndex + 9 + if rangeEndIndex < data.count { + range = nextDataIndex.. Data +} + +public struct TransactionSerializer: TransactionSerializing { + public init() {} + + public func data(transaction: Transaction) -> Data { + do { + return try dataThrows(transaction: transaction) + } catch let error as ErrorDescribing { + preconditionFailure(error.description) + } catch { + preconditionFailure() + } + } +} + +extension TransactionSerializer { + func dataThrows(transaction: Transaction) throws -> Data { + var data = Data() + // MARK: - Version + data += transaction.version.bytes + var flag = UInt8(0x00) + if (transaction.version & 0x40000000) == 0, transaction.witnesses?.isEmpty == false { + data += [UInt8(0x00)] // SegWit + flag |= 0x01 // SegWit + data += flag.bytes + } + // MARK: - Inputs + data += CompactSizeUInt(transaction.inputs?.count ?? 0).bytes + if let inputs = transaction.inputs { + for input in inputs { + data += input.outpoint.id.serialized(requiredLength: 32) + data += input.outpoint.index.bytes + data += CompactSizeUInt(input.scriptSignature?.count ?? 0).bytes + if let scriptSignature = input.scriptSignature { + data += scriptSignature + } + data += input.sequence.bytes + } + } + // MARK: - Outputs + data += CompactSizeUInt(transaction.outputs.count).bytes + for output in transaction.outputs { + var outputFlags = UInt64(0x00) + if output.value == Int64.max { + if output.rangeProof?.Vs.isEmpty == false { + outputFlags |= 0x01 // BLSCT + } + if output.tokenIdentifier != nil { + outputFlags |= 0x02 // Token + } + if outputFlags != 0x00 { + data += Int64.max.bytes + data += outputFlags.bytes + } else { + data += output.value.bytes + } + } + data += CompactSizeUInt(output.publicScriptKey?.count ?? 0).bytes + if let publicScriptKey = output.publicScriptKey { + data += publicScriptKey + } + if (outputFlags & 0x01) != 0 { + guard let rangeProof = output.rangeProof else { + preconditionFailure() + } + data += CompactSizeUInt(rangeProof.Vs.count).bytes + for V in rangeProof.Vs { + data += V + } + data += CompactSizeUInt(rangeProof.Ls.count).bytes + for L in rangeProof.Ls { + data += L + } + data += CompactSizeUInt(rangeProof.Rs.count).bytes + for R in rangeProof.Rs { + data += R + } + data += rangeProof.A + data += rangeProof.S + data += rangeProof.T1 + data += rangeProof.T2 + data += rangeProof.taux + data += rangeProof.mu + data += rangeProof.a + data += rangeProof.b + data += rangeProof.t + guard let publicBLSSpendKey = output.publicBLSSpendKey else { + throw TransactionSerializerOutputError.noPublicBLSSpendKey + } + data += publicBLSSpendKey + guard let publicBLSBlindKey = output.publicBLSBlindKey else { + throw TransactionSerializerOutputError.noPublicBLSBlindKey + } + data += publicBLSBlindKey + guard let publicBLSEphemeralKey = output.publicBLSEphemeralKey else { + throw TransactionSerializerOutputError.noPublicBLSEphemeralKey + } + data += publicBLSEphemeralKey + guard let viewTag = output.viewTag else { + throw TransactionSerializerOutputError.noViewTag + } + data += viewTag.bytes + } + if (outputFlags & 0x02) != 0 { + guard let tokenIdentifier = output.tokenIdentifier else { + preconditionFailure() + } + data += tokenIdentifier.id.serialized(requiredLength: 32) + data += tokenIdentifier.nftId.bytes + } + } + // MARK: - Witnesses + if (flag & 0x01) != 0 { + for witness in transaction.witnesses! { + data += CompactSizeUInt(witness.stack.count).bytes + for element in witness.stack { + data += CompactSizeUInt(element.count).bytes + data += element + } + } + } + // MARK: - LockTime + data += transaction.lockTime.bytes + // MARK: - Signature + if (transaction.version & 0x20) != 0 { + guard let signature = transaction.signature else { + throw TransactionSerializerError.noSignature + } + data += signature + } + return data + } +} diff --git a/Sources/Navcoin/TransactionSerializer/TransactionSerializerError.swift b/Sources/Navcoin/TransactionSerializer/TransactionSerializerError.swift new file mode 100644 index 0000000..5cd7364 --- /dev/null +++ b/Sources/Navcoin/TransactionSerializer/TransactionSerializerError.swift @@ -0,0 +1,12 @@ +enum TransactionSerializerError: Error { + case noSignature +} + +extension TransactionSerializerError: ErrorDescribing { + var description: String { + switch self { + case .noSignature: + return "No field `signature` in `Transaction` data" + } + } +} diff --git a/Sources/Navcoin/TransactionSerializer/TransactionSerializerOutputError.swift b/Sources/Navcoin/TransactionSerializer/TransactionSerializerOutputError.swift new file mode 100644 index 0000000..4dfaca1 --- /dev/null +++ b/Sources/Navcoin/TransactionSerializer/TransactionSerializerOutputError.swift @@ -0,0 +1,21 @@ +enum TransactionSerializerOutputError: Error { + case noPublicBLSSpendKey + case noPublicBLSBlindKey + case noPublicBLSEphemeralKey + case noViewTag +} + +extension TransactionSerializerOutputError: ErrorDescribing { + var description: String { + switch self { + case .noPublicBLSSpendKey: + return "No field `publicBLSSpendKey` in `TransactionOutput` data" + case .noPublicBLSBlindKey: + return "No field `publicBLSBlindKey` in `TransactionOutput` data" + case .noPublicBLSEphemeralKey: + return "No field `publicBLSEphemeralKey` in `TransactionOutput` data" + case .noViewTag: + return "No field `viewTag` in `TransactionOutput` data" + } + } +} diff --git a/Tests/NavcoinTests/BigUIntTests.swift b/Tests/NavcoinTests/BigUIntTests.swift new file mode 100644 index 0000000..7b60815 --- /dev/null +++ b/Tests/NavcoinTests/BigUIntTests.swift @@ -0,0 +1,26 @@ +import XCTest +import BigInt +@testable import Navcoin + +final class BigUIntTests: XCTestCase { + func testGiven1Byte_WhenSerialized_AndRequiredLength1_ThenLength1() { + let data = Data([1]) + let value = BigUInt(data).serialized(requiredLength: 1) + XCTAssertEqual(value.count, 1) + XCTAssertEqual(value.bytes, [1]) + } + + func testGiven1Byte_WhenSerialized_AndRequiredLength2_ThenLength2() { + let data = Data([1]) + let value = BigUInt(data).serialized(requiredLength: 2) + XCTAssertEqual(value.count, 2) + XCTAssertEqual(value.bytes, [0, 1]) + } + + func testGiven2Bytes_WhenSerialized_AndRequiredLength1_ThenLength2() { + let data = Data([1, 2]) + let value = BigUInt(data).serialized(requiredLength: 1) + XCTAssertEqual(value.count, 2) + XCTAssertEqual(value.bytes, [1, 2]) + } +} diff --git a/Tests/NavcoinTests/CompactSizeUIntTests.swift b/Tests/NavcoinTests/CompactSizeUIntTests.swift new file mode 100644 index 0000000..679315d --- /dev/null +++ b/Tests/NavcoinTests/CompactSizeUIntTests.swift @@ -0,0 +1,104 @@ +import XCTest +@testable import Navcoin + +final class CompactSizeUIntTests: XCTestCase { + func testGiven0Bytes_WhenInit_ThenNil() { + XCTAssertNil(CompactSizeUInt(data: Data())) + } + + func testGiven1Byte_WithByteInRangeFrom0To0xfc_WhenInit_ThenAssignByteToValue() { + let size1 = CompactSizeUInt(data: Data([0]))! + let size2 = CompactSizeUInt(data: Data([0xfc]))! + XCTAssertEqual(size1.value, 0) + XCTAssertEqual(size2.value, 0xfc) + } + + func testGiven3Bytes_WithFirstByte0xfd_WhenInit_ThenAssignLast2ToValue() { + let size1 = CompactSizeUInt(data: Data([253, 253, 0]))! + let size2 = CompactSizeUInt(data: Data([253, 255, 255]))! + XCTAssertEqual(size1.value, UInt(data: Data([253, 0]))) + XCTAssertEqual(size2.value, UInt(data: Data([255, 255]))) + } + + func testGivenLessThan3Bytes_WithFirstByte0xfd_WhenInit_ThenNil() { + XCTAssertNil(CompactSizeUInt(data: Data([253, 253]))) + } + + func testGiven5Bytes_WithFirstByte0xfe_WhenInit_ThenAssignLast4BytesToValue() { + let size1 = CompactSizeUInt(data: Data([254, 0, 0, 1, 0]))! + let size2 = CompactSizeUInt(data: Data([254, 255, 255, 255, 255]))! + XCTAssertEqual(size1.value, UInt(data: Data([0, 0, 1, 0]))) + XCTAssertEqual(size2.value, UInt(data: Data([255, 255, 255, 255]))) + } + + func testGivenLessThan5Bytes_WithFirstByte0xfe_WhenInit_ThenNil() { + XCTAssertNil(CompactSizeUInt(data: Data([254, 0, 0, 1]))) + } + + func testGiven9Bytes_Data_WithFirstByte0xff_ThenAssignLast8BytesToValue() { + let size1 = CompactSizeUInt(data: Data([255, 0, 0, 0, 0, 1, 0, 0, 0]))! + let size2 = CompactSizeUInt(data: Data([255, 255, 255, 255, 255, 255, 255, 255, 255]))! + XCTAssertEqual(size1.value, UInt(data: Data([0, 0, 0, 0, 1, 0, 0, 0]))) + XCTAssertEqual(size2.value, UInt(data: Data([255, 255, 255, 255, 255, 255, 255, 255]))) + } + + func testGivenLessThan9Bytes_WithFirstByte0xff_WhenInit_ThenNil() { + XCTAssertNil(CompactSizeUInt(data: Data([255, 0, 0, 0, 0, 1, 0, 0]))) + } + + func testGivenValue_InRangeFrom0To0xfc_WhenInit_AndGetBytes_ThenAssertEqual() { + let size1 = CompactSizeUInt(0) + let size2 = CompactSizeUInt(0xfc) + XCTAssertEqual(size1.bytes, [0]) + XCTAssertEqual(size2.bytes, [0xfc]) + } + + func testGivenValue_InRangeFrom0xfdTo0xffff_WhenInit_AndGetBytes_ThenAssertEqual() { + let size1 = CompactSizeUInt(0xfd) + let size2 = CompactSizeUInt(0xffff) + XCTAssertEqual(size1.bytes, [253, 253, 0]) + XCTAssertEqual(size2.bytes, [253, 255, 255]) + } + + func testGivenValue_InRangeFrom0x10000To0xffffffff_WhenInit_AndGetBytes_ThenAssertEqual() { + let size1 = CompactSizeUInt(0x10000) + let size2 = CompactSizeUInt(0xffffffff) + XCTAssertEqual(size1.bytes, [254, 0, 0, 1, 0]) + XCTAssertEqual(size2.bytes, [254, 255, 255, 255, 255]) + } + + func testGivenValue_InRangeFrom0x100000000To0xffffffffffffffff_WhenInit_AndGetBytes_ThenAssertEqual() { + let size1 = CompactSizeUInt(0x100000000) + let size2 = CompactSizeUInt(UInt(0xffffffffffffffff)) + XCTAssertEqual(size1.bytes, [255, 0, 0, 0, 0, 1, 0, 0, 0]) + XCTAssertEqual(size2.bytes, [255, 255, 255, 255, 255, 255, 255, 255, 255]) + } + + func testGivenValue_InRangeFrom0To0xfc_WhenInit_AndGetTotalByteCount_ThenEqual1() { + let size1 = CompactSizeUInt(0) + let size2 = CompactSizeUInt(0xfc) + XCTAssertEqual(size1.totalByteCount, 1) + XCTAssertEqual(size2.totalByteCount, 1) + } + + func testGivenValue_InRangeFrom0xfdTo0xffff_WhenInit_AndGetTotalByteCount_ThenEqual3() { + let size1 = CompactSizeUInt(0xfd) + let size2 = CompactSizeUInt(0xffff) + XCTAssertEqual(size1.totalByteCount, 3) + XCTAssertEqual(size2.totalByteCount, 3) + } + + func testGivenValue_InRangeFrom0x10000To0xffffffff_WhenInit_AndGetTotalByteCount_ThenEqual5() { + let size1 = CompactSizeUInt(0x10000) + let size2 = CompactSizeUInt(0xffffffff) + XCTAssertEqual(size1.totalByteCount, 5) + XCTAssertEqual(size2.totalByteCount, 5) + } + + func testGivenValue_InRangeFrom0x100000000To0xffffffffffffffff_WhenInit_AndGetTotalByteCount_ThenEqual9() { + let size1 = CompactSizeUInt(0x100000000) + let size2 = CompactSizeUInt(UInt(0xffffffffffffffff)) + XCTAssertEqual(size1.totalByteCount, 9) + XCTAssertEqual(size2.totalByteCount, 9) + } +} diff --git a/Tests/NavcoinTests/RawTransactions.swift b/Tests/NavcoinTests/RawTransactions.swift new file mode 100644 index 0000000..64d6aa0 --- /dev/null +++ b/Tests/NavcoinTests/RawTransactions.swift @@ -0,0 +1,10 @@ +struct RawTransactions { + static let hexEncodedNonSegWit = """ + 2100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff01ffffffffffffff7f03000000000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac01b8363724d6bf8ec759203e6ece2b6ef42e28fb2a4f0334cef9bd46aa4d6b17fbfad841d7e4ca2a9b70efef90cd6bfe9b068006fedc482aa77be0c735e62684f3468923a8d67ca7b9a37ed14959f0b445bdc9d2b9106eb8f487e4e3da643621c846b4279f50d36baebe2b157c6fe53118d8b8f0dd6ca452edabe9e9514a074eabafe141dc33db7fc70d9734171f1224938a810c505c9af6ba090e5ce694b92b13e297e61ed0cb547887bd610b46aa6eb0f7fb390d382b8276254358bd6ba5d6080aab4e747b61cee06b43ff297713c6438064051b7b8311ead621abcbdc5e64c09a01bc6d490d7665a4a336100f062aebcc943ef11a8e1498e5bd28e551616d1a8a12b21fe4188759f0b250f8bc848ed0972c32efa4cb1e0eae35dda287761761ff8a2813611092f99dda691631bf671071eb34cd2dc1eef2e396d06bceaa01522b1c0fdfdc6986ea42e8618fa836e1b02806b9b7e83e46efd71c738d490104a376cc7d90e7a6ea4a53af927107a5a6deadbb921ddd035ffc03d0cb5b3ec5ad6c3271b80d73b7b2fd19b6bee5fbac27a377748cc1014a119d328cef72e2aca7ee6268d8b707baba86659eac60e5019267c8db91a1dde859b5350263e2e3b7ea282e3eb7ce2da20b85896e1a8cfba2e2bcf6bf18c12edf27a04bc498756a7b4db6072da8f14979a8727786a6eca97427a32fc1a154abacbef24408f5d0054a0eec746d58a64a6ee7f1f4528b1cf2e49ada6831ad5a1d75eafc185d5f2e04ea3226cd7e51a337c7726c01c50ec7cc1abcb826dbfabda285e37565f0c4839fdf05e6ecac8093b9f65fd89ecbc8e848ce8f4200af8a563213eabb398dcb77ac8916dad7499eef1fb674bda30383cac8c67e235e6eb18e1094aa05f973dd4bb1804cfee5c913f1c7f512cd303e3163d1aeb3173585e088a24ca850b4e042ad6b93dbe0e15cb048be8e286bd45f2d45a837245b670197a11f0de9ee2771cff6e43f620496a94282f4b70b53f540280f81dc51b10ca682bba46977245baeeb4b8a3aa532251d54aeaeffdf59355736291ff4d4db6e1778ef34361a1b0a1ed2f43f1d21a64d348ca39861334bfcc1296e2ccaf2dcb2a3710663cb8959f41dff8641d33f6240b2eeb61f20e375099460d725df474a0554427f34638862247c51b289563ccf141341bfbea14e10e2a01a748323d4f96a206a53282a43b2054dd168d00d81f5e5349b2b039b261e320149e7de8c6884e86d5ca1fee2a98af40879b6356b5c6cee1ac2ef702e4c22ddf7810505d66e1f08e735dfed00845164d67a94670cf4ca4fe1f16ea1076a2e8d3b6f4a5b38086644771bec6ed42c96dba2eb778fde0608caa78b89845983334b532bec99bd96a2059c958dca246ff9ddf516fec76a47f05686cc15a9668bdd687052abbee581e26083b5831411f0c110b019f8a600ed431f00b79bbc131bf99c5ac77573b56b98b656b5fcef8479784dfdcd2e8d6ae253af53b651109ab069a0e9e59bb75774a2145d978bcae0561566981d2c72e969261849db2fb880ce6978e5d3833d769b7035dc387ffe865e238649eeadaad1b427a42800007b00000000000000000000000000000000000000000000000000000000000000ffffffffffffffff00000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + """ + static let hexEncodedSegWit = """ + 21000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01ffffffffffffff7f03000000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac01807e0bd44065a46334e43abfdf67285171ec01f41e717c1e8466815693a43ca91abe809480f3202ddb53bb72532edc95069114c12949a851b57ed8304d4b72d85708e6bc7695851d858cfc821473d5bc2c023ee327dace06beed35c4ab87e427818e7615ccf5a5d9f3226c7e15a267632327979a3d6e9f77c9350af4f586984d9625e059120ff5cda95144e0992e4f9de59866ababf42b07614ab14a5759540d580f6094d6ce6a1783a6a690224ff8619f9048c5cb4b16b368b1671edb801e3c09828b9758ff3645175af97242e8ccea0f6d9600de6940e1d7a963b3e7c9798f1000448829f5835b4b028f9732608f1dcb894ee5723771331441f29a3c89ac9fa6c23ae24f9c9eccdf0e1992ae1876d9ded82bf2c5ebc4e66bfa89dc426d789f2395886c2ecbed1c8da8bcf0161bf1bdc3db7b5cd474c109392bec6957467cc76aa6da60922dce0f8430bfd791940cb4cd068c93ea263edb9e8738e4badb3fef1e38879194fe5a640d64d5b557739e79e3261c1c0dc61ed317220d24243f417cfdc0b923b44ae594fee35af4732e3d8147b15de6d56fa5f83c035981da08f0548815ad771e47012450b3acc397fa7c58e0f8b073bafef02860f2dabcd8c314d8c7479f86e585967a382c2bc597ab9078693f4b8f82677976997e7ddb0fd99c794750aab2b2ed9db2e2cc34d8fb9b835f37a09ab3ce0f28c37c95efc8e212ddd31d28d179ff87f16eb6f50907a9581f8263848fcfad58dec130c51cfc9a68ee45491a4c0fbf0823e7948f549360170abedcf9f650167ce3dbe20117bd4a2958c6bc3c81bb70cee61f1aaa23c71d8c67829340cdcc7f9972ce6eeed2dfb9c966512b1b0a25a1c7ab7051efda427df76ab82874856971b832e86b1a0e75d2b7ff5187ab95a9362c8ccaf29efd92bac81bd99d600f3db804c529684d26400e70ae4c6a748436541fbf6b223d556e648eb082145d0dac8a7140357d15aaa6db199ce1c98c319a8a47737cac132e5aa11a4db765ae960f0c2157636ce4952236460cb87c52f87e54e96416ffc839b4fe60742252f35a24d0169387f0823ab8792741d702acb8c81cc2bc5e56cb9f8dae3f276f2af1dd560e3b8ef933dc1ac4766eeb305a31094c1ca76d1e2665eea843b3690f29b239bd232e1d2b77d7a50cfaa48e05e307eb3d653aeda5ee9154f2bf0994aabef96c86805726ad7ed8095ed859433632c7c17e992dd2dd04ebbb6d0724a6d0e98e06456ef656b686e467240be4c6b5bf464b60bcd6366d5c073a83543688c805114c811716a56235f5cf70f7e82c1e7cc3374297f597205f50036630120c877ac348b2764041401766b7bd48227bfca86b8f68efd2e3567b90ed3d5d13d5a1bae8b754d949b4f8bfa5404bb1a45cd3c29c9211f1b12080fc6ccfcf8fc28dc04a05871fa3cf1629ef38446f564728a5cad08109cb270a0008c576f6422d2e7fb75e96aa40c41e8cc7e7ec76238e8b28f73184d5f4b54a549ab0d832c6f50703951e8bf1655d1107cd5f90032e4a172c82e5e23b0787aff49a5321d38b371c88573ef865a1f46808eddc6854b5eeaf24291700007b00000000000000000000000000000000000000000000000000000000000000ffffffffffffffff02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + """ + + private init() {} +} diff --git a/Tests/NavcoinTests/TransactionDeserializerTests.swift b/Tests/NavcoinTests/TransactionDeserializerTests.swift new file mode 100644 index 0000000..84bf553 --- /dev/null +++ b/Tests/NavcoinTests/TransactionDeserializerTests.swift @@ -0,0 +1,246 @@ +import XCTest +import BigInt +@testable import Navcoin + +final class TransactionDeserializerTests: XCTestCase { + private func sut() -> TransactionDeserializer { + .init() + } + + func testGivenHexEncodedNonSegwitTransaction_WhenDeserialize_ThenNotNil() { + let data = Data(hex: RawTransactions.hexEncodedNonSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction) + } + + func testGivenHexEncodedNonSegwitTransaction_WhenDeserialize_ThenWitnessesNil() { + let data = Data(hex: RawTransactions.hexEncodedNonSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenDeserialize_ThenNotNil() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction) + } + + func testGivenHexEncodedSegwitTransaction_WhenDeserialize_ThenWitnessesNotNil() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenVersionValid_AndDeserialize_ThenWitnessesNotNil() { + let hex = "21000000" + RawTransactions.hexEncodedSegWit.dropFirst(8) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenVersionNotValid_AndDeserialize_ThenWitnessesNil() { + let hex = "FFFFFFFF" + RawTransactions.hexEncodedSegWit.dropFirst(8) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenMarkerValid_AndDeserialize_ThenWitnessesNotNil() { + let hex = "2100000000" + RawTransactions.hexEncodedSegWit.dropFirst(10) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenMarkerNotValid_AndDeserialize_ThenWitnessesNil() { + let hex = "2100000001" + RawTransactions.hexEncodedSegWit.dropFirst(10) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenFlagsValid_AndDeserialize_ThenWitnessesNotNil() { + let hex = "210000000001" + RawTransactions.hexEncodedSegWit.dropFirst(12) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.witnesses) + } + + func testGivenHexEncodedSegwitTransaction_WhenFlagsNotValid_AndDeserialize_ThenWitnessesNil() { + let hex = "210000000000" + RawTransactions.hexEncodedSegWit.dropFirst(12) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.witnesses) + } + + func testGivenHexEncodedTransaction_WhenVersionValid_AndDeserialize_ThenSignatureNotNil() { + let hex = "21000000" + RawTransactions.hexEncodedSegWit.dropFirst(8) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertEqual(transaction?.version, 33) + XCTAssertNotNil(transaction?.signature) + } + + func testGivenHexEncodedTransaction_WhenVersionNotValid_AndDeserialize_ThenSignatureNil() { + let hex = "1F000000" + RawTransactions.hexEncodedSegWit.dropFirst(8) + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertEqual(transaction?.version, 31) + XCTAssertNil(transaction?.signature) + } + + func testGivenHexEncodedTransaction_WhenOutputValueInt64Max_AndDeserialize_ThenValueInt64Max() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data) + XCTAssertEqual(transaction?.outputs[0].value, Int64.max) + } + + func testGivenHexEncodedTransaction_WhenOutputValueNotInt64Max_AndDeserialize_ThenValue0() { + let hex = RawTransactions.hexEncodedSegWit.replacingOccurrences(of: "ffffffffffffff7f", with: "0000000000000000") + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertEqual(transaction?.outputs[0].value, 0) + } + + func testGivenHexEncodedTransaction_WhenOutputValueInt64Max_AndOutputFlagValid1_AndDeserialize_ThenBLSCTFieldsNotNil() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.outputs[0].rangeProof) + XCTAssertNotNil(transaction?.outputs[0].publicBLSSpendKey) + XCTAssertNotNil(transaction?.outputs[0].publicBLSBlindKey) + XCTAssertNotNil(transaction?.outputs[0].publicBLSEphemeralKey) + XCTAssertNotNil(transaction?.outputs[0].viewTag) + } + + func testGivenHexEncodedTransaction_WhenOutputValueInt64Max_AndOutputFlagNotValid1_AndDeserialize_ThenBLSCTFieldsNil() { + let hex = RawTransactions.hexEncodedSegWit.replacingOccurrences(of: "0300000000000000", with: "0000000000000000") + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.outputs[0].rangeProof) + XCTAssertNil(transaction?.outputs[0].publicBLSSpendKey) + XCTAssertNil(transaction?.outputs[0].publicBLSBlindKey) + XCTAssertNil(transaction?.outputs[0].publicBLSEphemeralKey) + XCTAssertNil(transaction?.outputs[0].viewTag) + } + + func testGivenHexEncodedTransaction_WhenOutputValueNotInt64Max_AndDeserialize_ThenBLSCTFieldsNil() { + let hex = RawTransactions.hexEncodedSegWit.replacingOccurrences(of: "ffffffffffffff7f", with: "0000000000000000") + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.outputs[0].rangeProof) + XCTAssertNil(transaction?.outputs[0].publicBLSSpendKey) + XCTAssertNil(transaction?.outputs[0].publicBLSBlindKey) + XCTAssertNil(transaction?.outputs[0].publicBLSEphemeralKey) + XCTAssertNil(transaction?.outputs[0].viewTag) + } + + func testGivenHexEncodedTransaction_WhenOutputValueInt64Max_AndOutputFlagValid2_AndDeserialize_ThenTokenIdentifierNotNil() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data) + XCTAssertNotNil(transaction?.outputs[0].tokenIdentifier) + } + + func testGivenHexEncodedTransaction_WhenOutputValueInt64Max_AndOutputFlagNotValid2_AndDeserialize_ThenTokenIdentifierNil() { + let hex = RawTransactions.hexEncodedSegWit.replacingOccurrences(of: "0300000000000000", with: "0000000000000000") + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.outputs[0].tokenIdentifier) + } + + func testGivenHexEncodedTransaction_WhenOutputValueNotInt64Max_AndDeserialize_ThenTokenIdentifierNil() { + let hex = RawTransactions.hexEncodedSegWit.replacingOccurrences(of: "ffffffffffffff7f", with: "0000000000000000") + let data = Data(hex: hex) + let transaction = sut().transaction(data: data) + XCTAssertNil(transaction?.outputs[0].tokenIdentifier) + } + + func testGivenHexEncodedTransaction_WhenDeserialize_ThenAssertEqual() { + let data = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = sut().transaction(data: data)! + let version = "21000000" + let outpointId = "0000000000000000000000000000000000000000000000000000000000000100" + let outpointIndex = "00000000" + let scriptSignature = "1600144c9c3dfac4207d5d8cb89df5722cb3d712385e3f" + let sequence = "ffffffff" + let value = "ffffffffffffff7f" + let publicScriptKey = "76a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac" + let rangeProof_V1 = "807e0bd44065a46334e43abfdf67285171ec01f41e717c1e8466815693a43ca91abe809480f3202ddb53bb72532edc95" + let rangeProof_L1 = "9114c12949a851b57ed8304d4b72d85708e6bc7695851d858cfc821473d5bc2c023ee327dace06beed35c4ab87e42781" + let rangeProof_L2 = "8e7615ccf5a5d9f3226c7e15a267632327979a3d6e9f77c9350af4f586984d9625e059120ff5cda95144e0992e4f9de5" + let rangeProof_L3 = "9866ababf42b07614ab14a5759540d580f6094d6ce6a1783a6a690224ff8619f9048c5cb4b16b368b1671edb801e3c09" + let rangeProof_L4 = "828b9758ff3645175af97242e8ccea0f6d9600de6940e1d7a963b3e7c9798f1000448829f5835b4b028f9732608f1dcb" + let rangeProof_L5 = "894ee5723771331441f29a3c89ac9fa6c23ae24f9c9eccdf0e1992ae1876d9ded82bf2c5ebc4e66bfa89dc426d789f23" + let rangeProof_L6 = "95886c2ecbed1c8da8bcf0161bf1bdc3db7b5cd474c109392bec6957467cc76aa6da60922dce0f8430bfd791940cb4cd" + let rangeProof_R1 = "8c93ea263edb9e8738e4badb3fef1e38879194fe5a640d64d5b557739e79e3261c1c0dc61ed317220d24243f417cfdc0" + let rangeProof_R2 = "b923b44ae594fee35af4732e3d8147b15de6d56fa5f83c035981da08f0548815ad771e47012450b3acc397fa7c58e0f8" + let rangeProof_R3 = "b073bafef02860f2dabcd8c314d8c7479f86e585967a382c2bc597ab9078693f4b8f82677976997e7ddb0fd99c794750" + let rangeProof_R4 = "aab2b2ed9db2e2cc34d8fb9b835f37a09ab3ce0f28c37c95efc8e212ddd31d28d179ff87f16eb6f50907a9581f826384" + let rangeProof_R5 = "8fcfad58dec130c51cfc9a68ee45491a4c0fbf0823e7948f549360170abedcf9f650167ce3dbe20117bd4a2958c6bc3c" + let rangeProof_R6 = "81bb70cee61f1aaa23c71d8c67829340cdcc7f9972ce6eeed2dfb9c966512b1b0a25a1c7ab7051efda427df76ab82874" + let rangeProof_A = "856971b832e86b1a0e75d2b7ff5187ab95a9362c8ccaf29efd92bac81bd99d600f3db804c529684d26400e70ae4c6a74" + let rangeProof_S = "8436541fbf6b223d556e648eb082145d0dac8a7140357d15aaa6db199ce1c98c319a8a47737cac132e5aa11a4db765ae" + let rangeProof_T1 = "960f0c2157636ce4952236460cb87c52f87e54e96416ffc839b4fe60742252f35a24d0169387f0823ab8792741d702ac" + let rangeProof_T2 = "b8c81cc2bc5e56cb9f8dae3f276f2af1dd560e3b8ef933dc1ac4766eeb305a31094c1ca76d1e2665eea843b3690f29b2" + let rangeProof_taux = "39bd232e1d2b77d7a50cfaa48e05e307eb3d653aeda5ee9154f2bf0994aabef9" + let rangeProof_mu = "6c86805726ad7ed8095ed859433632c7c17e992dd2dd04ebbb6d0724a6d0e98e" + let rangeProof_a = "06456ef656b686e467240be4c6b5bf464b60bcd6366d5c073a83543688c80511" + let rangeProof_b = "4c811716a56235f5cf70f7e82c1e7cc3374297f597205f50036630120c877ac3" + let rangeProof_t = "48b2764041401766b7bd48227bfca86b8f68efd2e3567b90ed3d5d13d5a1bae8" + let publicBLSSpendKey = "b754d949b4f8bfa5404bb1a45cd3c29c9211f1b12080fc6ccfcf8fc28dc04a05871fa3cf1629ef38446f564728a5cad0" + let publicBLSBlindKey = "8109cb270a0008c576f6422d2e7fb75e96aa40c41e8cc7e7ec76238e8b28f73184d5f4b54a549ab0d832c6f50703951e" + let publicBLSEphemeralKey = "8bf1655d1107cd5f90032e4a172c82e5e23b0787aff49a5321d38b371c88573ef865a1f46808eddc6854b5eeaf242917" + let viewTag = "0000" + let tokenIdentifierId = "000000000000000000000000000000000000000000000000000000000000007b" + let tokenIdentifierNftId = "ffffffffffffffff" + let witnessSignature1 = """ + 3045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed01 + """ + let witnessSignature2 = "03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71" + let lockTime = "00000000" + let signature = """ + c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + """ + XCTAssertEqual(transaction.version.bytes.toHexString(), version) + XCTAssertEqual(transaction.inputs?.count, 1) + XCTAssertEqual(Data(transaction.inputs![0].outpoint.id.serialized(requiredLength: 32).reversed()).toHexString(), outpointId) + XCTAssertEqual(transaction.inputs![0].outpoint.index.bytes.toHexString(), outpointIndex) + XCTAssertEqual(transaction.inputs![0].scriptSignature!.toHexString(), scriptSignature) + XCTAssertEqual(transaction.inputs![0].sequence.bytes.toHexString(), sequence) + XCTAssertEqual(transaction.outputs.count, 1) + XCTAssertEqual(transaction.outputs[0].value.bytes.toHexString(), value) + XCTAssertEqual(transaction.outputs[0].publicScriptKey!.toHexString(), publicScriptKey) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Vs[0].toHexString(), rangeProof_V1) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[0].toHexString(), rangeProof_L1) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[1].toHexString(), rangeProof_L2) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[2].toHexString(), rangeProof_L3) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[3].toHexString(), rangeProof_L4) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[4].toHexString(), rangeProof_L5) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Ls[5].toHexString(), rangeProof_L6) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[0].toHexString(), rangeProof_R1) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[1].toHexString(), rangeProof_R2) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[2].toHexString(), rangeProof_R3) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[3].toHexString(), rangeProof_R4) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[4].toHexString(), rangeProof_R5) + XCTAssertEqual(transaction.outputs[0].rangeProof!.Rs[5].toHexString(), rangeProof_R6) + XCTAssertEqual(transaction.outputs[0].rangeProof!.A.toHexString(), rangeProof_A) + XCTAssertEqual(transaction.outputs[0].rangeProof!.S.toHexString(), rangeProof_S) + XCTAssertEqual(transaction.outputs[0].rangeProof!.T1.toHexString(), rangeProof_T1) + XCTAssertEqual(transaction.outputs[0].rangeProof!.T2.toHexString(), rangeProof_T2) + XCTAssertEqual(transaction.outputs[0].rangeProof!.taux.toHexString(), rangeProof_taux) + XCTAssertEqual(transaction.outputs[0].rangeProof!.mu.toHexString(), rangeProof_mu) + XCTAssertEqual(transaction.outputs[0].rangeProof!.a.toHexString(), rangeProof_a) + XCTAssertEqual(transaction.outputs[0].rangeProof!.b.toHexString(), rangeProof_b) + XCTAssertEqual(transaction.outputs[0].rangeProof!.t.toHexString(), rangeProof_t) + XCTAssertEqual(transaction.outputs[0].publicBLSSpendKey!.toHexString(), publicBLSSpendKey) + XCTAssertEqual(transaction.outputs[0].publicBLSBlindKey!.toHexString(), publicBLSBlindKey) + XCTAssertEqual(transaction.outputs[0].publicBLSEphemeralKey!.toHexString(), publicBLSEphemeralKey) + XCTAssertEqual(transaction.outputs[0].viewTag!.bytes.toHexString(), viewTag) + XCTAssertEqual(Data(transaction.outputs[0].tokenIdentifier!.id.serialized(requiredLength: 32).reversed()).toHexString(), tokenIdentifierId) + XCTAssertEqual(transaction.outputs[0].tokenIdentifier!.nftId.bytes.toHexString(), tokenIdentifierNftId) + XCTAssertEqual(transaction.witnesses?.count, 1) + XCTAssertEqual(transaction.witnesses![0].stack[0].toHexString(), witnessSignature1) + XCTAssertEqual(transaction.witnesses![0].stack[1].toHexString(), witnessSignature2) + XCTAssertEqual(transaction.lockTime.bytes.toHexString(), lockTime) + XCTAssertEqual(transaction.signature?.toHexString(), signature) + } +} diff --git a/Tests/NavcoinTests/TransactionSerializerTests.swift b/Tests/NavcoinTests/TransactionSerializerTests.swift new file mode 100644 index 0000000..2349098 --- /dev/null +++ b/Tests/NavcoinTests/TransactionSerializerTests.swift @@ -0,0 +1,136 @@ +import XCTest +@testable import Navcoin + +final class TransactionSerializerTests: XCTestCase { + private let deserializer = TransactionDeserializer() + + private func sut() -> TransactionSerializer { + .init() + } + + func testGivenNonSegWitTransaction_WhenSerialize_ThenAssertEqual() { + let serializedData = Data(hex: RawTransactions.hexEncodedNonSegWit) + let transaction = deserializer.transaction(data: serializedData)! + let serializedData2 = sut().data(transaction: transaction) + XCTAssertEqual(serializedData2, serializedData) + } + + func testGivenSegWitTransaction_WhenSerialize_ThenAssertEqual() { + let serializedData = Data(hex: RawTransactions.hexEncodedSegWit) + let transaction = deserializer.transaction(data: serializedData)! + let serializedData2 = sut().data(transaction: transaction) + XCTAssertEqual(serializedData2, serializedData) + } + + func testGivenVersionValid_AndSignatureNil_WhenSerializeTransaction_ThenThrowError() { + let transaction = self.transaction(version: 32, signature: nil) + XCTAssertThrowsError(try sut().dataThrows(transaction: transaction)) { + XCTAssertEqual($0 as? TransactionSerializerError, .noSignature) + } + } + + func testGivenOutput_AndValueInt64Max_AndPublicBLSSpendKeyNil_WhenSerializeTransaction_ThenThrowError() { + let output = transactionOutput(value: Int64.max, rangeProof: mockRangeProof(), publicBLSSpendKey: nil) + let transaction = self.transaction(outputs: [output]) + XCTAssertThrowsError(try sut().dataThrows(transaction: transaction)) { + XCTAssertEqual($0 as? TransactionSerializerOutputError, .noPublicBLSSpendKey) + } + } + + func testGivenOutput_AndValueInt64Max_AndPublicBLSBlindKeyNil_WhenSerializeTransaction_ThenThrowError() { + let output = transactionOutput(value: Int64.max, rangeProof: mockRangeProof(), publicBLSSpendKey: Data(), publicBLSBlindKey: nil) + let transaction = self.transaction(outputs: [output]) + XCTAssertThrowsError(try sut().dataThrows(transaction: transaction)) { + XCTAssertEqual($0 as? TransactionSerializerOutputError, .noPublicBLSBlindKey) + } + } + + func testGivenOutput_AndValueInt64Max_AndPublicBLSEphemeralKeyNil_WhenSerializeTransaction_ThenThrowError() { + let output = transactionOutput( + value: Int64.max, + rangeProof: mockRangeProof(), + publicBLSSpendKey: Data(), + publicBLSBlindKey: Data(), + publicBLSEphemeralKey: nil + ) + let transaction = self.transaction(outputs: [output]) + XCTAssertThrowsError(try sut().dataThrows(transaction: transaction)) { + XCTAssertEqual($0 as? TransactionSerializerOutputError, .noPublicBLSEphemeralKey) + } + } + + func testGivenOutput_AndValueInt64Max_AndViewTagNil_WhenSerializeTransaction_ThenThrowError() { + let output = transactionOutput( + value: Int64.max, + rangeProof: mockRangeProof(), + publicBLSSpendKey: Data(), + publicBLSBlindKey: Data(), + publicBLSEphemeralKey: Data(), + viewTag: nil + ) + let transaction = self.transaction(outputs: [output]) + XCTAssertThrowsError(try sut().dataThrows(transaction: transaction)) { + XCTAssertEqual($0 as? TransactionSerializerOutputError, .noViewTag) + } + } +} + +fileprivate extension TransactionSerializerTests { + func transaction( + version: Int32 = 0, + inputs: [TransactionInput]? = nil, + outputs: [TransactionOutput] = [], + witnesses: [TransactionWitness]? = nil, + signature: Data? = nil, + lockTime: UInt32 = 0 + ) -> Transaction { + .init( + version: version, + inputs: inputs, + outputs: outputs, + witnesses: witnesses, + signature: signature, + lockTime: lockTime + ) + } + + func transactionOutput( + value: Int64 = 0, + publicScriptKey: Data? = nil, + rangeProof: RangeProof? = nil, + publicBLSSpendKey: Data? = nil, + publicBLSBlindKey: Data? = nil, + publicBLSEphemeralKey: Data? = nil, + viewTag: UInt16? = nil, + tokenIdentifier: TokenIdentifier? = nil + ) -> TransactionOutput { + .init( + value: value, + publicScriptKey: publicScriptKey, + rangeProof: rangeProof, + publicBLSSpendKey: publicBLSSpendKey, + publicBLSBlindKey: publicBLSBlindKey, + publicBLSEphemeralKey: publicBLSEphemeralKey, + viewTag: viewTag, + tokenIdentifier: tokenIdentifier + ) + } + + func mockRangeProof() -> RangeProof { + let data = Data() + return .init( + Vs: [data], + Ls: [data], + Rs: [data], + A: data, + S: data, + T1: data, + T2: data, + taux: data, + mu: data, + a: data, + b: data, + t: data + ) + } +}