Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-account and PCZT support #1517

Open
wants to merge 33 commits into
base: 1514-Finish-multi-account-support
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a09f2b1
import-ufvk-ffi
LukasKorba Dec 3, 2024
3bfe5b3
ImportAccount UFVK
LukasKorba Dec 4, 2024
06181fb
AccountUUID refactor - phase 1
LukasKorba Dec 4, 2024
7a6bd72
AccountUUID refactor - phase 2
LukasKorba Dec 4, 2024
27af6f1
PR Comments addressed and code cleaned up
LukasKorba Dec 5, 2024
92bd2e6
Update swift.yml
LukasKorba Dec 5, 2024
05002a8
Generated mocks
LukasKorba Dec 5, 2024
bb03333
Performance, Network and Dark side tests
LukasKorba Dec 5, 2024
5179f4e
getAccount for UUID
LukasKorba Dec 6, 2024
e48cd60
Account conformace
LukasKorba Dec 6, 2024
df1b319
hd_account_index wrapped in Zip32AccountIndex
LukasKorba Dec 9, 2024
89773c6
account_id -> account_uuid refactor
LukasKorba Dec 9, 2024
f69aa19
Public importAccount method
LukasKorba Dec 9, 2024
8c099e3
seedFingerprint and zip32AccountIndex parameters in importAccount added
LukasKorba Dec 9, 2024
dcf6cd9
Codable UnifiedAddress conformance
LukasKorba Dec 9, 2024
7782d6f
Optional account_name
LukasKorba Dec 9, 2024
32bb9ba
FFI bump
LukasKorba Dec 10, 2024
08e5a73
DerivationTool.deriveUnifiedAddressFrom(ufvk)
LukasKorba Dec 10, 2024
6bab337
PCZT methods
LukasKorba Dec 10, 2024
0fba7f2
PCZT methods updated
LukasKorba Dec 10, 2024
5943d63
FFI bump
LukasKorba Dec 10, 2024
30af9c6
PCZT API updated
LukasKorba Dec 10, 2024
dd587ac
from_account_id reverted
LukasKorba Dec 10, 2024
26ad508
FFI bumped
LukasKorba Dec 10, 2024
632626a
Reset of synchronizer alias
LukasKorba Dec 11, 2024
ee32938
FFI bump
LukasKorba Dec 11, 2024
f499898
Create and submit PCZT
LukasKorba Dec 12, 2024
fb38ac8
Alias checker disconnected
LukasKorba Dec 12, 2024
da00a69
Pczt typealias and clean up
LukasKorba Dec 13, 2024
f16d098
Code cleanup
LukasKorba Dec 13, 2024
ce667eb
FFI 0.12.0 bump
LukasKorba Dec 17, 2024
a8b1524
Review comments addressed
LukasKorba Jan 7, 2025
3bea79c
Comments updated
LukasKorba Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ jobs:
permissions:
contents: read

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

steps:
- uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
timeout-minutes: 1
- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_15.0.1.app/Contents/Developer'
run: sudo xcode-select -s '/Applications/Xcode_16.0.app/Contents/Developer'
- name: Build ZcashLightClientKit Swift Package
timeout-minutes: 15
run: swift build -v
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"
LukasKorba marked this conversation as resolved.
Show resolved Hide resolved
}
}
],
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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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 {
nuttycom marked this conversation as resolved.
Show resolved Hide resolved
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
Loading