diff --git a/modules/Package.swift b/modules/Package.swift index df9069990..e5cb896e7 100644 --- a/modules/Package.swift +++ b/modules/Package.swift @@ -67,7 +67,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-url-routing", from: "0.6.0"), .package(url: "https://github.com/zcash-hackworks/MnemonicSwift", from: "2.2.4"), - .package(url: "https://github.com/zcash/ZcashLightClientKit", from: "2.0.10"), + .package(url: "https://github.com/zcash/ZcashLightClientKit", branch: "1204-expose-proposals"), .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.17.0") ], targets: [ diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift index bb18b54c7..99581a3fc 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift @@ -43,4 +43,9 @@ public struct SDKSynchronizerClient { public var wipe: () -> AnyPublisher? public var switchToEndpoint: (LightWalletEndpoint) async throws -> Void + + // Proposals + public var proposeTransfer: (Int, Recipient, Zatoshi, Memo?) async throws -> Proposal + public let createProposedTransactions: (Proposal, UnifiedSpendingKey) async throws -> AsyncThrowingStream + public var proposeShielding: (Int, Zatoshi, Memo, TransparentAddress?) async throws -> Proposal? } diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift index e68987e22..547c12362 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift @@ -116,6 +116,28 @@ extension SDKSynchronizerClient: DependencyKey { wipe: { synchronizer.wipe() }, switchToEndpoint: { endpoint in try await synchronizer.switchTo(endpoint: endpoint) + }, + proposeTransfer: { accountIndex, recipient, amount, memo in + try await synchronizer.proposeTransfer( + accountIndex: accountIndex, + recipient: recipient, + amount: amount, + memo: memo + ) + }, + createProposedTransactions: { proposal, spendingKey in + try await synchronizer.createProposedTransactions( + proposal: proposal, + spendingKey: spendingKey + ) + }, + proposeShielding: { accountIndex, shieldingThreshold, memo, transparentReceiver in + try await synchronizer.proposeShielding( + accountIndex: accountIndex, + shieldingThreshold: shieldingThreshold, + memo: memo, + transparentReceiver: transparentReceiver + ) } ) } diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift index 88f467bb4..683cf9da0 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift @@ -30,7 +30,10 @@ extension SDKSynchronizerClient: TestDependencyKey { sendTransaction: XCTUnimplemented("\(Self.self).sendTransaction", placeholder: .placeholder()), shieldFunds: XCTUnimplemented("\(Self.self).shieldFunds", placeholder: .placeholder()), wipe: XCTUnimplemented("\(Self.self).wipe"), - switchToEndpoint: XCTUnimplemented("\(Self.self).switchToEndpoint") + switchToEndpoint: XCTUnimplemented("\(Self.self).switchToEndpoint"), + proposeTransfer: XCTUnimplemented("\(Self.self).proposeTransfer", placeholder: .testOnlyFakeProposal(totalFee: 0)), + createProposedTransactions: XCTUnimplemented("\(Self.self).createProposedTransactions", placeholder: .never), + proposeShielding: XCTUnimplemented("\(Self.self).proposeShielding", placeholder: nil) ) } @@ -52,7 +55,10 @@ extension SDKSynchronizerClient { sendTransaction: { _, _, _, _ in return .placeholder() }, shieldFunds: { _, _, _ in return .placeholder() }, wipe: { Empty().eraseToAnyPublisher() }, - switchToEndpoint: { _ in } + switchToEndpoint: { _ in }, + proposeTransfer: { _, _, _, _ in .testOnlyFakeProposal(totalFee: 0) }, + createProposedTransactions: { _, _ in .never }, + proposeShielding: { _, _, _, _ in nil } ) public static let mock = Self.mocked() @@ -172,7 +178,13 @@ extension SDKSynchronizerClient { ) }, wipe: @escaping () -> AnyPublisher? = { Fail(error: "Error").eraseToAnyPublisher() }, - switchToEndpoint: @escaping (LightWalletEndpoint) async throws -> Void = { _ in } + switchToEndpoint: @escaping (LightWalletEndpoint) async throws -> Void = { _ in }, + proposeTransfer: + @escaping (Int, Recipient, Zatoshi, Memo?) async throws -> Proposal = { _, _, _, _ in .testOnlyFakeProposal(totalFee: 0) }, + createProposedTransactions: + @escaping (Proposal, UnifiedSpendingKey) async throws -> AsyncThrowingStream = { _, _ in .never }, + proposeShielding: + @escaping (Int, Zatoshi, Memo, TransparentAddress?) async throws -> Proposal? = { _, _, _, _ in nil } ) -> SDKSynchronizerClient { SDKSynchronizerClient( stateStream: stateStream, @@ -191,7 +203,10 @@ extension SDKSynchronizerClient { sendTransaction: sendTransaction, shieldFunds: shieldFunds, wipe: wipe, - switchToEndpoint: switchToEndpoint + switchToEndpoint: switchToEndpoint, + proposeTransfer: proposeTransfer, + createProposedTransactions: createProposedTransactions, + proposeShielding: proposeShielding ) } } diff --git a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentInterface.swift b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentInterface.swift index f91c64aad..2005fcf5e 100644 --- a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentInterface.swift +++ b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentInterface.swift @@ -126,5 +126,6 @@ public struct ZcashSDKEnvironment { public let network: ZcashNetwork public let requiredTransactionConfirmations: Int public let sdkVersion: String + public let shieldingThreshold: Zatoshi public let tokenName: String } diff --git a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentLiveKey.swift b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentLiveKey.swift index 809a3206f..9d1b68473 100644 --- a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentLiveKey.swift +++ b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentLiveKey.swift @@ -45,6 +45,7 @@ extension ZcashSDKEnvironment { network: network, requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations, sdkVersion: "0.18.1-beta", + shieldingThreshold: Zatoshi(100_000), tokenName: network.networkType == .testnet ? "TAZ" : "ZEC" ) } diff --git a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentTestKey.swift b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentTestKey.swift index a180fbba1..d43c0984e 100644 --- a/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentTestKey.swift +++ b/modules/Sources/Dependencies/ZcashSDKEnvironment/ZcashSDKEnvironmentTestKey.swift @@ -27,6 +27,7 @@ extension ZcashSDKEnvironment: TestDependencyKey { network: ZcashNetworkBuilder.network(for: .testnet), requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations, sdkVersion: "0.18.1-beta", + shieldingThreshold: .zero, tokenName: "TAZ" ) } diff --git a/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownStore.swift b/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownStore.swift index 939359b36..8bc4ade35 100644 --- a/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownStore.swift +++ b/modules/Sources/Features/BalanceBreakdown/BalanceBreakdownStore.swift @@ -83,10 +83,11 @@ public struct BalanceBreakdownReducer: Reducer { case restoreWalletTask case restoreWalletValue(Bool) case shieldFunds - case shieldFundsSuccess(TransactionState) + case shieldFundsSuccess case shieldFundsFailure(ZcashError) case synchronizerStateChanged(RedactableSynchronizerState) case syncProgress(SyncProgressReducer.Action) + case transactionSubmitResult(TransactionSubmitResult) case updateHintBoxVisibility(Bool) } @@ -119,6 +120,7 @@ public struct BalanceBreakdownReducer: Reducer { return .none case .onAppear: + state.autoShieldingThreshold = zcashSDKEnvironment.shieldingThreshold return .publisher { sdkSynchronizer.stateStream() .throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true) @@ -143,27 +145,34 @@ public struct BalanceBreakdownReducer: Reducer { case .shieldFunds: state.isShieldingFunds = true - return .run { [state] send in + return .run { send in do { let storedWallet = try walletStorage.exportWallet() let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value()) let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0, zcashSDKEnvironment.network.networkType) + + guard let uAddress = try await sdkSynchronizer.getUnifiedAddress(0) else { throw "sdkSynchronizer.getUnifiedAddress" } - let transaction = try await sdkSynchronizer.shieldFunds(spendingKey, Memo(string: ""), state.autoShieldingThreshold) - - await send(.shieldFundsSuccess(transaction)) + let address = try uAddress.transparentReceiver() + let proposal = try await sdkSynchronizer.proposeShielding(0, zcashSDKEnvironment.shieldingThreshold, .empty, address) + + guard let proposal else { throw "sdkSynchronizer.proposeShielding" } + + let stream = try await sdkSynchronizer.createProposedTransactions(proposal, spendingKey) + + for try await transactionSubmitResult in stream { + await send(.transactionSubmitResult(transactionSubmitResult)) + } } catch { await send(.shieldFundsFailure(error.toZcashError())) } } case .shieldFundsSuccess: - state.isShieldingFunds = false state.transparentBalance = .zero return .none case .shieldFundsFailure(let error): - state.isShieldingFunds = false state.alert = AlertState.shieldFundsFailure(error) return .none @@ -179,6 +188,22 @@ public struct BalanceBreakdownReducer: Reducer { case .syncProgress: return .none + case .transactionSubmitResult(let transactionSubmitResult): + state.isShieldingFunds = false + switch transactionSubmitResult { + case .success: + return .send(.shieldFundsSuccess) + case .grpcFailure(txId: let txId, error: let error): + let zcashError = error.toZcashError() + return .send(.shieldFundsFailure(zcashError)) + case .submitFailure(txId: let txId, code: let code, description: let description): + let zcashError = "submitFailure, code(\(code) desc: \(description)".toZcashError() + return .send(.shieldFundsFailure(zcashError)) + case .notAttempted(txId: let txId): + let zcashError = "transactionSubmitResult notAttempted".toZcashError() + return .send(.shieldFundsFailure(zcashError)) + } + case .updateHintBoxVisibility(let visibility): state.isHintBoxVisible = visibility return .none @@ -203,7 +228,7 @@ extension AlertState where Action == BalanceBreakdownReducer.Action { extension BalanceBreakdownReducer.State { public static let placeholder = BalanceBreakdownReducer.State( - autoShieldingThreshold: Zatoshi(1_000_000), + autoShieldingThreshold: .zero, changePending: .zero, isShieldingFunds: false, pendingTransactions: .zero, @@ -214,7 +239,7 @@ extension BalanceBreakdownReducer.State { ) public static let initial = BalanceBreakdownReducer.State( - autoShieldingThreshold: Zatoshi(1_000_000), + autoShieldingThreshold: .zero, changePending: .zero, isShieldingFunds: false, pendingTransactions: .zero, diff --git a/modules/Sources/Features/SendFlow/SendFlowStore.swift b/modules/Sources/Features/SendFlow/SendFlowStore.swift index bf48d8f61..9a11ca6b5 100644 --- a/modules/Sources/Features/SendFlow/SendFlowStore.swift +++ b/modules/Sources/Features/SendFlow/SendFlowStore.swift @@ -37,6 +37,7 @@ public struct SendFlowReducer: Reducer { public var destination: Destination? public var isSending = false public var memoState: MessageEditorReducer.State + public var proposal: Proposal? public var scanState: Scan.State public var spendableBalance = Zatoshi.zero public var totalBalance = Zatoshi.zero @@ -108,6 +109,7 @@ public struct SendFlowReducer: Reducer { public init( addMemoState: Bool, destination: Destination? = nil, + isSending: Bool = false, memoState: MessageEditorReducer.State, scanState: Scan.State, spendableBalance: Zatoshi = .zero, @@ -117,6 +119,7 @@ public struct SendFlowReducer: Reducer { ) { self.addMemoState = addMemoState self.destination = destination + self.isSending = isSending self.memoState = memoState self.scanState = scanState self.spendableBalance = spendableBalance @@ -132,14 +135,16 @@ public struct SendFlowReducer: Reducer { case memo(MessageEditorReducer.Action) case onAppear case onDisappear + case proposal(Proposal) case reviewPressed case scan(Scan.Action) case sendPressed - case sendDone(TransactionState) + case sendDone case sendFailed(ZcashError) case synchronizerStateChanged(RedactableSynchronizerState) case transactionAddressInput(TransactionAddressTextFieldReducer.Action) case transactionAmountInput(TransactionAmountTextFieldReducer.Action) + case transactionSubmitResult(TransactionSubmitResult) case updateDestination(SendFlowReducer.State.Destination?) } @@ -199,56 +204,71 @@ public struct SendFlowReducer: Reducer { state.destination = nil state.isSending = false return .none - + + case let .proposal(proposal): + state.proposal = proposal + return .none + case let .updateDestination(destination): state.destination = destination return .none case .reviewPressed: - state.destination = .sendConfirmation - return .none + return .run { [state] send in + do { + let recipient = try Recipient(state.address, network: zcashSDKEnvironment.network.networkType) + + let memo: Memo? + if state.transactionAddressInputState.isValidTransparentAddress { + memo = nil + } else if let memoText = state.addMemoState ? state.memoState.text : nil { + memo = memoText.data.isEmpty ? nil : try Memo(string: memoText.data) + } else { + memo = nil + } + + let proposal = try await sdkSynchronizer.proposeTransfer(0, recipient, state.amount, memo) + + await send(.proposal(proposal)) + await send(.updateDestination(.sendConfirmation)) + } catch { + await send(.sendFailed(error.toZcashError())) + } + } case .sendPressed: + state.isSending = true + + guard let proposal = state.proposal else { + return .send(.sendFailed("missing proposal".toZcashError())) + } + state.amount = Zatoshi(state.transactionAmountInputState.amount.data) state.address = state.transactionAddressInputState.textFieldState.text.data - do { - let storedWallet = try walletStorage.exportWallet() - let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value()) - let network = zcashSDKEnvironment.network.networkType - let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0, network) - - let memo: Memo? - if state.transactionAddressInputState.isValidTransparentAddress { - memo = nil - } else if let memoText = state.addMemoState ? state.memoState.text : nil { - memo = memoText.data.isEmpty ? nil : try Memo(string: memoText.data) - } else { - memo = nil - } - - let recipient = try Recipient(state.address, network: network) - state.isSending = true - - return .run { [state] send in - do { - let transaction = try await sdkSynchronizer.sendTransaction(spendingKey, state.amount, recipient, memo) - await send(.sendDone(transaction)) - } catch { - await send(.sendFailed(error.toZcashError())) + return .run { send in + do { + let storedWallet = try walletStorage.exportWallet() + let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase.value()) + let network = zcashSDKEnvironment.network.networkType + let spendingKey = try derivationTool.deriveSpendingKey(seedBytes, 0, network) + + let stream = try await sdkSynchronizer.createProposedTransactions(proposal, spendingKey) + + for try await transactionSubmitResult in stream { + await send(.transactionSubmitResult(transactionSubmitResult)) } + } catch { + await send(.sendFailed(error.toZcashError())) } - } catch { - return .send(.sendFailed(error.toZcashError())) } - + case .sendDone: state.destination = nil state.memoState.text = "".redacted state.transactionAmountInputState.textFieldState.text = "".redacted state.transactionAmountInputState.amount = Int64(0).redacted state.transactionAddressInputState.textFieldState.text = "".redacted - state.isSending = false return .none case .sendFailed(let error): @@ -271,6 +291,22 @@ public struct SendFlowReducer: Reducer { case .transactionAddressInput: return .none + case .transactionSubmitResult(let transactionSubmitResult): + switch transactionSubmitResult { + case .success: + state.isSending = false + return .send(.sendDone) + case .grpcFailure(txId: _, error: let error): + let zcashError = error.toZcashError() + return .send(.sendFailed(zcashError)) + case let .submitFailure(txId: _, code: code, description: description): + let zcashError = "submitFailure, code(\(code) desc: \(description)".toZcashError() + return .send(.sendFailed(zcashError)) + case .notAttempted: + let zcashError = "transactionSubmitResult notAttempted".toZcashError() + return .send(.sendFailed(zcashError)) + } + case .synchronizerStateChanged(let latestState): state.spendableBalance = latestState.data.accountBalance?.data?.saplingBalance.spendableValue ?? .zero state.totalBalance = latestState.data.accountBalance?.data?.saplingBalance.total() ?? .zero diff --git a/modules/Sources/Features/Tabs/TabsStore.swift b/modules/Sources/Features/Tabs/TabsStore.swift index d06638859..bb5c7f331 100644 --- a/modules/Sources/Features/Tabs/TabsStore.swift +++ b/modules/Sources/Features/Tabs/TabsStore.swift @@ -119,8 +119,7 @@ public struct TabsReducer: Reducer { case .addressDetails: return .none - case .balanceBreakdown(.shieldFundsSuccess(let transaction)): - state.homeState.transactionListState.transactionList.insert(transaction, at: 0) + case .balanceBreakdown(.shieldFundsSuccess): return .none case .balanceBreakdown: @@ -144,8 +143,7 @@ public struct TabsReducer: Reducer { state.isRestoringWallet = value return .none - case .send(.sendDone(let transaction)): - state.homeState.transactionListState.transactionList.insert(transaction, at: 0) + case .send(.sendDone): state.selectedTab = .account return .none diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 783a74520..a19690280 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stephencelis/SQLite.swift.git", "state" : { - "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", - "version" : "0.14.1" + "revision" : "e78ae0220e17525a15ac68c697a155eb7a672a8e", + "version" : "0.15.0" } }, { @@ -266,8 +266,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "363da63c1966405764f380c627409b2f9d9e710b", - "version" : "1.21.0" + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" } }, { @@ -374,8 +374,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi", "state" : { - "revision" : "c90afd6cc092468e71810bc715ddb49be8210b75", - "version" : "0.5.1" + "revision" : "789d0c068fb32e2ab149cdd785f16e0ac88f3594" } }, { @@ -383,8 +382,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash/ZcashLightClientKit", "state" : { - "revision" : "2ef0e00385a8495b9d7115f7d0f9f1f19f91afa8", - "version" : "2.0.10" + "branch" : "1204-expose-proposals", + "revision" : "e9177a28f73d19f81849108e9b4978a10a08a582" } } ],