Skip to content

Commit

Permalink
Expose APIs for working with transaction proposals
Browse files Browse the repository at this point in the history
Closes #1204.
  • Loading branch information
str4d committed Feb 21, 2024
1 parent 2ef0e00 commit 9afed2a
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 113 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this library will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

# Unreleased

## Added

### [#1204] Expose APIs for working with transaction proposals
New `Synchronizer` APIs that enable constructing a proposal for transferring or
shielding funds, and then creating transactions from a proposal. The intermediate
proposal can be used to determine the required fee, before committing to producing
transactions.

# 2.0.10 - 2024-02-12

## Added
Expand Down
18 changes: 18 additions & 0 deletions Sources/ZcashLightClientKit/Model/Proposal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Proposal.swift
//
//
// Created by Jack Grigg on 20/02/2024.
//

import Foundation

/// A data structure that describes a series of transactions to be created.
public struct Proposal {
let inner: FfiProposal

/// Returns the total fee to be paid across all proposed transactions, in zatoshis.
public func totalFeeRequired() -> Zatoshi {
return Zatoshi(Int64(inner.balance.feeRequired))
}
}
43 changes: 42 additions & 1 deletion Sources/ZcashLightClientKit/Synchronizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,48 @@ public protocol Synchronizer: AnyObject {
/// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used.
/// - Returns the address or nil if account index is incorrect
func getTransparentAddress(accountIndex: Int) async throws -> TransparentAddress


/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions. Use `nil` when sending to transparent receivers otherwise the function will throw an error.
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: Recipient,
amount: Zatoshi,
memo: Memo?
) async throws -> Proposal

/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions.
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memo: Memo
) async throws -> Proposal

/// Creates the transactions in the given proposal
///
/// - Parameter proposal: the proposal to create.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the notes that will be spent.
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> [ZcashTransaction.Overview]

/// Sends zatoshi.
/// - Parameter spendingKey: the `UnifiedSpendingKey` that allows spends to occur.
/// - Parameter zatoshi: the amount to send in Zatoshi.
Expand Down
88 changes: 78 additions & 10 deletions Sources/ZcashLightClientKit/Synchronizer/SDKSynchronizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,60 @@ public class SDKSynchronizer: Synchronizer {

// MARK: Synchronizer methods

public func proposeTransfer(accountIndex: Int, recipient: Recipient, amount: Zatoshi, memo: Memo?) async throws -> Proposal {
try throwIfUnprepared()

if case Recipient.transparent = recipient, memo != nil {
throw ZcashError.synchronizerSendMemoToTransparentAddress
}

let proposal = try await transactionEncoder.proposeTransfer(
accountIndex: accountIndex,
recipient: recipient.stringEncoded,
amount: amount,
memoBytes: memo?.asMemoBytes()
)

return proposal
}

public func proposeShielding(accountIndex: Int, shieldingThreshold: Zatoshi, memo: Memo) async throws -> Proposal {
try throwIfUnprepared()

let proposal = try await transactionEncoder.proposeShielding(
accountIndex: accountIndex,
shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes()
)

return proposal
}

public func createProposedTransactions(proposal: Proposal, spendingKey: UnifiedSpendingKey) async throws -> [ZcashTransaction.Overview] {
try throwIfUnprepared()

try await SaplingParameterDownloader.downloadParamsIfnotPresent(
spendURL: initializer.spendParamsURL,
spendSourceURL: initializer.saplingParamsSourceURL.spendParamFileURL,
outputURL: initializer.outputParamsURL,
outputSourceURL: initializer.saplingParamsSourceURL.outputParamFileURL,
logger: logger
)

let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)

for transaction in transactions {
let encodedTransaction = try transaction.encodedTransaction()

try await transactionEncoder.submit(transaction: encodedTransaction)
}

return transactions
}

public func sendToAddress(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
Expand Down Expand Up @@ -312,13 +366,20 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerShieldFundsInsuficientTransparentFunds
}

let transaction = try await transactionEncoder.createShieldingTransaction(
spendingKey: spendingKey,
let proposal = try await transactionEncoder.proposeShielding(
accountIndex: Int(spendingKey.account),
shieldingThreshold: shieldingThreshold,
memoBytes: memo.asMemoBytes(),
from: Int(spendingKey.account)
memoBytes: memo.asMemoBytes()
)

let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)

