[#384] Adopt Type Safe Memos in the FFI and SDK

Closes #384

This makes the SDK use the Memo and MemoBytes types across the FFI and the
public interface of the SDKSynchronizer. Addresses the comments by @defuse  on
issue https://github.com/zcash/ZcashLightClientKit/issues/378.
These changes depend on zcash-light-client-ffi repo changes managing
memos as byte arrays instead of string encoding of memos.

There are some compromises in terms of database representation of memos
and their exposure in transaction representing entities because we
wanted to avoid changing the database at this moment and be retro-
compatible with earlier versions and maintain deprecated APIs for wallets
to have time to adopt.

Memo and MemoBytes are WIP and subject to change. we are exploring
making Memo an opaque type based on a struct and drop the enum approach
to make the interface less throwing.

Fix: wrap new functions and deprecate old API. catch Memo errors

update get_sent and get_received memo functions from rust welding
point to branch in FFI repo

Fix type Tests/DarksideTests/BalanceTests.swift

Co-authored-by: Carter Jernigan <git@carterjernigan.com>

PR suggestions. Inverted condition

PR Suggestions + cleanup

merge fixes

Suggestions from PR to treat null bytes on TextMemo

refactor unpaddedRawBytes function and add tests

undo change in PendingTransactionSQLDAO

Fix broken import on Swift Package for libzcashlc

Fix libzcashlc url

Fix travis.yml
This commit is contained in:
Francisco Gindre 2022-05-31 09:27:24 -03:00
parent a8e6b5b5e4
commit 9b930391a4
22 changed files with 522 additions and 223 deletions

View File

@ -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:

View File

@ -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
)
}

View File

@ -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<UInt8>) 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)
}
}
}

View File

@ -0,0 +1,7 @@
{
"network": "main",
"height": "1686871",
"hash": "0000000000443d2be0b66f0f2c03784cf267e50935562a23c10d9d3841343cd7",
"time": 1654001768,
"saplingTree": "01802ef6c171eca37050fbc6cdf8d9712a8c5e4650af1a31300c143e063a680e2100140000000001e684c306416b750c7b4883e145922c129e5919a658b02e6146d2da8d17a3ff42000001ae61a98ec05d976a41e46f0b7ae8dee5bda5ab490eea7b701153511a15be42590198c748a5e9516711db607e282f08013ba00b3e77bc66ec28702ca17893a5154b000139113a1e0f54d93c6180f2df7d16afbf7c320f6c7665cb10e1fc309878b0d716019a0214bcb1fd7a70e53166101992147512f3debb0fdce45ac625fffd64771010000134136b9f1f00c2e9d16dc7358ef920862511c8fc42fb7074cbc9016d8d4e8b4c015eddc191a81221b7900bbdcd8610e5df595e3cdc7fd5b3432e3825206ae35b05017eda713cd733ccc555123788692a1876f9ca292b0aa2ddf3f45ed2b47f027340000000015ec9e9b1295908beed437df4126032ca57ada8e3ebb67067cd22a73c79a84009"
}

View File

@ -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<UInt8>(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<UInt8>(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,

View File

@ -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

View File

@ -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<String>")
public func sendToAddress(
spendingKey: SaplingExtendedSpendingKey,
zatoshi: Zatoshi,
@ -489,6 +490,29 @@ public class SDKSynchronizer: Synchronizer {
memo: String?,
from accountIndex: Int,
resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> 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<PendingTransactionEntity, Error>) -> 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)))
}
}

View File

@ -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

View File

@ -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
}

View File

@ -17,7 +17,7 @@ protocol OutboundTransactionManager {
func initSpend(
zatoshi: Zatoshi,
toAddress: String,
memo: String?,
memo: MemoBytes,
from accountIndex: Int
) throws -> PendingTransactionEntity

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<UInt8>(Zip302MemoTests.emptyMemoBytes)
let emptyMemoBytes = try MemoBytes(contiguousBytes: contiguousEmptyBytes)
XCTAssertEqual(emptyMemoBytes.bytes, .emptyMemoBytes)
let contiguousTextMemoBytes = ContiguousArray<UInt8>(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)
}
}

View File

@ -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)