Skip to content

Commit

Permalink
Merge pull request #1517 from LukasKorba/import-ufvk-ffi-preview
Browse files Browse the repository at this point in the history
Multi-account and PCZT support
  • Loading branch information
str4d authored Jan 9, 2025
2 parents 2b3ff26 + 3bea79c commit f7029ed
Show file tree
Hide file tree
Showing 61 changed files with 1,917 additions and 1,404 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
permissions:
contents: read

runs-on: macos-13
runs-on: macos-14

steps:
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Added
- `DerivationTool.deriveArbitraryWalletKey`
- `DerivationTool.deriveArbitraryAccountKey`
- `DerivationTool.deriveUnifiedAddressFrom(ufvk)`
- `SDKSynchronizer.listAccounts` Returns a list of the accounts in the wallet.
- `SDKSynchronizer.importAccount` Imports a new account for unified full viewing key.
- `SDKSynchronizer.createPCZTFromProposal` Creates a partially-created (unsigned without proofs) transaction from the given proposal.
- `SDKSynchronizer.addProofsToPCZT` Adds proofs to the given PCZT
- `SDKSynchronizer.createTransactionFromPCZT` Takes a PCZT that has been separately proven and signed, finalizes it, and stores it in the wallet. Internally, this logic also submits and checks the newly stored and encoded transaction.

## Changed
- `zcashlc_propose_transfer`, `zcashlc_propose_transfer_from_uri` and `zcashlc_propose_shielding` no longer accpt a `use_zip317_fees` parameter; ZIP 317 standard fees are now always used and are not configurable.
- The SDK no longer assumes a default account. All business logic with instances of Zip32AccountIndex(<index>) has been refactored.
- `SDKSynchronizer.getAccountBalance -> AccountBalance?` into `SDKSynchronizer.getAccountsBalances -> [AccountUUID: AccountBalance]`

## Removed
- `SDKSynchronizer.sendToAddress`, deprecated in 2.1
- `SDKSynchronizer.shieldFunds`, deprecated in 2.1