assert(transactions.count == 1, "Rust backend doesn't produce multiple transactions yet")
let transaction = transactions[0]

let encodedTx = try transaction.encodedTransaction()

try await transactionEncoder.submit(transaction: encodedTx)
Expand All @@ -339,14 +400,21 @@ public class SDKSynchronizer: Synchronizer {
throw ZcashError.synchronizerSendMemoToTransparentAddress
}

let transaction = try await transactionEncoder.createTransaction(
spendingKey: spendingKey,
zatoshi: zatoshi,
to: recipient.stringEncoded,
memoBytes: memo?.asMemoBytes(),
from: Int(spendingKey.account)
let proposal = try await transactionEncoder.proposeTransfer(
accountIndex: Int(spendingKey.account),
recipient: recipient.stringEncoded,
amount: zatoshi,
memoBytes: memo?.asMemoBytes()
)

let transactions = try await transactionEncoder.createProposedTransactions(
proposal: proposal,
spendingKey: spendingKey
)

assert(transactions.count == 1, "Rust backend doesn't produce multiple transactions yet")
let transaction = transactions[0]

let encodedTransaction = try transaction.encodedTransaction()

try await transactionEncoder.submit(transaction: encodedTransaction)
Expand Down
71 changes: 38 additions & 33 deletions Sources/ZcashLightClientKit/Transaction/TransactionEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,49 @@ public enum TransactionEncoderError: Error {
}

protocol TransactionEncoder {
/// 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).
/// Creates a proposal for transferring funds to the given recipient.
///
/// - Parameters:
/// - Parameter spendingKey: a `UnifiedSpendingKey` containing the spending key
/// - Parameter zatoshi: the amount to send in `Zatoshi`
/// - Parameter to: string containing the recipient address
/// - Parameter MemoBytes: string containing the memo (optional)
/// - Parameter accountIndex: index of the account that will be used to send the funds
/// - Parameter accountIndex: the account from which to transfer funds.
/// - Parameter recipient: string containing the recipient's address.
/// - Parameter amount: the amount to send in Zatoshi.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions. Use `nil` when sending to transparent receivers otherwise the function will throw an error.
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeTransfer(
accountIndex: Int,
recipient: String,
amount: Zatoshi,
memoBytes: MemoBytes?
) async throws -> Proposal

/// Creates a proposal for shielding any transparent funds received by the given account.
///
/// - Parameter accountIndex: the account for which to shield funds.
/// - Parameter memo: an optional memo to include as part of the proposal's transactions.
///
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func proposeShielding(
accountIndex: Int,
shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?
) async throws -> Proposal

/// Creates the transactions in the given proposal
///
/// - Parameter proposal: the proposal to create.
/// - Parameter spendingKey: the `UnifiedSpendingKey` associated with the notes that will be spent.
/// - Throws:
/// - `walletTransEncoderCreateTransactionMissingSaplingParams` if the sapling parameters aren't downloaded.
/// - Some `ZcashError.rust*` if the creation of transaction fails.
func createTransaction(
spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to address: String,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview

/// Creates a transaction that will attempt to shield transparent funds that are present on the blocks cache .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).
///
/// - Parameters:
/// - Parameter spendingKey: `UnifiedSpendingKey` to spend the UTXOs
/// - Parameter memoBytes: containing the memo (optional)
/// - Parameter accountIndex: index of the account that will be used to send the funds
/// - Throws:
/// - `walletTransEncoderShieldFundsMissingSaplingParams` if the sapling parameters aren't downloaded.
/// - Some `ZcashError.rust*` if the creation of transaction fails.
func createShieldingTransaction(
spendingKey: UnifiedSpendingKey,
shieldingThreshold: Zatoshi,
memoBytes: MemoBytes?,
from accountIndex: Int
) async throws -> ZcashTransaction.Overview
/// If `prepare()` hasn't already been called since creating of synchronizer instance or since the last wipe then this method throws
/// `SynchronizerErrors.notPrepared`.
func createProposedTransactions(
proposal: Proposal,
spendingKey: UnifiedSpendingKey
) async throws -> [ZcashTransaction.Overview]

/// submits a transaction to the Zcash peer-to-peer network.
/// - Parameter transaction: a transaction overview
Expand Down
Loading

0 comments on commit 9afed2a

Please sign in to comment.