WIP - shield funds
This commit is contained in:
parent
6ec126d471
commit
45fa30838f
|
@ -121,6 +121,18 @@ class UnspentTransactionOutputSQLDAO: UnspentTransactionOutputRepository {
|
|||
return allTxs
|
||||
}
|
||||
}
|
||||
|
||||
func balance(address: String) throws -> Int {
|
||||
|
||||
guard let sum = try dbProvider.connection().scalar(
|
||||
table.select(TableColumns.valueZat.sum)
|
||||
.filter(TableColumns.address == address)
|
||||
) else {
|
||||
throw StorageError.operationFailed
|
||||
}
|
||||
return sum
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class UTXORepositoryBuilder {
|
||||
|
|
|
@ -11,6 +11,8 @@ protocol UnspentTransactionOutputRepository {
|
|||
|
||||
func getAll(address: String?) throws -> [UnspentTransactionOutputEntity]
|
||||
|
||||
func balance(address: String) throws -> Int
|
||||
|
||||
func store(utxos: [UnspentTransactionOutputEntity]) throws
|
||||
|
||||
func clearAll(address: String?) throws
|
||||
|
|
|
@ -225,7 +225,6 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
|||
UInt(outputParamsPath.lengthOfBytes(using: .utf8)))
|
||||
}
|
||||
|
||||
|
||||
static func shieldFunds(dbCache: URL, dbData: URL, account: Int32, tsk: String, extsk: String, memo: String?, spendParamsPath: String, outputParamsPath: String) -> Int64 {
|
||||
let dbData = dbData.osStr()
|
||||
let dbCache = dbCache.osStr()
|
||||
|
@ -356,6 +355,22 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
|||
return sk
|
||||
}
|
||||
|
||||
static func deriveTransparentAddressFromSecretKey(_ tsk: String) throws -> String? {
|
||||
|
||||
guard !tsk.containsCStringNullBytesBeforeStringEnding() else {
|
||||
throw RustWeldingError.malformedStringInput
|
||||
}
|
||||
guard let tAddrCStr = zcashlc_derive_transparent_address_from_secret_key([CChar](tsk.utf8CString)) else {
|
||||
if let error = lastError() {
|
||||
throw error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
let tAddr = String(validatingUTF8: tAddrCStr)
|
||||
|
||||
return tAddr
|
||||
}
|
||||
|
||||
static func consensusBranchIdFor(height: Int32) throws -> Int32 {
|
||||
let branchId = zcashlc_branch_id_for_height(height)
|
||||
|
||||
|
@ -365,6 +380,7 @@ class ZcashRustBackend: ZcashRustBackendWelding {
|
|||
|
||||
return branchId
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension ZcashRustBackend {
|
||||
|
|
|
@ -255,9 +255,21 @@ public protocol ZcashRustBackendWelding {
|
|||
*/
|
||||
static func deriveTransparentAddressFromSeed(seed: [UInt8]) throws -> String?
|
||||
|
||||
|
||||
/**
|
||||
Derives a transparent secret key from Seed
|
||||
- Parameter seed: an array of bytes containing the seed
|
||||
- Returns: an optional String containing the transparent secret (private) key
|
||||
*/
|
||||
static func deriveTransparentPrivateKeyFromSeed(seed: [UInt8]) throws -> String?
|
||||
|
||||
/**
|
||||
Derives a transparent address from a secret key
|
||||
- Parameter tsk: a hex string containing the Secret Key
|
||||
- Returns: an optional String containing the transparent address.
|
||||
*/
|
||||
|
||||
static func deriveTransparentAddressFromSecretKey(_ tsk: String) throws -> String?
|
||||
|
||||
/**
|
||||
Gets the consensus branch id for the given height
|
||||
- Parameter height: the height you what to know the branch id for
|
||||
|
|
|
@ -24,6 +24,12 @@ public enum SynchronizerError: Error {
|
|||
case parameterMissing(underlyingError: Error)
|
||||
}
|
||||
|
||||
public enum ShieldFundsError: Error {
|
||||
case noUTXOFound
|
||||
case insuficientTransparentFunds
|
||||
case shieldingFailed(underlyingError: Error)
|
||||
}
|
||||
|
||||
/**
|
||||
Primary interface for interacting with the SDK. Defines the contract that specific
|
||||
implementations like SdkSynchronizer fulfill.
|
||||
|
@ -73,6 +79,16 @@ public protocol Synchronizer {
|
|||
*/
|
||||
func sendToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> Void)
|
||||
|
||||
/**
|
||||
Sends zatoshi.
|
||||
- Parameter spendingKey: the key that allows spends to occur.
|
||||
- Parameter transparentSecretKey: the key that allows to spend transaprent funds
|
||||
- Parameter zatoshi: the amount of zatoshi to send.
|
||||
- Parameter memo: the optional memo to include as part of the transaction.
|
||||
- Parameter accountIndex: the optional account id to use. By default, the first account is used.
|
||||
*/
|
||||
func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> Void)
|
||||
|
||||
/**
|
||||
Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only
|
||||
an option if the transaction has not yet been submitted to the server.
|
||||
|
|
|
@ -68,26 +68,27 @@ public protocol KeyDeriving {
|
|||
*/
|
||||
func deriveShieldedAddress(viewingKey: String) throws -> String
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Validates the given viewing key
|
||||
- Throws DerivationError when it's invalid
|
||||
*/
|
||||
func validateViewingKey(viewingKey: String) throws
|
||||
|
||||
|
||||
// WIP probably shouldn't be used just yet. Why?
|
||||
// - because we need the private key associated with this seed and this function doesn't return it.
|
||||
// - the underlying implementation needs to be split out into a few lower-level calls
|
||||
func deriveTransparentAddress(seed: [UInt8]) throws -> String
|
||||
|
||||
|
||||
/**
|
||||
Derives a SecretKey to spend transparent funds from the given seed
|
||||
*/
|
||||
func deriveTransparentPrivateKey(seed: [UInt8]) throws -> String
|
||||
|
||||
/**
|
||||
Derives a transparent address from the given transparent Secret Key
|
||||
*/
|
||||
func deriveTransparentAddressFromPrivateKey(_ tsk: String) throws -> String
|
||||
|
||||
}
|
||||
|
||||
public enum KeyDerivationErrors: Error {
|
||||
|
@ -227,7 +228,7 @@ public class DerivationTool: KeyDeriving {
|
|||
|
||||
public func validateViewingKey(viewingKey: String) throws {
|
||||
// TODO
|
||||
throw KeyDerivationErrors.unableToDerive
|
||||
// throw KeyDerivationErrors.unableToDerive
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,4 +276,14 @@ extension DerivationTool: KeyValidation {
|
|||
}
|
||||
|
||||
|
||||
public func deriveTransparentAddressFromPrivateKey(_ tsk: String) throws -> String {
|
||||
do {
|
||||
guard let tAddr = try rustwelding.deriveTransparentAddressFromSecretKey(tsk) else {
|
||||
throw KeyDerivationErrors.unableToDerive
|
||||
}
|
||||
return tAddr
|
||||
} catch {
|
||||
throw KeyDerivationErrors.derivationError(underlyingError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ enum TransactionManagerError: Error {
|
|||
case cancelled(tx: PendingTransactionEntity)
|
||||
case internalInconsistency(tx: PendingTransactionEntity)
|
||||
case submitFailed(tx: PendingTransactionEntity, errorCode: Int)
|
||||
case shieldingEncodingFailed(tx: PendingTransactionEntity, reason: String)
|
||||
}
|
||||
|
||||
class PersistentTransactionManager: OutboundTransactionManager {
|
||||
|
@ -39,6 +40,45 @@ class PersistentTransactionManager: OutboundTransactionManager {
|
|||
return insertedTx
|
||||
}
|
||||
|
||||
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let derivationTool = DerivationTool()
|
||||
guard let vk = try? derivationTool.deriveViewingKey(spendingKey: spendingKey),
|
||||
let zAddr = try? derivationTool.deriveShieldedAddress(viewingKey: vk) else {
|
||||
result(.failure(TransactionManagerError.shieldingEncodingFailed(tx: pendingTransaction, reason: "There was an error Deriving your keys")))
|
||||
return
|
||||
}
|
||||
|
||||
guard pendingTransaction.toAddress == zAddr else {
|
||||
result(.failure(TransactionManagerError.shieldingEncodingFailed(tx: pendingTransaction, reason: "the recipient address does not match your derived shielded address. Shielding transactions addresses must match the ones derived from your keys. This is a serious error. We are not letting you encode this shielding transaction because it can lead to loss of funds")))
|
||||
return
|
||||
}
|
||||
do {
|
||||
let encodedTransaction = try self.encoder.createShieldingTransaction(spendingKey: spendingKey, tSecretKey: tsk, memo: pendingTransaction.memo?.asZcashTransactionMemo(), from: pendingTransaction.accountIndex)
|
||||
let transaction = try self.encoder.expandEncodedTransaction(encodedTransaction)
|
||||
|
||||
var pending = pendingTransaction
|
||||
pending.encodeAttempts = pending.encodeAttempts + 1
|
||||
pending.raw = encodedTransaction.raw
|
||||
pending.rawTransactionId = encodedTransaction.transactionId
|
||||
pending.expiryHeight = transaction.expiryHeight ?? BlockHeight.empty()
|
||||
pending.minedHeight = transaction.minedHeight ?? BlockHeight.empty()
|
||||
try self.repository.update(pending)
|
||||
result(.success(pending))
|
||||
} catch StorageError.updateFailed {
|
||||
DispatchQueue.main.async {
|
||||
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
result(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
||||
|
||||
queue.async { [weak self] in
|
||||
|
@ -60,6 +100,13 @@ class PersistentTransactionManager: OutboundTransactionManager {
|
|||
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
|
||||
}
|
||||
} catch {
|
||||
do {
|
||||
try self.updateOnFailure(tx: pendingTransaction, error: error)
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
result(.failure(TransactionManagerError.updateFailed(tx: pendingTransaction)))
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
result(.failure(error))
|
||||
}
|
||||
|
|
|
@ -52,6 +52,39 @@ protocol TransactionEncoder {
|
|||
*/
|
||||
func createTransaction(spendingKey: String, zatoshi: Int, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock)
|
||||
|
||||
/**
|
||||
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 memo: string containing the memo (optional)
|
||||
- Parameter accountIndex: index of the account that will be used to send the funds
|
||||
|
||||
- Throws: a TransactionEncoderError
|
||||
*/
|
||||
func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction
|
||||
|
||||
/**
|
||||
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).
|
||||
Non-Blocking
|
||||
|
||||
- Parameters:
|
||||
- Parameter spendingKey: a string containing the spending key
|
||||
- Parameter tSecretKey: transparent secret key to spend the UTXOs
|
||||
- Parameter memo: string containing the memo (optional)
|
||||
- Parameter accountIndex: index of the account that will be used to send the funds
|
||||
|
||||
- Returns: a TransactionEncoderResultBlock
|
||||
*/
|
||||
|
||||
func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock)
|
||||
|
||||
/**
|
||||
Fetch the Transaction Entity from the encoded representation
|
||||
- Parameter encodedTransaction: The encoded transaction to expand
|
||||
|
|
|
@ -16,6 +16,8 @@ import Foundation
|
|||
protocol OutboundTransactionManager {
|
||||
func initSpend(zatoshi: Int, toAddress: String, memo: String?, from accountIndex: Int) throws -> PendingTransactionEntity
|
||||
|
||||
func encodeShieldingTransaction(spendingKey: String, tsk: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
|
||||
|
||||
func encode(spendingKey: String, pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
|
||||
|
||||
func submit(pendingTransaction: PendingTransactionEntity, result: @escaping (Result<PendingTransactionEntity, Error>) -> Void)
|
||||
|
|
|
@ -15,15 +15,18 @@ class WalletTransactionEncoder: TransactionEncoder {
|
|||
private var outputParamsURL: URL
|
||||
private var spendParamsURL: URL
|
||||
private var dataDbURL: URL
|
||||
private var cacheDbURL: URL
|
||||
|
||||
init(rust: ZcashRustBackendWelding.Type,
|
||||
dataDb: URL,
|
||||
cacheDb: URL,
|
||||
repository: TransactionRepository,
|
||||
outputParams: URL,
|
||||
spendParams: URL) {
|
||||
|
||||
self.rustBackend = rust
|
||||
self.dataDbURL = dataDb
|
||||
self.cacheDbURL = cacheDb
|
||||
self.repository = repository
|
||||
self.outputParamsURL = outputParams
|
||||
self.spendParamsURL = spendParams
|
||||
|
@ -34,6 +37,7 @@ class WalletTransactionEncoder: TransactionEncoder {
|
|||
convenience init(initializer: Initializer) {
|
||||
self.init(rust: initializer.rustBackend,
|
||||
dataDb: initializer.dataDbURL,
|
||||
cacheDb: initializer.cacheDbURL,
|
||||
repository: initializer.transactionRepository,
|
||||
outputParams: initializer.outputParamsURL,
|
||||
spendParams: initializer.spendParamsURL)
|
||||
|
@ -99,6 +103,51 @@ class WalletTransactionEncoder: TransactionEncoder {
|
|||
return Int(txId)
|
||||
}
|
||||
|
||||
func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction {
|
||||
let txId = try createShieldingSpend(spendingKey: spendingKey, tsk: tSecretKey, memo: memo, accountIndex: accountIndex)
|
||||
|
||||
do {
|
||||
let transaction = try repository.findBy(id: txId)
|
||||
|
||||
guard let tx = transaction else {
|
||||
throw TransactionEncoderError.notFound(transactionId: txId)
|
||||
}
|
||||
|
||||
LoggerProxy.debug("sentTransaction id: \(txId)")
|
||||
return EncodedTransaction(transactionId: tx.transactionId , raw: tx.raw)
|
||||
} catch {
|
||||
throw TransactionEncoderError.notFound(transactionId: txId)
|
||||
}
|
||||
}
|
||||
|
||||
func createShieldingTransaction(spendingKey: String, tSecretKey: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) {
|
||||
queue.async {
|
||||
result(.failure(RustWeldingError.genericError(message: "not implemented")))
|
||||
}
|
||||
}
|
||||
|
||||
func createShieldingSpend(spendingKey: String, tsk: String, memo: String?, accountIndex: Int) throws -> Int {
|
||||
guard ensureParams(spend: self.spendParamsURL, output: self.spendParamsURL) else {
|
||||
throw TransactionEncoderError.missingParams
|
||||
}
|
||||
|
||||
let txId = rustBackend.shieldFunds(dbCache: self.cacheDbURL,
|
||||
dbData: self.dataDbURL,
|
||||
account: Int32(accountIndex),
|
||||
tsk: tsk,
|
||||
extsk: spendingKey,
|
||||
memo: memo,
|
||||
spendParamsPath: self.spendParamsURL.path,
|
||||
outputParamsPath: self.outputParamsURL.path)
|
||||
|
||||
guard txId > 0 else {
|
||||
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed")
|
||||
}
|
||||
|
||||
return Int(txId)
|
||||
|
||||
}
|
||||
|
||||
func ensureParams(spend: URL, output: URL) -> Bool {
|
||||
|
||||
let readableSpend = FileManager.default.isReadableFile(atPath: spend.path)
|
||||
|
|
|
@ -79,6 +79,8 @@ public class SDKSynchronizer: Synchronizer {
|
|||
public static let error = "SDKSynchronizer.error"
|
||||
}
|
||||
|
||||
private static let shieldingThreshold: Int = 10000
|
||||
|
||||
public private(set) var status: Status {
|
||||
didSet {
|
||||
notify(status: status)
|
||||
|
@ -346,6 +348,52 @@ public class SDKSynchronizer: Synchronizer {
|
|||
}
|
||||
}
|
||||
|
||||
public func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
||||
|
||||
// let's see if there are funds to shield
|
||||
let derivationTool = DerivationTool.default
|
||||
|
||||
do {
|
||||
let tAddr = try derivationTool.deriveTransparentAddressFromPrivateKey(transparentSecretKey)
|
||||
let tBalance = try utxoRepository.balance(address: tAddr)
|
||||
|
||||
guard tBalance > Self.shieldingThreshold else {
|
||||
resultBlock(.failure(ShieldFundsError.insuficientTransparentFunds))
|
||||
return
|
||||
}
|
||||
let vk = try derivationTool.deriveViewingKey(spendingKey: spendingKey)
|
||||
let zAddr = try derivationTool.deriveShieldedAddress(viewingKey: vk)
|
||||
|
||||
let shieldingSpend = try transactionManager.initSpend(zatoshi: tBalance, toAddress: zAddr, memo: memo, from: 0)
|
||||
|
||||
transactionManager.encodeShieldingTransaction(spendingKey: spendingKey, tsk: transparentSecretKey, pendingTransaction: shieldingSpend) {[weak self] (result) in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
|
||||
case .success(let tx):
|
||||
self.transactionManager.submit(pendingTransaction: tx) { (submitResult) in
|
||||
switch submitResult {
|
||||
case .success(let submittedTx):
|
||||
resultBlock(.success(submittedTx))
|
||||
case .failure(let submissionError):
|
||||
DispatchQueue.main.async {
|
||||
resultBlock(.failure(submissionError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
resultBlock(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
resultBlock(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createToAddress(spendingKey: String, zatoshi: Int64, toAddress: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
|
||||
|
||||
do {
|
||||
|
|
Loading…
Reference in New Issue