## Checkpoints

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Electric-Coin-Company/zcash-light-client-ffi",
"state" : {
"revision" : "31a97a1478bc0354abdf208722b670f7fd3d9f8c",
"version" : "0.11.0"
"revision" : "11b0db058288b12ada9c5a95ed56f17f82d2868f",
"version" : "0.12.0"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ class GetAddressViewController: UIViewController {
let synchronizer = SDKSynchronizer.shared

Task { @MainActor in
guard let uAddress = try? await synchronizer.getUnifiedAddress(accountIndex: Zip32AccountIndex(0)) else {
guard let account = try? await synchronizer.listAccounts().first else {
return
}

guard let uAddress = try? await synchronizer.getUnifiedAddress(accountUUID: account.id) else {
unifiedAddressLabel.text = "could not derive UA"
tAddressLabel.text = "could not derive tAddress"
saplingAddress.text = "could not derive zAddress"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ class GetBalanceViewController: UIViewController {
var accountBalance: AccountBalance?
var rate: FiatCurrencyResult?

let accountIndex = Zip32AccountIndex(0)

override func viewDidLoad() {
super.viewDidLoad()
let synchronizer = AppDelegate.shared.sharedSynchronizer
self.title = "Account 0 Balance"

Task { @MainActor [weak self] in
guard let accountIndex = self?.accountIndex else { return }

self?.accountBalance = try? await synchronizer.getAccountsBalances()[accountIndex]
guard let account = try? await synchronizer.listAccounts().first else {
return
}

self?.accountBalance = try? await synchronizer.getAccountsBalances()[account.id]
self?.updateLabels()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,23 @@ class GetUTXOsViewController: UIViewController {

func updateUI() {
let synchronizer = SDKSynchronizer.shared

Task { @MainActor in
let tAddress = (try? await synchronizer.getTransparentAddress(accountIndex: accountIndex))?.stringEncoded ?? "no t-address found"
guard let account = try? await synchronizer.listAccounts().first else {
return
}

let tAddress = (try? await synchronizer.getTransparentAddress(accountUUID: account.id))?.stringEncoded ?? "no t-address found"

self.transparentAddressLabel.text = tAddress

// swiftlint:disable:next force_try
let balance = try! await AppDelegate.shared.sharedSynchronizer.getAccountsBalances()[accountIndex]?.unshielded ?? .zero
let balance = try! await AppDelegate.shared.sharedSynchronizer.getAccountsBalances()[account.id]?.unshielded ?? .zero

self.totalBalanceLabel.text = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: balance.amount))
self.verifiedBalanceLabel.text = NumberFormatter.zcashNumberFormatter.string(from: NSNumber(value: balance.amount))
}
}

@IBAction func shieldFunds(_ sender: Any) {
do {
let derivationTool = DerivationTool(networkType: kZcashNetwork.networkType)

let usk = try derivationTool.deriveUnifiedSpendingKey(seed: DemoAppConfig.defaultSeed, accountIndex: accountIndex)

KRProgressHUD.showMessage("🛡 Shielding 🛡")

Task { @MainActor in
let transaction = try await AppDelegate.shared.sharedSynchronizer.shieldFunds(
spendingKey: usk,
memo: try Memo(string: "shielding is fun!"),
shieldingThreshold: Zatoshi(10000)
)
KRProgressHUD.dismiss()
self.messageLabel.text = "funds shielded \(transaction)"
}
} catch {
self.messageLabel.text = "Shielding failed \(error)"
}
}
}

extension GetUTXOsViewController: UITextFieldDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ class SendViewController: UIViewController {
@IBOutlet weak var charactersLeftLabel: UILabel!

let characterLimit: Int = 512
let accountIndex = Zip32AccountIndex(0)

var wallet = Initializer.shared

// swiftlint:disable:next implicitly_unwrapped_optional
Expand All @@ -47,7 +45,9 @@ class SendViewController: UIViewController {
closureSynchronizer.prepare(
with: DemoAppConfig.defaultSeed,
walletBirthday: DemoAppConfig.defaultBirthdayHeight,
for: .existingWallet
for: .existingWallet,
name: "",
keySource: nil
) { result in
loggerProxy.debug("Prepare result: \(result)")
}
Expand Down Expand Up @@ -104,12 +104,17 @@ class SendViewController: UIViewController {
}

func updateBalance() async {
balanceLabel.text = format(
balance: (try? await synchronizer.getAccountsBalances()[accountIndex])?.saplingBalance.total() ?? .zero
)
verifiedBalanceLabel.text = format(
balance: (try? await synchronizer.getAccountsBalances()[accountIndex])?.saplingBalance.spendableValue ?? .zero
)
Task { @MainActor in
guard let account = try? await synchronizer.listAccounts().first else {
return
}
balanceLabel.text = format(
balance: (try? await synchronizer.getAccountsBalances()[account.id])?.saplingBalance.total() ?? .zero
)
verifiedBalanceLabel.text = format(
balance: (try? await synchronizer.getAccountsBalances()[account.id])?.saplingBalance.spendableValue ?? .zero
)
}
}

func format(balance: Zatoshi = Zatoshi()) -> String {
Expand All @@ -122,8 +127,12 @@ class SendViewController: UIViewController {

func maxFundsOn() {
Task { @MainActor in
guard let account = try? await synchronizer.listAccounts().first else {
return
}

let fee = Zatoshi(10000)
let max: Zatoshi = ((try? await synchronizer.getAccountsBalances()[accountIndex])?.saplingBalance.spendableValue ?? .zero) - fee
let max: Zatoshi = ((try? await synchronizer.getAccountsBalances()[account.id])?.saplingBalance.spendableValue ?? .zero) - fee
amountTextField.text = format(balance: max)
amountTextField.isEnabled = false
}
Expand All @@ -145,12 +154,18 @@ class SendViewController: UIViewController {
}

func isBalanceValid() async -> Bool {
let balance = (try? await synchronizer.getAccountsBalances()[accountIndex])?.saplingBalance.spendableValue ?? .zero
guard let account = try? await synchronizer.listAccounts().first else {
return false
}
let balance = (try? await synchronizer.getAccountsBalances()[account.id])?.saplingBalance.spendableValue ?? .zero
return balance > .zero
}

func isAmountValid() async -> Bool {
let balance = (try? await synchronizer.getAccountsBalances()[accountIndex])?.saplingBalance.spendableValue ?? .zero
guard let account = try? await synchronizer.listAccounts().first else {
return false
}
let balance = (try? await synchronizer.getAccountsBalances()[account.id])?.saplingBalance.spendableValue ?? .zero
guard
let value = amountTextField.text,
let amount = NumberFormatter.zcashNumberFormatter.number(from: value).flatMap({ Zatoshi($0.int64Value) }),
Expand Down Expand Up @@ -229,23 +244,14 @@ class SendViewController: UIViewController {
}

let derivationTool = DerivationTool(networkType: kZcashNetwork.networkType)
guard let spendingKey = try? derivationTool.deriveUnifiedSpendingKey(seed: DemoAppConfig.defaultSeed, accountIndex: accountIndex) else {
guard let spendingKey = try? derivationTool.deriveUnifiedSpendingKey(seed: DemoAppConfig.defaultSeed, accountIndex: Zip32AccountIndex(0)) else {
loggerProxy.error("NO SPENDING KEY")
return
}

KRProgressHUD.show()

do {
let pendingTransaction = try await synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: zec,
// swiftlint:disable:next force_try
toAddress: try! Recipient(recipient, network: kZcashNetwork.networkType),
// swiftlint:disable:next force_try
memo: try! self.memoField.text.asMemo()
)
loggerProxy.info("transaction created: \(pendingTransaction)")
KRProgressHUD.dismiss()
} catch {
loggerProxy.error("SEND FAILED: \(error)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ class SyncBlocksListViewController: UIViewController {
_ = try! await synchronizer.prepare(
with: synchronizerData.seed,
walletBirthday: synchronizerData.birthday,
for: .existingWallet
for: .existingWallet,
name: "",
keySource: nil
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ class SyncBlocksViewController: UIViewController {
_ = try await synchronizer.prepare(
with: DemoAppConfig.defaultSeed,
walletBirthday: DemoAppConfig.defaultBirthdayHeight,
for: .existingWallet
for: .existingWallet,
name: "",
keySource: nil
)
} catch {
loggerProxy.error(error.toZcashError().message)
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.24.2"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.3"),
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.11.0")
.package(url: "https://github.com/Electric-Coin-Company/zcash-light-client-ffi", exact: "0.12.0")
],
targets: [
.target(
Expand Down
14 changes: 13 additions & 1 deletion Sources/ZcashLightClientKit/Account/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct Zip32AccountIndex: Equatable, Codable, Hashable {

/// - Parameter index: the ZIP 32 account index, which must be less than ``1<<31``.
public init(_ index: UInt32) {
guard index < (1<<31) else {
guard index < (1 << 31) else {
fatalError("Account index must be less than 1<<31. Input value is \(index).")
}

Expand All @@ -34,3 +34,15 @@ public struct AccountId: Equatable, Codable, Hashable {
self.id = id
}
}

public struct AccountUUID: Equatable, Codable, Hashable, Identifiable {
public let id: [UInt8]

init(id: [UInt8]) {
guard id.count == 16 else {
fatalError("Account UUID must be 16 bytes long. Input value is \(id).")
}

self.id = id
}
}
12 changes: 6 additions & 6 deletions Sources/ZcashLightClientKit/Block/CompactBlockProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -837,16 +837,16 @@ extension CompactBlockProcessor {
}

extension CompactBlockProcessor {
func getUnifiedAddress(accountIndex: Zip32AccountIndex) async throws -> UnifiedAddress {
try await rustBackend.getCurrentAddress(accountIndex: accountIndex)
func getUnifiedAddress(accountUUID: AccountUUID) async throws -> UnifiedAddress {
try await rustBackend.getCurrentAddress(accountUUID: accountUUID)
}

func getSaplingAddress(accountIndex: Zip32AccountIndex) async throws -> SaplingAddress {
try await getUnifiedAddress(accountIndex: accountIndex).saplingReceiver()
func getSaplingAddress(accountUUID: AccountUUID) async throws -> SaplingAddress {
try await getUnifiedAddress(accountUUID: accountUUID).saplingReceiver()
}

func getTransparentAddress(accountIndex: Zip32AccountIndex) async throws -> TransparentAddress {
try await getUnifiedAddress(accountIndex: accountIndex).transparentReceiver()
func getTransparentAddress(accountUUID: AccountUUID) async throws -> TransparentAddress {
try await getUnifiedAddress(accountUUID: accountUUID).transparentReceiver()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ extension UTXOFetcherImpl: UTXOFetcher {
let accounts = try await rustBackend.listAccounts()

var tAddresses: [TransparentAddress] = []
for accountIndex in accounts {
tAddresses += try await rustBackend.listTransparentReceivers(accountIndex: accountIndex)
for account in accounts {
tAddresses += try await rustBackend.listTransparentReceivers(accountUUID: account.id)
}

var utxos: [UnspentTransactionOutputEntity] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@ extension SaplingParametersHandlerImpl: SaplingParametersHandler {
let accountBalances = try await rustBackend.getWalletSummary()?.accountBalances

for account in accounts {
let zip32AccountIndex = Zip32AccountIndex(account.index)

let totalSaplingBalance = accountBalances?[zip32AccountIndex]?.saplingBalance.total().amount ?? 0
let totalSaplingBalance = accountBalances?[account.id]?.saplingBalance.total().amount ?? 0

if totalSaplingBalance > 0 {
totalSaplingBalanceTrigger = true
break
}

let totalTransparentBalance = try await rustBackend.getTransparentBalance(accountIndex: zip32AccountIndex)
let totalTransparentBalance = try await rustBackend.getTransparentBalance(accountUUID: account.id)

if totalTransparentBalance > 0 {
totalTransparentBalanceTrigger = true
Expand Down
Loading

0 comments on commit f7029ed

Please sign in to comment.