diff --git a/.travis.yml b/.travis.yml index 20229f0e..5db0a059 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ os: osx osx_image: xcode13.4 xcode_project: ./Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj xcode_scheme: ZcashLightClientSample -xcode_destination: platform=iOS Simulator,OS=15.2,name=iPhone 8 +xcode_destination: platform=iOS Simulator,OS=15.5,name=iPhone 8 addons: homebrew: packages: diff --git a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift index ea323a3f..adf6f8fd 100644 --- a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift +++ b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift @@ -7,7 +7,8 @@ import Foundation import SQLite -struct PendingTransaction: PendingTransactionEntity, Codable { +struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable { + enum CodingKeys: String, CodingKey { case toAddress = "to_address" case accountIndex = "account_index" @@ -57,7 +58,7 @@ struct PendingTransaction: PendingTransactionEntity, Codable { raw: entity.raw, id: entity.id, value: entity.value, - memo: entity.memo, + memo: entity.memo == nil ? Data(MemoBytes.empty().bytes) : entity.memo, rawTransactionId: entity.raw ) } @@ -144,8 +145,7 @@ struct PendingTransaction: PendingTransactionEntity, Codable { } extension PendingTransaction { - // TODO: Handle Memo - init(value: Zatoshi, toAddress: String, memo: String?, account index: Int) { + init(value: Zatoshi, toAddress: String, memo: MemoBytes, account index: Int) { self = PendingTransaction( toAddress: toAddress, accountIndex: index, @@ -160,7 +160,7 @@ extension PendingTransaction { raw: nil, id: nil, value: value, - memo: memo?.encodeAsZcashTransactionMemo(), + memo: Data(memo.bytes), rawTransactionId: nil ) } diff --git a/Sources/ZcashLightClientKit/Model/Memo.swift b/Sources/ZcashLightClientKit/Model/Memo.swift index 3db94f4e..ba646562 100644 --- a/Sources/ZcashLightClientKit/Model/Memo.swift +++ b/Sources/ZcashLightClientKit/Model/Memo.swift @@ -14,12 +14,12 @@ public enum Memo: Equatable { case arbitrary([UInt8]) /// Parses the given bytes as in ZIP-302 - public init?(bytes: [UInt8]) throws { + public init(bytes: [UInt8]) throws { self = try MemoBytes(bytes: bytes).intoMemo() } /// Converts these memo bytes into a ZIP-302 Memo - public init?(memoBytes: MemoBytes) throws { + public init(memoBytes: MemoBytes) throws { self = try memoBytes.intoMemo() } @@ -27,7 +27,7 @@ public enum Memo: Equatable { /// - Throws: /// - `MemoBytes.Errors.tooLong(length)` if the UTF-8 length /// of this string is greater than `MemoBytes.capacity` (512 bytes) - public init?(string: String) throws { + public init(string: String) throws { self = .text(try MemoText(String(string.utf8))) } } @@ -60,12 +60,14 @@ public struct MemoText: Equatable { public private(set) var string: String init(_ string: String) throws { - guard string.utf8.count <= MemoBytes.capacity else { - throw MemoBytes.Errors.tooLong(string.utf8.count) + let trimmedString = String(string.reversed().drop(while: { $0 == "\u{0}"}).reversed()) + + guard trimmedString.count == string.count else { + throw MemoBytes.Errors.endsWithNullBytes } - guard !string.containsCStringNullBytesBeforeStringEnding() else { - throw MemoBytes.Errors.invalidUTF8 + guard string.utf8.count <= MemoBytes.capacity else { + throw MemoBytes.Errors.tooLong(string.utf8.count) } self.string = string @@ -74,7 +76,11 @@ public struct MemoText: Equatable { public struct MemoBytes: Equatable { public enum Errors: Error { + /// Invalid UTF-8 Bytes where detected when attempting to create a Text Memo case invalidUTF8 + /// Trailing null-bytes were found when attempting to create a Text memo + case endsWithNullBytes + /// the resulting bytes provided are too long to be stored as a Memo in any of its forms. case tooLong(Int) } @@ -100,6 +106,18 @@ public struct MemoBytes: Equatable { self.bytes = rawBytes } + init(contiguousBytes: ContiguousArray) throws { + guard contiguousBytes.capacity <= Self.capacity else { throw Errors.tooLong(contiguousBytes.capacity) } + + var rawBytes = [UInt8](repeating: 0x0, count: Self.capacity) + + _ = contiguousBytes.withUnsafeBufferPointer { ptr in + memmove(&rawBytes[0], ptr.baseAddress, ptr.count) + } + + self.bytes = rawBytes + } + public static func empty() -> Self { try! Self(bytes: .emptyMemoBytes) } @@ -108,6 +126,8 @@ public struct MemoBytes: Equatable { extension MemoBytes.Errors: LocalizedError { var localizedDescription: String { switch self { + case .endsWithNullBytes: + return "MemoBytes.Errors.endsWithNullBytes: The UTF-8 bytes provided have trailing null-bytes." case .invalidUTF8: return "MemoBytes.Errors.invalidUTF8: Invalid UTF-8 byte found on memo bytes" case .tooLong(let length): @@ -161,12 +181,7 @@ public extension MemoBytes { extension MemoBytes { /// Returns raw bytes, excluding null padding func unpaddedRawBytes() -> [UInt8] { - guard let firstNullByte = self.bytes.enumerated() - .reversed() - .first(where: { $0.1 != 0 }) - .map({ $0.0 + 1 }) else { return [UInt8](bytes[0 ... 1]) } - - return [UInt8](bytes[0 ... firstNullByte]) + self.bytes.unpaddedRawBytes() } } @@ -176,6 +191,17 @@ extension Array where Element == UInt8 { emptyMemo[0] = 0xF6 return emptyMemo } + + func unpaddedRawBytes() -> [UInt8] { + guard let lastNullByte = self.enumerated() + .reversed() + .first(where: { $0.1 != 0 }) + .map({ $0.0 + 1 }) else { + return [UInt8](self[0 ..< 1]) + } + + return [UInt8](self[0 ..< lastNullByte]) + } } extension String { @@ -187,3 +213,25 @@ extension String { self = s } } + +extension Optional where WrappedType == String { + func intoMemo() throws -> Memo { + switch self { + case .none: + return .empty + case .some(let string): + return try Memo(string: string) + } + } +} + +extension Optional where WrappedType == Data { + func intoMemoBytes() throws -> MemoBytes { + switch self { + case .none: + return .empty() + case .some(let data): + return try .init(bytes: data.bytes) + } + } +} diff --git a/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1686871.json b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1686871.json new file mode 100644 index 00000000..c23317ad --- /dev/null +++ b/Sources/ZcashLightClientKit/Resources/checkpoints/mainnet/1686871.json @@ -0,0 +1,7 @@ +{ + "network": "main", + "height": "1686871", + "hash": "0000000000443d2be0b66f0f2c03784cf267e50935562a23c10d9d3841343cd7", + "time": 1654001768, + "saplingTree": "01802ef6c171eca37050fbc6cdf8d9712a8c5e4650af1a31300c143e063a680e2100140000000001e684c306416b750c7b4883e145922c129e5919a658b02e6146d2da8d17a3ff42000001ae61a98ec05d976a41e46f0b7ae8dee5bda5ab490eea7b701153511a15be42590198c748a5e9516711db607e282f08013ba00b3e77bc66ec28702ca17893a5154b000139113a1e0f54d93c6180f2df7d16afbf7c320f6c7665cb10e1fc309878b0d716019a0214bcb1fd7a70e53166101992147512f3debb0fdce45ac625fffd64771010000134136b9f1f00c2e9d16dc7358ef920862511c8fc42fb7074cbc9016d8d4e8b4c015eddc191a81221b7900bbdcd8610e5df595e3cdc7fd5b3432e3825206ae35b05017eda713cd733ccc555123788692a1876f9ca292b0aa2ddf3f45ed2b47f027340000000015ec9e9b1295908beed437df4126032ca57ada8e3ebb67067cd22a73c79a84009" +} diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift index 4037a63f..da50fdf1 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackend.swift @@ -359,25 +359,74 @@ class ZcashRustBackend: ZcashRustBackendWelding { return WalletBalance(verified: Zatoshi(verified), total: Zatoshi(total)) } + @available(*, deprecated, message: "This function will be deprecated soon. Use `getReceivedMemo(dbData:idNote:networkType)` instead") static func getReceivedMemoAsUTF8(dbData: URL, idNote: Int64, networkType: NetworkType) -> String? { let dbData = dbData.osStr() guard let memoCStr = zcashlc_get_received_memo_as_utf8(dbData.0, dbData.1, idNote, networkType.networkId) else { return nil } - let memo = String(validatingUTF8: memoCStr) - zcashlc_string_free(memoCStr) - return memo + defer { + zcashlc_string_free(memoCStr) + } + + return String(validatingUTF8: memoCStr) } - - static func getSentMemoAsUTF8(dbData: URL, idNote: Int64, networkType: NetworkType) -> String? { + + @available(*, deprecated, message: "This function will be deprecated soon. Use `getSentMemo(dbData:idNote:networkType)` instead") + static func getSentMemoAsUTF8( + dbData: URL, + idNote: Int64, + networkType: NetworkType + ) -> String? { let dbData = dbData.osStr() guard let memoCStr = zcashlc_get_sent_memo_as_utf8(dbData.0, dbData.1, idNote, networkType.networkId) else { return nil } - let memo = String(validatingUTF8: memoCStr) - zcashlc_string_free(memoCStr) - return memo + defer { + zcashlc_string_free(memoCStr) + } + + return String(validatingUTF8: memoCStr) } + + static func getSentMemo( + dbData: URL, + idNote: Int64, + networkType: NetworkType + ) -> Memo? { + let dbData = dbData.osStr() + + var contiguousMemoBytes = ContiguousArray(MemoBytes.empty().bytes) + var success = false + + contiguousMemoBytes.withUnsafeMutableBytes{ memoBytePtr in + success = zcashlc_get_sent_memo(dbData.0, dbData.1, idNote, memoBytePtr.baseAddress, networkType.networkId) + } + + guard success else { return nil } + + return (try? MemoBytes(contiguousBytes: contiguousMemoBytes)).flatMap { try? $0.intoMemo() } + } + + static func getReceivedMemo( + dbData: URL, + idNote: Int64, + networkType: NetworkType + ) -> Memo? { + let dbData = dbData.osStr() + + var contiguousMemoBytes = ContiguousArray(MemoBytes.empty().bytes) + var success = false + + contiguousMemoBytes.withUnsafeMutableBufferPointer { memoBytePtr in + success = zcashlc_get_received_memo(dbData.0, dbData.1, idNote, memoBytePtr.baseAddress, networkType.networkId) + } + + guard success else { return nil } + + return (try? MemoBytes(contiguousBytes: contiguousMemoBytes)).flatMap { try? $0.intoMemo() } + } + static func validateCombinedChain(dbCache: URL, dbData: URL, networkType: NetworkType) -> Int32 { let dbCache = dbCache.osStr() @@ -420,13 +469,12 @@ class ZcashRustBackend: ZcashRustBackendWelding { extsk: String, to address: String, value: Int64, - memo: String?, + memo: MemoBytes, spendParamsPath: String, outputParamsPath: String, networkType: NetworkType ) -> Int64 { let dbData = dbData.osStr() - let memoBytes = memo ?? "" return zcashlc_create_to_address( dbData.0, @@ -435,7 +483,7 @@ class ZcashRustBackend: ZcashRustBackendWelding { [CChar](extsk.utf8CString), [CChar](address.utf8CString), value, - [CChar](memoBytes.utf8CString), + memo.bytes, spendParamsPath, UInt(spendParamsPath.lengthOfBytes(using: .utf8)), outputParamsPath, @@ -450,20 +498,19 @@ class ZcashRustBackend: ZcashRustBackendWelding { dbData: URL, account: Int32, xprv: String, - memo: String?, + memo: MemoBytes, spendParamsPath: String, outputParamsPath: String, networkType: NetworkType ) -> Int64 { let dbData = dbData.osStr() - let memoBytes = memo ?? "" return zcashlc_shield_funds( dbData.0, dbData.1, account, [CChar](xprv.utf8CString), - [CChar](memoBytes.utf8CString), + memo.bytes, spendParamsPath, UInt(spendParamsPath.lengthOfBytes(using: .utf8)), outputParamsPath, diff --git a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift index bede117b..7e1f813f 100644 --- a/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift +++ b/Sources/ZcashLightClientKit/Rust/ZcashRustBackendWelding.swift @@ -160,7 +160,16 @@ protocol ZcashRustBackendWelding { - dbData: location of the data db file - idNote: note_id of note where the memo is located */ + @available(*, deprecated, message: "This function will be deprecated soon. Use `getReceivedMemo(dbData:idNote:networkType)` instead") static func getReceivedMemoAsUTF8(dbData: URL, idNote: Int64, networkType: NetworkType) -> String? + + /** + get received memo from note + - Parameters: + - dbData: location of the data db file + - idNote: note_id of note where the memo is located + */ + static func getReceivedMemo(dbData: URL, idNote: Int64, networkType: NetworkType) -> Memo? /** get sent memo from note @@ -168,7 +177,16 @@ protocol ZcashRustBackendWelding { - dbData: location of the data db file - idNote: note_id of note where the memo is located */ + @available(*, deprecated, message: "This function will be deprecated soon. Use `getSentMemo(dbData:idNote:networkType)` instead") static func getSentMemoAsUTF8(dbData: URL, idNote: Int64, networkType: NetworkType) -> String? + + /** + get sent memo from note + - Parameters: + - dbData: location of the data db file + - idNote: note_id of note where the memo is located + */ + static func getSentMemo(dbData: URL, idNote: Int64, networkType: NetworkType) -> Memo? /** Checks that the scanned blocks in the data database, when combined with the recent @@ -301,7 +319,7 @@ protocol ZcashRustBackendWelding { extsk: String, to address: String, value: Int64, - memo: String?, + memo: MemoBytes, spendParamsPath: String, outputParamsPath: String, networkType: NetworkType @@ -324,7 +342,7 @@ protocol ZcashRustBackendWelding { dbData: URL, account: Int32, xprv: String, - memo: String?, + memo: MemoBytes, spendParamsPath: String, outputParamsPath: String, networkType: NetworkType diff --git a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift index a7dd8bfe..a4e73935 100644 --- a/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift +++ b/Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift @@ -458,7 +458,7 @@ public class SDKSynchronizer: Synchronizer { // MARK: Synchronizer methods - @available(*, deprecated, message: "This function will be removed soon, use the one reveiving a `Zatoshi` value instead") + @available(*, deprecated, message: "This function will be removed soon, use the one receiving a `Zatoshi` value instead") public func sendToAddress( spendingKey: String, zatoshi: Int64, @@ -482,6 +482,7 @@ public class SDKSynchronizer: Synchronizer { } // swiftlint:disable:next function_parameter_count + @available(*, deprecated, message: "use Memo type instead of Optional") public func sendToAddress( spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, @@ -489,6 +490,29 @@ public class SDKSynchronizer: Synchronizer { memo: String?, from accountIndex: Int, resultBlock: @escaping (Result) -> Void + ) { + do { + self.sendToAddress( + spendingKey: spendingKey, + zatoshi: zatoshi, + toAddress: toAddress, + memo: try memo.intoMemo(), + from: accountIndex, + resultBlock: resultBlock + ) + } catch { + resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) + } + } + + // swiftlint:disable:next function_parameter_count + public func sendToAddress( + spendingKey: String, + zatoshi: Zatoshi, + toAddress: String, + memo: Memo, + from accountIndex: Int, + resultBlock: @escaping (Result) -> Void ) { initializer.downloadParametersIfNeeded { downloadResult in DispatchQueue.main.async { [weak self] in @@ -508,16 +532,16 @@ public class SDKSynchronizer: Synchronizer { } } } - + public func shieldFunds( - transparentAccountPrivateKey: TransparentAccountPrivKey, - memo: String?, + transparentSecretKey: String, + memo: Memo, from accountIndex: Int, resultBlock: @escaping (Result) -> Void ) { // let's see if there are funds to shield let derivationTool = DerivationTool(networkType: self.network.networkType) - + do { let tAddr = try derivationTool.deriveTransparentAddressFromAccountPrivateKey(transparentAccountPrivateKey, index: 0) @@ -562,8 +586,26 @@ public class SDKSynchronizer: Synchronizer { } } } catch { - resultBlock(.failure(error)) - return + resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) + } + } + + @available(*, deprecated, message: "use shieldFunds with a Memo type") + public func shieldFunds( + transparentSecretKey: String, + memo: String?, + from accountIndex: Int, + resultBlock: @escaping (Result) -> Void + ) { + do { + shieldFunds( + transparentSecretKey: transparentSecretKey, + memo: try memo.intoMemo(), + from: accountIndex, + resultBlock: resultBlock + ) + } catch { + resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) } } @@ -572,7 +614,7 @@ public class SDKSynchronizer: Synchronizer { spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, toAddress: String, - memo: String?, + memo: Memo, from accountIndex: Int, resultBlock: @escaping (Result) -> Void ) { @@ -580,7 +622,7 @@ public class SDKSynchronizer: Synchronizer { let spend = try transactionManager.initSpend( zatoshi: zatoshi, toAddress: toAddress, - memo: memo, + memo: memo.asMemoBytes(), from: accountIndex ) @@ -604,7 +646,7 @@ public class SDKSynchronizer: Synchronizer { } } } catch { - resultBlock(.failure(error)) + resultBlock(.failure(SynchronizerError.uncategorized(underlyingError: error))) } } diff --git a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift index 54d940fb..e3f328a9 100644 --- a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift +++ b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift @@ -19,6 +19,7 @@ enum TransactionManagerError: Error { } class PersistentTransactionManager: OutboundTransactionManager { + var repository: PendingTransactionRepository var encoder: TransactionEncoder var service: LightWalletService @@ -41,7 +42,7 @@ class PersistentTransactionManager: OutboundTransactionManager { func initSpend( zatoshi: Zatoshi, toAddress: String, - memo: String?, + memo: MemoBytes, from accountIndex: Int ) throws -> PendingTransactionEntity { guard let insertedTx = try repository.find( @@ -75,7 +76,7 @@ class PersistentTransactionManager: OutboundTransactionManager { do { let encodedTransaction = try self.encoder.createShieldingTransaction( tAccountPrivateKey: xprv, - memo: pendingTransaction.memo?.asZcashTransactionMemo(), + memoBytes: try pendingTransaction.memo.intoMemoBytes(), from: pendingTransaction.accountIndex ) let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction) @@ -94,6 +95,14 @@ class PersistentTransactionManager: OutboundTransactionManager { DispatchQueue.main.async { result(.failure(TransactionManagerError.updateFailed(pendingTransaction))) } + } catch MemoBytes.Errors.invalidUTF8 { + DispatchQueue.main.async { + result(.failure(TransactionManagerError.shieldingEncodingFailed(pendingTransaction, reason: "Memo contains invalid UTF-8 bytes"))) + } + } catch MemoBytes.Errors.tooLong(let length) { + DispatchQueue.main.async { + result(.failure(TransactionManagerError.shieldingEncodingFailed(pendingTransaction, reason: "Memo is too long. expected 512 bytes, received \(length)"))) + } } catch { DispatchQueue.main.async { result(.failure(error)) @@ -109,14 +118,17 @@ class PersistentTransactionManager: OutboundTransactionManager { ) { queue.async { [weak self] in guard let self = self else { return } + do { + let encodedTransaction = try self.encoder.createTransaction( spendingKey: spendingKey, zatoshi: pendingTransaction.value, to: pendingTransaction.toAddress, - memo: pendingTransaction.memo?.asZcashTransactionMemo(), + memoBytes: try pendingTransaction.memo.intoMemoBytes(), from: pendingTransaction.accountIndex ) + let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction) var pending = pendingTransaction diff --git a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift index e71f99a5..4435b313 100644 --- a/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift @@ -28,7 +28,7 @@ protocol TransactionEncoder { /// - Parameter spendingKey: a `SaplingExtendedSpendingKey` containing the spending key /// - Parameter zatoshi: the amount to send in `Zatoshi` /// - Parameter to: string containing the recipient address - /// - Parameter memo: string containing the memo (optional) + /// - Parameter memoBytes: MemoBytes for this transaction /// - Parameter accountIndex: index of the account that will be used to send the funds /// /// - Throws: a TransactionEncoderError @@ -36,7 +36,7 @@ protocol TransactionEncoder { spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, to address: String, - memo: String?, + memoBytes: MemoBytes, from accountIndex: Int ) throws -> EncodedTransaction @@ -80,11 +80,50 @@ protocol TransactionEncoder { from accountIndex: Int ) throws -> EncodedTransaction - /** - Fetch the Transaction Entity from the encoded representation - - Parameter encodedTransaction: The encoded transaction to expand - - Returns: a TransactionEntity based on the given Encoded Transaction - - Throws: a TransactionEncoderError - */ +/// Creates a transaction, throwing an exception whenever things are missing. When the provided ///wallet implementation +/// doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather ///than using +/// double-bangs for things). +/// Non-blocking + +/// - Parameters: +/// - Parameter spendingKey: a string containing the spending key +/// - Parameter zatoshi: the amount to send in zatoshis +/// - Parameter to: string containing the recipient address +/// - Parameter memoBytes: MemoBytes for this transaction +/// - Parameter accountIndex: index of the account that will be used to send the funds +/// - Parameter result: a non escaping closure that receives a Result containing either an ///EncodedTransaction or a TransactionEncoderError + func createTransaction( + spendingKey: String, + zatoshi: Int, + to address: String, + memoBytes: MemoBytes, + from accountIndex: Int, + result: @escaping TransactionEncoderResultBlock + )// swiftlint:disable function_parameter_count + +/// Creates a transaction that will attempt to shield transparent funds that are present on the cacheDB. +/// Throwing an exception whenever things are missing. When the provided wallet implementation +/// doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using double-bangs for things). +/// +/// Blocking +/// +/// - Parameters: +/// - Parameter spendingKey: a string containing the spending key +/// - Parameter tSecretKey: transparent secret key to spend the UTXOs +/// - Parameter Parameter memoBytes: MemoBytes for this transaction +/// - Parameter accountIndex: index of the account that will be used to send the funds +/// +/// - Throws: a TransactionEncoderError + func createShieldingTransaction( + spendingKey: String, + tSecretKey: String, + memoBytes: MemoBytes, + from accountIndex: Int + ) throws -> EncodedTransaction + + ///Fetch the Transaction Entity from the encoded representation + /// - Parameter encodedTransaction: The encoded transaction to expand + /// - Returns: a TransactionEntity based on the given Encoded Transaction + /// - Throws: a TransactionEncoderError func expandEncodedTransaction(_ encodedTransaction: EncodedTransaction) throws -> TransactionEntity } diff --git a/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift b/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift index a9cc4277..469a6707 100644 --- a/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift +++ b/Sources/ZcashLightClientKit/Transaction/TransactionManager.swift @@ -17,7 +17,7 @@ protocol OutboundTransactionManager { func initSpend( zatoshi: Zatoshi, toAddress: String, - memo: String?, + memo: MemoBytes, from accountIndex: Int ) throws -> PendingTransactionEntity diff --git a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift index 9f020410..651b10c3 100644 --- a/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift +++ b/Sources/ZcashLightClientKit/Transaction/WalletTransactionEncoder.swift @@ -53,14 +53,14 @@ class WalletTransactionEncoder: TransactionEncoder { spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, to address: String, - memo: String?, + memoBytes: MemoBytes, from accountIndex: Int ) throws -> EncodedTransaction { let txId = try createSpend( spendingKey: spendingKey, zatoshi: zatoshi, to: address, - memo: memo, + memoBytes: memoBytes, from: accountIndex ) @@ -83,7 +83,7 @@ class WalletTransactionEncoder: TransactionEncoder { spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, to address: String, - memo: String?, + memoBytes: MemoBytes, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock ) { @@ -96,7 +96,7 @@ class WalletTransactionEncoder: TransactionEncoder { spendingKey: spendingKey, zatoshi: zatoshi, to: address, - memo: memo, + memoBytes: memoBytes, from: accountIndex ) ) @@ -111,7 +111,7 @@ class WalletTransactionEncoder: TransactionEncoder { spendingKey: SaplingExtendedSpendingKey, zatoshi: Zatoshi, to address: String, - memo: String?, + memoBytes: MemoBytes, from accountIndex: Int ) throws -> Int { guard ensureParams(spend: self.spendParamsURL, output: self.spendParamsURL) else { @@ -124,7 +124,7 @@ class WalletTransactionEncoder: TransactionEncoder { extsk: spendingKey.stringEncoded, to: address, value: zatoshi.amount, - memo: memo, + memo: memoBytes, spendParamsPath: self.spendParamsURL.path, outputParamsPath: self.outputParamsURL.path, networkType: networkType @@ -139,12 +139,12 @@ class WalletTransactionEncoder: TransactionEncoder { func createShieldingTransaction( tAccountPrivateKey: TransparentAccountPrivKey, - memo: String?, + memoBytes: MemoBytes, from accountIndex: Int ) throws -> EncodedTransaction { let txId = try createShieldingSpend( xprv: tAccountPrivateKey.encoding, - memo: memo, + memo: memoBytes, accountIndex: accountIndex ) @@ -162,7 +162,7 @@ class WalletTransactionEncoder: TransactionEncoder { } } - func createShieldingSpend(xprv: String, memo: String?, accountIndex: Int) throws -> Int { + func createShieldingSpend(xprv: String, memo: MemoBytes, accountIndex: Int) throws -> Int { guard ensureParams(spend: self.spendParamsURL, output: self.spendParamsURL) else { throw TransactionEncoderError.missingParams } diff --git a/Tests/DarksideTests/AdvancedReOrgTests.swift b/Tests/DarksideTests/AdvancedReOrgTests.swift index a2d8e11e..d5eccce9 100644 --- a/Tests/DarksideTests/AdvancedReOrgTests.swift +++ b/Tests/DarksideTests/AdvancedReOrgTests.swift @@ -66,22 +66,21 @@ class AdvancedReOrgTests: XCTestCase { reorgExpectation.fulfill() } - /* - pre-condition: know balances before tx at received_Tx_height arrives - 1. Setup w/ default dataset - 2. applyStaged(received_Tx_height) - 3. sync up to received_Tx_height - 3a. verify that balance is previous balance + tx amount - 4. get that transaction hex encoded data - 5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 3 - 6. stage tx at received_Tx_height + 3 - 6a. applyheight(received_Tx_height + 1) - 7. sync to received_Tx_height + 1 - 8. assert that reorg happened at received_Tx_height - 9. verify that balance equals initial balance - 10. sync up to received_Tx_height + 3 - 11. verify that balance equals initial balance + tx amount - */ + + /// pre-condition: know balances before tx at received_Tx_height arrives + /// 1. Setup w/ default dataset + /// 2. applyStaged(received_Tx_height) + /// 3. sync up to received_Tx_height + /// 3a. verify that balance is previous balance + tx amount + /// 4. get that transaction hex encoded data + /// 5. stage 5 empty blocks w/heights received_Tx_height to received_Tx_height + 3 + /// 6. stage tx at received_Tx_height + 3 + /// 6a. applyheight(received_Tx_height + 1) + /// 7. sync to received_Tx_height + 1 + /// 8. assert that reorg happened at received_Tx_height + /// 9. verify that balance equals initial balance + /// 10. sync up to received_Tx_height + 3 + /// 11. verify that balance equals initial balance + tx amount func testReOrgChangesInboundTxMinedHeight() throws { hookToReOrgNotification() try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) @@ -242,32 +241,31 @@ class AdvancedReOrgTests: XCTestCase { XCTAssertEqual(initialTotalBalance + receivedTx.value, finalReorgTxTotalBalance) } - /** - An outbound, unconfirmed transaction in a specific block changes height in the event of a reorg - - - The wallet handles this change, reflects it appropriately in local storage, and funds remain spendable post confirmation. - - Pre-conditions: - - Wallet has spendable funds - - 1. Setup w/ default dataset - 2. applyStaged(received_Tx_height) - 3. sync up to received_Tx_height - 4. create transaction - 5. stage 10 empty blocks - 6. submit tx at sentTxHeight - 6a. getIncomingTx - 6b. stageTransaction(sentTx, sentTxHeight) - 6c. applyheight(sentTxHeight + 1 ) - 7. sync to sentTxHeight + 2 - 8. stage sentTx and otherTx at sentTxheight - 9. applyStaged(sentTx + 2) - 10. sync up to received_Tx_height + 2 - 11. verify that the sent tx is mined and balance is correct - 12. applyStaged(sentTx + 10) - 13. verify that there's no more pending transaction - */ + + /// An outbound, unconfirmed transaction in a specific block changes height in the event of a reorg + /// + /// + /// The wallet handles this change, reflects it appropriately in local storage, and funds remain spendable post confirmation. + /// + /// Pre-conditions: + /// - Wallet has spendable funds + /// + /// 1. Setup w/ default dataset + /// 2. applyStaged(received_Tx_height) + /// 3. sync up to received_Tx_height + /// 4. create transaction + /// 5. stage 10 empty blocks + /// 6. submit tx at sentTxHeight + /// a. getIncomingTx + /// b. stageTransaction(sentTx, sentTxHeight) + /// c. applyheight(sentTxHeight + 1 ) + /// 7. sync to sentTxHeight + 2 + /// 8. stage sentTx and otherTx at sentTxheight + /// 9. applyStaged(sentTx + 2) + /// 10. sync up to received_Tx_height + 2 + /// 11. verify that the sent tx is mined and balance is correct + /// 12. applyStaged(sentTx + 10) + /// 13. verify that there's no more pending transaction func testReorgChangesOutboundTxIndex() throws { try FakeChainBuilder.buildChain(darksideWallet: self.coordinator.service, branchID: branchID, chainName: chainName) let receivedTxHeight: BlockHeight = 663188 @@ -303,7 +301,7 @@ class AdvancedReOrgTests: XCTestCase { spendingKey: coordinator.spendingKeys!.first!, zatoshi: sendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test transaction", + memo: try Memo(string: "test transaction"), from: 0 ) { result in switch result { @@ -522,17 +520,16 @@ class AdvancedReOrgTests: XCTestCase { XCTAssertEqual(afterReOrgVerifiedBalance, initialVerifiedBalance) } - /** - Steps: - 1. sync up to an incoming transaction (incomingTxHeight + 1) - 1a. save balances - 2. stage 4 blocks from incomingTxHeight - 1 with different nonce - 3. stage otherTx at incomingTxHeight - 4. stage incomingTx at incomingTxHeight - 5. applyHeight(incomingHeight + 3) - 6. sync to latest height - 7. check that balances still match - */ + + /// Steps: + /// 1. sync up to an incoming transaction (incomingTxHeight + 1) + /// 1a. save balances + /// 2. stage 4 blocks from incomingTxHeight - 1 with different nonce + /// 3. stage otherTx at incomingTxHeight + /// 4. stage incomingTx at incomingTxHeight + /// 5. applyHeight(incomingHeight + 3) + /// 6. sync to latest height + /// 7. check that balances still match func testReOrgChangesInboundTxIndexInBlock() throws { try FakeChainBuilder.buildChain(darksideWallet: coordinator.service, branchID: branchID, chainName: chainName) @@ -649,33 +646,30 @@ class AdvancedReOrgTests: XCTestCase { XCTAssertEqual(coordinator.synchronizer.initializer.getBalance(), initialBalance) XCTAssertEqual(coordinator.synchronizer.initializer.getVerifiedBalance(), initialVerifiedBalance) } - - /** - A Re Org occurs and changes the height of an outbound transaction - Pre-condition: Wallet has funds - - Steps: - 1. create fake chain - 1a. sync to latest height - 2. send transaction to recipient address - 3. getIncomingTransaction - 4. stage transaction at sentTxHeight - 5. applyHeight(sentTxHeight) - 6. sync to latest height - 6a. verify that there's a pending transaction with a mined height of sentTxHeight - 7. stage 15 blocks from sentTxHeight - 7. a stage sent tx to sentTxHeight + 2 - 8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg - 9. sync to latest height - 10. verify that there's a pending transaction with -1 mined height - 11. applyHeight(sentTxHeight + 2) - 11a. sync to latest height - 12. verify that there's a pending transaction with a mined height of sentTxHeight + 2 - 13. apply height(sentTxHeight + 15) - 14. sync to latest height - 15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection - - */ + + /// A Re Org occurs and changes the height of an outbound transaction + /// Pre-condition: Wallet has funds + /// + /// Steps: + /// 1. create fake chain + /// 1a. sync to latest height + /// 2. send transaction to recipient address + /// 3. getIncomingTransaction + /// 4. stage transaction at sentTxHeight + /// 5. applyHeight(sentTxHeight) + /// 6. sync to latest height + /// 6a. verify that there's a pending transaction with a mined height of sentTxHeight + /// 7. stage 15 blocks from sentTxHeight + /// 7. a stage sent tx to sentTxHeight + 2 + /// 8. applyHeight(sentTxHeight + 1) to cause a 1 block reorg + /// 9. sync to latest height + /// 10. verify that there's a pending transaction with -1 mined height + /// 11. applyHeight(sentTxHeight + 2) + /// 11a. sync to latest height + /// 12. verify that there's a pending transaction with a mined height of sentTxHeight + 2 + /// 13. apply height(sentTxHeight + 15) + /// 14. sync to latest height + /// 15. verify that there's no pending transaction and that the tx is displayed on the sentTransactions collection func testReOrgChangesOutboundTxMinedHeight() throws { hookToReOrgNotification() @@ -710,7 +704,7 @@ class AdvancedReOrgTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: Zatoshi(20000), toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -889,21 +883,20 @@ class AdvancedReOrgTests: XCTestCase { ) } - /** - Uses the zcash-hackworks data set. - A Re Org occurs at 663195, and sweeps an Inbound Tx that appears later on the chain. - Steps: - 1. reset dlwd - 2. load blocks from txHeightReOrgBefore - 3. applyStaged(663195) - 4. sync to latest height - 5. get balances - 6. load blocks from dataset txHeightReOrgBefore - 7. apply stage 663200 - 8. sync to latest height - 9. verify that the balance is equal to the one before the reorg - */ + /// Uses the zcash-hackworks data set. + + /// A Re Org occurs at 663195, and sweeps an Inbound Tx that appears later on the chain. + /// Steps: + /// 1. reset dlwd + /// 2. load blocks from txHeightReOrgBefore + /// 3. applyStaged(663195) + /// 4. sync to latest height + /// 5. get balances + /// 6. load blocks from dataset txHeightReOrgBefore + /// 7. apply stage 663200 + /// 8. sync to latest height + /// 9. verify that the balance is equal to the one before the reorg func testReOrgChangesInboundMinedHeight() throws { try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) sleep(2) @@ -950,16 +943,14 @@ class AdvancedReOrgTests: XCTestCase { XCTAssert(afterReOrgTxHeight > initialTxHeight) } - /** - Re Org removes incoming transaction and is never mined - Steps: - 1. sync prior to incomingTxHeight - 1 to get balances there - 2. sync to latest height - 3. cause reorg - 4. sync to latest height - 5. verify that reorg Happened at reorgHeight - 6. verify that balances match initial balances - */ + /// Re Org removes incoming transaction and is never mined + /// Steps: + /// 1. sync prior to incomingTxHeight - 1 to get balances there + /// 2. sync to latest height + /// 3. cause reorg + /// 4. sync to latest height + /// 5. verify that reorg Happened at reorgHeight + /// 6. verify that balances match initial balances func testReOrgRemovesIncomingTxForever() throws { hookToReOrgNotification() try coordinator.reset(saplingActivation: 663150, branchID: branchID, chainName: chainName) @@ -1019,21 +1010,19 @@ class AdvancedReOrgTests: XCTestCase { XCTAssertEqual(initialTotalBalance, coordinator.synchronizer.initializer.getBalance()) } - /** - Transaction was included in a block, and then is not included in a block after a reorg, and expires. - Steps: - 1. create fake chain - 1a. sync to latest height - 2. send transaction to recipient address - 3. getIncomingTransaction - 4. stage transaction at sentTxHeight - 5. applyHeight(sentTxHeight) - 6. sync to latest height - 6a. verify that there's a pending transaction with a mined height of sentTxHeight - 7. stage 15 blocks from sentTxHeigth to cause a reorg - 8. sync to latest height - 9. verify that there's an expired transaction as a pending transaction - */ + /// Transaction was included in a block, and then is not included in a block after a reorg, and expires. + /// Steps: + /// 1. create fake chain + /// 1a. sync to latest height + /// 2. send transaction to recipient address + /// 3. getIncomingTransaction + /// 4. stage transaction at sentTxHeight + /// 5. applyHeight(sentTxHeight) + /// 6. sync to latest height + /// 6a. verify that there's a pending transaction with a mined height of sentTxHeight + /// 7. stage 15 blocks from sentTxHeigth to cause a reorg + /// 8. sync to latest height + /// 9. verify that there's an expired transaction as a pending transaction func testReOrgRemovesOutboundTxAndIsNeverMined() throws { hookToReOrgNotification() @@ -1070,7 +1059,7 @@ class AdvancedReOrgTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: Zatoshi(20000), toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try! Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { diff --git a/Tests/DarksideTests/BalanceTests.swift b/Tests/DarksideTests/BalanceTests.swift index 2c0db15e..0a29f900 100644 --- a/Tests/DarksideTests/BalanceTests.swift +++ b/Tests/DarksideTests/BalanceTests.swift @@ -83,7 +83,7 @@ class BalanceTests: XCTestCase { spendingKey: spendingKey, zatoshi: maxBalance, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -227,7 +227,7 @@ class BalanceTests: XCTestCase { spendingKey: spendingKey, zatoshi: maxBalanceMinusOne, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + memo: try Memo(string: "\(self.description) \(Date().description)"), from: 0, resultBlock: { result in switch result { @@ -368,7 +368,7 @@ class BalanceTests: XCTestCase { spendingKey: spendingKey, zatoshi: maxBalanceMinusOne, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + memo: try Memo(string: "test send \(self.description) \(Date().description)"), from: 0, resultBlock: { result in switch result { @@ -510,8 +510,8 @@ class BalanceTests: XCTestCase { coordinator.synchronizer.sendToAddress( spendingKey: spendingKey, zatoshi: sendAmount, - toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + toAddress: testRecipientAddress, + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -682,7 +682,7 @@ class BalanceTests: XCTestCase { spendingKey: spendingKey, zatoshi: sendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + memo: try Memo(string: "test send \(self.description) \(Date().description)"), from: 0, resultBlock: { result in switch result { @@ -831,7 +831,7 @@ class BalanceTests: XCTestCase { /* Send */ - let memo = "shielding is fun!" + let memo = try Memo(string: "shielding is fun!") var pendingTx: PendingTransactionEntity? coordinator.synchronizer.sendToAddress( spendingKey: spendingKeys, @@ -892,7 +892,12 @@ class BalanceTests: XCTestCase { */ XCTAssertEqual(confirmedTx.value, self.sendAmount) XCTAssertEqual(confirmedTx.toAddress, self.testRecipientAddress) - XCTAssertEqual(confirmedTx.memo?.asZcashTransactionMemo(), memo) + do { + let confirmedMemo = try confirmedTx.memo.intoMemoBytes().intoMemo() + XCTAssertEqual(confirmedMemo, memo) + } catch { + XCTFail("failed retrieving memo from confirmed transaction. Error: \(error.localizedDescription)") + } guard let transactionId = confirmedTx.rawTransactionId else { XCTFail("no raw transaction id") @@ -1009,7 +1014,7 @@ class BalanceTests: XCTestCase { spendingKey: spendingKey, zatoshi: sendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description)", + memo: try Memo(string: "test send \(self.description)"), from: 0, resultBlock: { result in switch result { diff --git a/Tests/DarksideTests/NetworkUpgradeTests.swift b/Tests/DarksideTests/NetworkUpgradeTests.swift index af87fc3b..fc72f8c3 100644 --- a/Tests/DarksideTests/NetworkUpgradeTests.swift +++ b/Tests/DarksideTests/NetworkUpgradeTests.swift @@ -83,7 +83,7 @@ class NetworkUpgradeTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: spendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -172,7 +172,7 @@ class NetworkUpgradeTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: spendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -241,7 +241,7 @@ class NetworkUpgradeTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: spendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -342,7 +342,7 @@ class NetworkUpgradeTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: spendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { @@ -454,7 +454,7 @@ class NetworkUpgradeTests: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: spendAmount, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { diff --git a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift index b4fa0e78..ff5b9490 100644 --- a/Tests/DarksideTests/PendingTransactionUpdatesTest.swift +++ b/Tests/DarksideTests/PendingTransactionUpdatesTest.swift @@ -98,7 +98,7 @@ class PendingTransactionUpdatesTest: XCTestCase { spendingKey: self.coordinator.spendingKeys!.first!, zatoshi: Zatoshi(20000), toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "this is a test", + memo: try Memo(string: "this is a test"), from: 0, resultBlock: { result in switch result { diff --git a/Tests/DarksideTests/RewindRescanTests.swift b/Tests/DarksideTests/RewindRescanTests.swift index c64fe284..cf00ee7f 100644 --- a/Tests/DarksideTests/RewindRescanTests.swift +++ b/Tests/DarksideTests/RewindRescanTests.swift @@ -170,7 +170,7 @@ class RewindRescanTests: XCTestCase { spendingKey: coordinator.spendingKey, zatoshi: Zatoshi(1000), toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: nil, + memo: .empty, from: 0 ) { result in sendExpectation.fulfill() @@ -271,7 +271,7 @@ class RewindRescanTests: XCTestCase { spendingKey: spendingKey, zatoshi: maxBalance, toAddress: try Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test send \(self.description) \(Date().description)", + memo: try Memo("test send \(self.description) \(Date().description)"), from: 0 ) { result in switch result { diff --git a/Tests/DarksideTests/ShieldFundsTests.swift b/Tests/DarksideTests/ShieldFundsTests.swift index 02e0e6cc..342ff2d5 100644 --- a/Tests/DarksideTests/ShieldFundsTests.swift +++ b/Tests/DarksideTests/ShieldFundsTests.swift @@ -210,7 +210,7 @@ class ShieldFundsTests: XCTestCase { // shield the funds coordinator.synchronizer.shieldFunds( transparentAccountPrivateKey: transparentAccountPrivateKey, - memo: "shield funds", + memo: try Memo(string: "shield funds"), from: 0 ) { result in switch result { diff --git a/Tests/DarksideTests/Z2TReceiveTests.swift b/Tests/DarksideTests/Z2TReceiveTests.swift index 6f746a4c..3dcfdf82 100644 --- a/Tests/DarksideTests/Z2TReceiveTests.swift +++ b/Tests/DarksideTests/Z2TReceiveTests.swift @@ -105,7 +105,7 @@ class Z2TReceiveTests: XCTestCase { spendingKey: coordinator.spendingKeys!.first!, zatoshi: sendAmount, toAddress: try! Recipient(testRecipientAddress, network: self.network.networkType), - memo: "test transaction", + memo: try Memo(string: "test transaction"), from: 0 ) { result in switch result { diff --git a/Tests/OfflineTests/NullBytesTests.swift b/Tests/OfflineTests/NullBytesTests.swift index 3b751a35..b6111398 100644 --- a/Tests/OfflineTests/NullBytesTests.swift +++ b/Tests/OfflineTests/NullBytesTests.swift @@ -122,10 +122,20 @@ class NullBytesTests: XCTestCase { XCTAssertFalse(validZaddr.containsCStringNullBytesBeforeStringEnding()) XCTAssertTrue( - "zs1gqtfu59z20s\09t20mxlxj86zpw6p69l0ev98uxrmlykf2nchj2dw8ny5e0l22kwmld2afc37gkfp" + "zs1gqtfu59z20s\u{0}9t20mxlxj86zpw6p69l0ev98uxrmlykf2nchj2dw8ny5e0l22kwmld2afc37gkfp" .containsCStringNullBytesBeforeStringEnding() ) - XCTAssertTrue("\0".containsCStringNullBytesBeforeStringEnding()) + XCTAssertTrue("\u{0}".containsCStringNullBytesBeforeStringEnding()) XCTAssertFalse("".containsCStringNullBytesBeforeStringEnding()) } + + func testTrimTrailingNullBytes() throws { + let nullTrailedString = "This Is a memo with text and trailing null bytes\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}" + + let nonNullTrailedString = "This Is a memo with text and trailing null bytes" + + let trimmedString = String(nullTrailedString.reversed().drop(while: { $0 == "\u{0}"}).reversed()) + + XCTAssertEqual(trimmedString, nonNullTrailedString) + } } diff --git a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift index f3b864e7..bcab55b9 100644 --- a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift +++ b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift @@ -157,8 +157,8 @@ class PendingTransactionRepositoryTests: XCTestCase { XCTAssertEqual(updatedTransaction.toAddress, stored!.toAddress) } - func createAndStoreMockedTransaction() -> PendingTransactionEntity { - var transaction = mockTransaction() + func createAndStoreMockedTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity { + var transaction = mockTransaction(with: value) var id: Int? XCTAssertNoThrow(try { id = try pendingRepository.create(transaction) }()) @@ -173,12 +173,7 @@ class PendingTransactionRepositoryTests: XCTestCase { } } - private func mockTransaction() -> PendingTransactionEntity { - PendingTransaction( - value: Zatoshi(Int64.random(in: 1 ... 1_000_000)), - toAddress: recipientAddress, - memo: nil, - account: 0 - ) + private func mockTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity { + PendingTransaction(value: value, toAddress: recipientAddress, memo: .empty(), account: 0) } } diff --git a/Tests/OfflineTests/Zip302MemoTests.swift b/Tests/OfflineTests/Zip302MemoTests.swift index 90d29a88..ce7e4c2a 100644 --- a/Tests/OfflineTests/Zip302MemoTests.swift +++ b/Tests/OfflineTests/Zip302MemoTests.swift @@ -105,10 +105,7 @@ class Zip302MemoTests: XCTestCase { func testItCreatesAMemoFromAValidAndShortEnoughText() throws { let almostTooLongString = "thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeryyyyyyyyyyyyyyyyyyyyyyyyyy looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong meeeeeeeeeeeeeeeeeeemooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo but it's just short enough" - guard let memo = try Memo(string: almostTooLongString) else { - XCTFail("Expected `Memo` or `Error` thrown but found `nil`") - return - } + let memo = try Memo(string: almostTooLongString) let memoBytes = try memo.asMemoBytes() @@ -128,12 +125,101 @@ class Zip302MemoTests: XCTestCase { switch error { case .tooLong(let count): XCTAssertEqual(count, 515) - case .invalidUTF8: XCTFail("Expected `.tooLong(515) but found `.invalidUTF8`") + case .endsWithNullBytes: + XCTFail("Expected `.tooLong(515) but found `.endsWithNullBytes`") } } } + + func testInitMemoBytesFromContiguousBytes() throws { + let contiguousEmptyBytes = ContiguousArray(Zip302MemoTests.emptyMemoBytes) + + let emptyMemoBytes = try MemoBytes(contiguousBytes: contiguousEmptyBytes) + + XCTAssertEqual(emptyMemoBytes.bytes, .emptyMemoBytes) + + let contiguousTextMemoBytes = ContiguousArray(Zip302MemoTests.helloImATextMemo) + + let textMemoBytes = try MemoBytes(contiguousBytes: contiguousTextMemoBytes) + + XCTAssertEqual(textMemoBytes.bytes, Zip302MemoTests.helloImATextMemo) + } + + func testThrowsWhenTextMemoIsConstructedWithTrailingNullBytes() throws { + let nullTrailedString = "This Is a memo with text and trailing null bytes\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}" + + XCTAssertThrowsError(try Memo(string: nullTrailedString)) { error in + guard let thrownError = error as? MemoBytes.Errors else { + XCTFail("Thrown erros is not MemoBytes.Error") + return + } + + switch thrownError { + case .invalidUTF8, .tooLong: + XCTFail("Expected .endsWithNullBytes found other errors") + case .endsWithNullBytes: + return + } + } + } + + func testThrowsWhenTextMemoIsConstructedWithNullBytes() throws { + let nullTrailedString = "\u{0}" + + XCTAssertThrowsError(try Memo(string: nullTrailedString)) { error in + guard let thrownError = error as? MemoBytes.Errors else { + XCTFail("Thrown erros is not MemoBytes.Error") + return + } + + switch thrownError { + case .invalidUTF8, .tooLong: + XCTFail("Expected .endsWithNullBytes found other errors") + case .endsWithNullBytes: + return + } + } + } + + func testTextMemoIsConstructedWithLeadingNullBytes() throws { + let nullLedString = "\u{0}ABC" + + let nullLedTextMemo = try MemoText(nullLedString) + + let nullLedMemo = try Memo(string: nullLedString) + + if case .text(let textMemo) = nullLedMemo { + XCTAssertEqual(nullLedTextMemo, textMemo) + } else { + XCTFail("Expected a TextMemo") + } + } + + func testTextMemoIsConstructedWithEmptyString() throws { + let emptyString = "" + + let emptyTextMemo = try MemoText(emptyString) + + let emptyStringMemo = try Memo(string: emptyString) + + if case .text(let textMemo) = emptyStringMemo { + XCTAssertEqual(emptyTextMemo, textMemo) + } else { + XCTFail("Expected a TextMemo") + } + } + + func testUnpaddedRawBytesWhenPaddingIsFound() throws { + let expected: [UInt8] = [0x56, 0x17, 0xe0, 0xac, 0x3c, 0xbc, 0xde] + + XCTAssertEqual(Zip302MemoTests.shortButPaddedBytes.unpaddedRawBytes(), expected) + } + + func testUnpaddedRawBytesWhenThereIsNoPadding() throws { + XCTAssertEqual(Self.fullMemoBytes.unpaddedRawBytes(), Self.fullMemoBytes) + } } diff --git a/Tests/TestUtils/Tests+Utils.swift b/Tests/TestUtils/Tests+Utils.swift index 4d110bc1..14378c01 100644 --- a/Tests/TestUtils/Tests+Utils.swift +++ b/Tests/TestUtils/Tests+Utils.swift @@ -117,6 +117,7 @@ func deleteParametersFromDocuments() throws { output: documents.appendingPathComponent("sapling-output.params") ) } + func deleteParamsFrom(spend: URL, output: URL) { try? FileManager.default.removeItem(at: spend) try? FileManager.default.removeItem(at: output)