Modify PendingTransactionEntity to be able to represent internal shielding tx.

This commit is contained in:
Kris Nuttycombe 2022-09-30 07:45:51 -06:00
parent 91ea6f5476
commit f5d7aa0f17
14 changed files with 175 additions and 62 deletions

View File

@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/grpc/grpc-swift.git",
"state" : {
"revision" : "87cecdeb2aae6b359b754d0dc7099e8237cf1824",
"version" : "1.11.0"
"revision" : "4c63368b7462305903507e8acebd77264c0fb695",
"version" : "1.8.2"
}
},
{
@ -18,31 +18,13 @@
"version" : "0.13.3"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
"version" : "1.0.2"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729",
"version" : "1.0.3"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
"version" : "1.4.4"
"revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
"version" : "1.4.2"
}
},
{
@ -50,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "bc4c55b9f9584f09eb971d67d956e28d08caa9d0",
"version" : "2.43.1"
"revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
"version" : "2.40.0"
}
},
{
@ -59,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "6c84d247754ad77487a6f0694273b89b83efd056",
"version" : "1.14.0"
"revision" : "a75e92bde3683241c15df3dd905b7a6dcac4d551",
"version" : "1.12.1"
}
},
{
@ -68,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "00576e6f1efa5c46dca2ca3081dc56dd233b402d",
"version" : "1.23.0"
"revision" : "108ac15087ea9b79abb6f6742699cf31de0e8772",
"version" : "1.22.0"
}
},
{
@ -77,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git",
"state" : {
"revision" : "ba7c0d7f82affc518147ea61d240330bf7f7ea9b",
"version" : "2.22.1"
"revision" : "42436a25ff32c390465567f5c089a9a8ce8d7baf",
"version" : "2.20.0"
}
},
{
@ -86,8 +68,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-transport-services.git",
"state" : {
"revision" : "b6e37a0d442745760d6ed0195d8f283d3ce0414a",
"version" : "1.14.1"
"revision" : "2cb54f91ddafc90832c5fa247faf5798d0a7c204",
"version" : "1.13.0"
}
},
{
@ -95,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "88c7d15e1242fdb6ecbafbc7926426a19be1e98a",
"version" : "1.20.2"
"revision" : "e1499bc69b9040b29184f7f2996f7bab467c1639",
"version" : "1.19.0"
}
},
{
@ -104,7 +86,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "8eefbd8d70356b426d8e1dd7b07d2703a5a8e222"
"revision" : "0059f090e655667f9ee5ed3306bd87ca78c7711a"
}
}
],

View File

@ -16,7 +16,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.8.0"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.0"),
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "8eefbd8d70356b426d8e1dd7b07d2703a5a8e222")
.package(name:"libzcashlc", url: "https://github.com/zcash-hackworks/zcash-light-client-ffi", revision: "0059f090e655667f9ee5ed3306bd87ca78c7711a")
],
targets: [
.target(

View File

@ -31,6 +31,14 @@ extension NetworkType {
default: return nil
}
}
static func forNetworkId(_ id: UInt32) -> NetworkType? {
switch id {
case 1: return .mainnet
case 0: return .testnet
default: return nil
}
}
}
public enum ZcashNetworkBuilder {

View File

@ -11,6 +11,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case toAddress = "to_address"
case toInternalAccount = "to_internal"
case accountIndex = "account_index"
case minedHeight = "mined_height"
case expiryHeight = "expiry_height"
@ -27,7 +28,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
case rawTransactionId = "txid"
}
var toAddress: String
var recipient: PendingTransactionRecipient
var accountIndex: Int
var minedHeight: BlockHeight
var expiryHeight: BlockHeight
@ -45,7 +46,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
static func from(entity: PendingTransactionEntity) -> PendingTransaction {
PendingTransaction(
toAddress: entity.toAddress,
recipient: entity.recipient,
accountIndex: entity.accountIndex,
minedHeight: entity.minedHeight,
expiryHeight: entity.expiryHeight,
@ -64,7 +65,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
}
init(
toAddress: String,
recipient: PendingTransactionRecipient,
accountIndex: Int,
minedHeight: BlockHeight,
expiryHeight: BlockHeight,
@ -80,7 +81,7 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
memo: Data?,
rawTransactionId: Data?
) {
self.toAddress = toAddress
self.recipient = recipient
self.accountIndex = accountIndex
self.minedHeight = minedHeight
self.expiryHeight = expiryHeight
@ -100,7 +101,18 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.toAddress = try container.decode(String.self, forKey: .toAddress)
let toAddress: String? = try container.decodeIfPresent(String.self, forKey: .toAddress)
let toInternalAccount: UInt32? = try container.decode(UInt32.self, forKey: .toInternalAccount)
switch (toAddress, toInternalAccount) {
case let (.some(address), nil):
self.recipient = .address(Recipient.forEncodedAddress(encoded: address)!.0)
case let (nil, .some(accountId)):
self.recipient = .internalAccount(accountId)
default:
throw StorageError.malformedEntity(fields: ["toAddress", "toInternalAccount"])
}
self.accountIndex = try container.decode(Int.self, forKey: .accountIndex)
self.minedHeight = try container.decode(BlockHeight.self, forKey: .minedHeight)
self.expiryHeight = try container.decode(BlockHeight.self, forKey: .expiryHeight)
@ -122,7 +134,18 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.toAddress, forKey: .toAddress)
var toAddress: String?
var accountId: UInt32?
switch (self.recipient) {
case .address(let recipient):
toAddress = recipient.stringEncoded
case .internalAccount(let acct):
accountId = acct
default:
break
}
try container.encode(toAddress, forKey: .toAddress)
try container.encode(accountId, forKey: .toInternalAccount)
try container.encode(self.accountIndex, forKey: .accountIndex)
try container.encode(self.minedHeight, forKey: .minedHeight)
try container.encode(self.expiryHeight, forKey: .expiryHeight)
@ -145,9 +168,9 @@ struct PendingTransaction: PendingTransactionEntity, Decodable, Encodable {
}
extension PendingTransaction {
init(value: Zatoshi, toAddress: String, memo: MemoBytes, account index: Int) {
init(value: Zatoshi, recipient: PendingTransactionRecipient, memo: MemoBytes, account index: Int) {
self = PendingTransaction(
toAddress: toAddress,
recipient: recipient,
accountIndex: index,
minedHeight: -1,
expiryHeight: -1,
@ -169,6 +192,7 @@ extension PendingTransaction {
class PendingTransactionSQLDAO: PendingTransactionRepository {
enum TableColumns {
static var toAddress = Expression<String>("to_address")
static var toInternalAccount = Expression<String>("to_internal")
static var accountIndex = Expression<Int>("account_index")
static var minedHeight = Expression<Int?>("mined_height")
static var expiryHeight = Expression<Int?>("expiry_height")
@ -197,6 +221,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository {
let statement = table.create(ifNotExists: true) { createdTable in
createdTable.column(TableColumns.id, primaryKey: .autoincrement)
createdTable.column(TableColumns.toAddress)
createdTable.column(TableColumns.toInternalAccount)
createdTable.column(TableColumns.accountIndex)
createdTable.column(TableColumns.minedHeight)
createdTable.column(TableColumns.expiryHeight)

View File

@ -6,6 +6,12 @@
//
import Foundation
public enum PendingTransactionRecipient: Equatable {
case address(Recipient)
case internalAccount(UInt32)
}
/**
Represents a sent transaction that has not been confirmed yet on the blockchain
*/
@ -13,7 +19,7 @@ public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTrans
/**
recipient address
*/
var toAddress: String { get }
var recipient: PendingTransactionRecipient { get }
/**
index of the account from which the funds were sent

View File

@ -1,6 +1,6 @@
//
// WalletTypes.swift
//
//
//
// Created by Francisco Gindre on 4/6/21.
//
@ -94,6 +94,34 @@ public struct SaplingExtendedFullViewingKey: Equatable, StringEncoded, Undescrib
}
}
public enum AddressType: Equatable {
case p2pkh
case p2sh
case sapling
case unified
var id: UInt32 {
switch self {
case .p2pkh: return 0
case .p2sh: return 1
case .sapling: return 2
case .unified: return 3
}
}
}
extension AddressType {
static func forId(_ id: UInt32) -> AddressType? {
switch id {
case 0: return .p2pkh
case 1: return .p2sh
case 2: return .sapling
case 3: return .unified
default: return nil
}
}
}
/// A Transparent Address that can be encoded as a String
///
/// Transactions sent to this address are totally visible in the public
@ -121,7 +149,7 @@ public struct TransparentAddress: Equatable, StringEncoded {
/// Represents a Sapling receiver address. Comonly called zAddress.
/// This address corresponds to the Zcash Sapling shielded pool.
/// Although this it is fully functional, we encourage developers to
/// choose `UnifiedAddress` before Sapling or Transparent ones.
/// choose `UnifiedAddress` before Sapling or Transparent ones.
public struct SaplingAddress: Equatable, StringEncoded {
var encoding: String
@ -234,6 +262,18 @@ public enum Recipient: Equatable, StringEncoded {
throw KeyEncodingError.invalidEncoding
}
}
static func forEncodedAddress(encoded: String) -> (Recipient, NetworkType)? {
return DerivationTool.getAddressMetadata(encoded).map { m in
switch m.addressType {
case .p2pkh: return (.transparent(TransparentAddress(validatedEncoding: encoded)),
m.networkType)
case .p2sh: return (.transparent(TransparentAddress(validatedEncoding: encoded)), m.networkType)
case .sapling: return (.sapling(SaplingAddress(validatedEncoding: encoded)), m.networkType)
case .unified: return (.unified(UnifiedAddress(validatedEncoding: encoded)), m.networkType)
}
}
}
}
public struct WalletBalance: Equatable {

View File

@ -10,6 +10,7 @@ import Foundation
import libzcashlc
class ZcashRustBackend: ZcashRustBackendWelding {
static let minimumConfirmations: UInt32 = 10
static func createAccount(dbData: URL, seed: [UInt8], networkType: NetworkType) throws -> UnifiedSpendingKey {
@ -316,6 +317,26 @@ class ZcashRustBackend: ZcashRustBackendWelding {
return RustWeldingError.genericError(message: message)
}
static func getAddressMetadata(_ address: String) -> AddressMetadata? {
var networkId: UInt32 = 0
var addrId: UInt32 = 0
guard zcashlc_get_address_metadata(
[CChar](address.utf8CString),
&networkId,
&addrId
) else {
return nil
}
guard let network = NetworkType.forNetworkId(networkId),
let addrType = AddressType.forId(addrId)
else {
return nil
}
return AddressMetadata(network: network, addrType: addrType)
}
static func getTransparentReceiver(for uAddr: UnifiedAddress) throws -> TransparentAddress? {
guard let transparentCStr = zcashlc_get_transparent_receiver_for_unified_address(
[CChar](uAddr.encoding.utf8CString)

View File

@ -261,6 +261,10 @@ protocol ZcashRustBackendWelding {
networkType: NetworkType
) throws -> DbInitResult
/// Returns the network and address type for the given Zcash address string,
/// if the string represents a valid Zcash address.
static func getAddressMetadata(_ address: String) -> AddressMetadata?
/// Validates the if the given string is a valid Sapling Address
/// - Parameter address: UTF-8 encoded String to validate
/// - Parameter networkType: network type of this key

View File

@ -468,11 +468,11 @@ public class SDKSynchronizer: Synchronizer {
} catch {
throw SynchronizerError.parameterMissing(underlyingError: error)
}
return try await createToAddress(
spendingKey: spendingKey,
zatoshi: zatoshi,
toAddress: toAddress.stringEncoded,
recipient: toAddress,
memo: memo
)
}
@ -498,7 +498,7 @@ public class SDKSynchronizer: Synchronizer {
throw ShieldFundsError.shieldingFailed(underlyingError: KeyEncodingError.invalidEncoding)
}
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, toAddress: uAddr.stringEncoded, memo: try memo.asMemoBytes(), from: accountIndex)
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance.verified, recipient: .unified(uAddr), memo: try memo.asMemoBytes(), from: accountIndex)
// TODO: Task will be removed when this method is changed to async, issue 487, https://github.com/zcash/ZcashLightClientKit/issues/487
let transaction = try await transactionManager.encodeShieldingTransaction(
@ -515,13 +515,13 @@ public class SDKSynchronizer: Synchronizer {
func createToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
toAddress: String,
recipient: Recipient,
memo: Memo
) async throws -> PendingTransactionEntity {
do {
let spend = try transactionManager.initSpend(
zatoshi: zatoshi,
toAddress: toAddress,
recipient: recipient,
memo: memo.asMemoBytes(),
from: Int(spendingKey.account)
)

View File

@ -69,6 +69,10 @@ public class DerivationTool: KeyDeriving {
try rustwelding.getTransparentReceiver(for: unifiedAddress)
}
public static func getAddressMetadata(_ addr: String) -> AddressMetadata? {
rustwelding.getAddressMetadata(addr)
}
/// Given a spending key, return the associated viewing key.
/// - Parameter spendingKey: the `UnifiedSpendingKey` from which to derive the `UnifiedFullViewingKey` from.
/// - Returns: the viewing key that corresponds to the spending key.
@ -106,6 +110,16 @@ public class DerivationTool: KeyDeriving {
}
}
public struct AddressMetadata {
var networkType: NetworkType
var addressType: AddressType
public init(network: NetworkType, addrType: AddressType) {
self.networkType = network
self.addressType = addrType
}
}
extension DerivationTool: KeyValidation {
public func isValidUnifiedFullViewingKey(_ ufvk: String) -> Bool {
DerivationTool.rustwelding.isValidUnifiedFullViewingKey(ufvk, networkType: networkType)

View File

@ -8,7 +8,7 @@
import Foundation
enum TransactionManagerError: Error {
case couldNotCreateSpend(toAddress: String, account: Int, zatoshi: Zatoshi)
case couldNotCreateSpend(recipient: Recipient, account: Int, zatoshi: Zatoshi)
case encodingFailed(PendingTransactionEntity)
case updateFailed(PendingTransactionEntity)
case notPending(PendingTransactionEntity)
@ -16,6 +16,7 @@ enum TransactionManagerError: Error {
case internalInconsistency(PendingTransactionEntity)
case submitFailed(PendingTransactionEntity, errorCode: Int)
case shieldingEncodingFailed(PendingTransactionEntity, reason: String)
case cannotEncodeInternalTx(PendingTransactionEntity)
}
class PersistentTransactionManager: OutboundTransactionManager {
@ -40,7 +41,7 @@ class PersistentTransactionManager: OutboundTransactionManager {
func initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: Recipient,
memo: MemoBytes,
from accountIndex: Int
) throws -> PendingTransactionEntity {
@ -48,14 +49,14 @@ class PersistentTransactionManager: OutboundTransactionManager {
by: try repository.create(
PendingTransaction(
value: zatoshi,
toAddress: toAddress,
recipient: .address(recipient),
memo: memo,
account: accountIndex
)
)
) else {
throw TransactionManagerError.couldNotCreateSpend(
toAddress: toAddress,
recipient: recipient,
account: accountIndex,
zatoshi: zatoshi
)
@ -102,10 +103,18 @@ class PersistentTransactionManager: OutboundTransactionManager {
pendingTransaction: PendingTransactionEntity
) async throws -> PendingTransactionEntity {
do {
var toAddress: String?
switch (pendingTransaction.recipient) {
case .address(let addr):
toAddress = addr.stringEncoded
case .internalAccount(_):
throw TransactionManagerError.cannotEncodeInternalTx(pendingTransaction)
}
let encodedTransaction = try await self.encoder.createTransaction(
spendingKey: spendingKey,
zatoshi: pendingTransaction.value,
to: pendingTransaction.toAddress,
to: toAddress!,
memoBytes: try pendingTransaction.memo.intoMemoBytes(),
from: pendingTransaction.accountIndex
)

View File

@ -16,7 +16,7 @@ transactions through to completion.
protocol OutboundTransactionManager {
func initSpend(
zatoshi: Zatoshi,
toAddress: String,
recipient: Recipient,
memo: MemoBytes,
from accountIndex: Int
) throws -> PendingTransactionEntity

View File

@ -12,7 +12,7 @@ import XCTest
// swiftlint:disable force_try force_unwrapping implicitly_unwrapped_optional
class PendingTransactionRepositoryTests: XCTestCase {
let dbUrl = try! TestDbBuilder.pendingTransactionsDbURL()
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let recipient = SaplingAddress(validatedEncoding: "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6")
var pendingRepository: PendingTransactionRepository!
@ -51,7 +51,7 @@ class PendingTransactionRepositoryTests: XCTestCase {
XCTAssertEqual(transaction.accountIndex, expected.accountIndex)
XCTAssertEqual(transaction.value, expected.value)
XCTAssertEqual(transaction.toAddress, expected.toAddress)
XCTAssertEqual(transaction.recipient, expected.recipient)
}
func testFindById() {
@ -155,7 +155,7 @@ class PendingTransactionRepositoryTests: XCTestCase {
XCTAssertEqual(updatedTransaction.encodeAttempts, oldEncodeAttempts + 1)
XCTAssertEqual(updatedTransaction.submitAttempts, oldSubmitAttempts + 5)
XCTAssertEqual(updatedTransaction.toAddress, stored!.toAddress)
XCTAssertEqual(updatedTransaction.recipient, stored!.recipient)
}
func createAndStoreMockedTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
@ -175,6 +175,6 @@ class PendingTransactionRepositoryTests: XCTestCase {
}
private func mockTransaction(with value: Zatoshi = Zatoshi(1000)) -> PendingTransactionEntity {
PendingTransaction(value: value, toAddress: recipientAddress, memo: .empty(), account: 0)
PendingTransaction(value: value, recipient: .address(.sapling(recipient)), memo: .empty(), account: 0)
}
}

View File

@ -55,6 +55,10 @@ extension LightWalletServiceMockResponse {
}
class MockRustBackend: ZcashRustBackendWelding {
static func getAddressMetadata(_ address: String) -> ZcashLightClientKit.AddressMetadata? {
nil
}
static func clearUtxos(dbData: URL, address: ZcashLightClientKit.TransparentAddress, sinceHeight: ZcashLightClientKit.BlockHeight, networkType: ZcashLightClientKit.NetworkType) throws -> Int32 {
0
}