Adopt 0 17 (#317)

* This commit adopts Type Safe Addresses and Memos
It also include accommodations on the Receive Screen to show UA
without segments on chips as it was formerly with Sapling Addresses

There are several improvements on the use of @ViewBuilders to
replace AnyView to improve performance on rendering by SwiftUI.

* Adopt 0.17.0-beta

* Fix Chips on sapling address

* update build number

* adopt 0.17.0-alpha.3

* Fixed `ECC-WalletTests' and compiler errors

* Adopt 0.17.0-alpha.5

* Add entry to CHANGELOG

* Bump travis ci environment to Xcode 14

* Update CHANGELOG.md

Co-authored-by: Kris Nuttycombe <kris.nuttycombe@gmail.com>

* Remove reference to local package adopt 0.17.0-beta.rc1

* Travis CI failing because if iOS Simulator not available

* adopt changes for sync prepare and limit updates to UI when downloading more than 100 blocks

* Adopt ZcashLightClientKit 0.17.0-beta

* Build 0.5.0-140

Co-authored-by: Kris Nuttycombe <kris.nuttycombe@gmail.com>
This commit is contained in:
Francisco Gindre 2022-11-17 11:35:32 -03:00 committed by GitHub
parent 09d2879198
commit 31033b4d08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 906 additions and 884 deletions

View File

@ -1,9 +1,9 @@
language: swift
os: osx
osx_image: xcode13.4
osx_image: xcode14
xcode_project: ./wallet/ECC-Wallet.xcodeproj
xcode_scheme: ECC-Wallet
xcode_destination: platform=iOS Simulator,OS=15.5,name=iPhone 8
xcode_destination: platform=iOS Simulator,OS=16.0,name=iPhone 14
addons:
homebrew:
packages:
@ -18,4 +18,4 @@ install:
script:
- set -o pipefail && xcodebuild -version
- set -o pipefail && xcodebuild -showsdks
- travis_wait 60 xcodebuild -quiet -project ${TRAVIS_BUILD_DIR}/wallet/ECC-Wallet.xcodeproj -scheme ECC-Wallet-no-logging -destination platform\=iOS\ Simulator,OS\=15.5,name\=iPhone\ 8 build
- travis_wait 60 xcodebuild -quiet -project ${TRAVIS_BUILD_DIR}/wallet/ECC-Wallet.xcodeproj -scheme ECC-Wallet-no-logging -destination platform\=iOS\ Simulator,OS\=16.0,name\=iPhone\ 14 build

View File

@ -1,4 +1,22 @@
# Changelog
## 0.5.0 build 139: Adoption of 0.17.0-beta.rc1
The wallet has been updated to use [Unified Addresses](https://zips.z.cash/zip-0316)
as the primary address format. Unified addresses generated by the wallet currently
contain parts that allow the wallet to receive funds to the Sapling and transparent
pools, and makes these legacy addresses available for if you need to provide an
address to a service that doesn't yet understand the Unified Address format.
As part of this update, you may notice that the transparent address for your wallet
has changed! The new transparent address you see in your wallet is derived from
your wallet's unified address, but don't worry; your old address will still continue
to receive funds and make them available for shielding as normal.
Privacy is Normal.
## 0.5.0 build 135
* [#313] Application Hangs after Syncing on iOS 16-beta
* [#312] Download is interrupted every 30 seconds

View File

@ -157,6 +157,9 @@
0D9E89BB23DA3A9900AFD118 /* RestoreWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D9E89BA23DA3A9900AFD118 /* RestoreWallet.swift */; };
0D9E89BD23DA3BD600AFD118 /* SeedManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D9E89BC23DA3BD600AFD118 /* SeedManagement.swift */; };
0D9E89C223DA620E00AFD118 /* ZECCWalletEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D9E89C123DA620E00AFD118 /* ZECCWalletEnvironment.swift */; };
0DAC4DC628EE3829007466F3 /* Future+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DAC4DC528EE3829007466F3 /* Future+async.swift */; };
0DAC4DC728EE3829007466F3 /* Future+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DAC4DC528EE3829007466F3 /* Future+async.swift */; };
0DAC4DC828EE3829007466F3 /* Future+async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DAC4DC528EE3829007466F3 /* Future+async.swift */; };
0DACBB33264980630085F1CD /* OhMyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACBB32264980620085F1CD /* OhMyScreen.swift */; };
0DACBB34264980630085F1CD /* OhMyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACBB32264980620085F1CD /* OhMyScreen.swift */; };
0DACBB35264980630085F1CD /* OhMyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACBB32264980620085F1CD /* OhMyScreen.swift */; };
@ -545,6 +548,7 @@
0D9E89BA23DA3A9900AFD118 /* RestoreWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreWallet.swift; sourceTree = "<group>"; };
0D9E89BC23DA3BD600AFD118 /* SeedManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedManagement.swift; sourceTree = "<group>"; };
0D9E89C123DA620E00AFD118 /* ZECCWalletEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZECCWalletEnvironment.swift; sourceTree = "<group>"; };
0DAC4DC528EE3829007466F3 /* Future+async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Future+async.swift"; sourceTree = "<group>"; };
0DACBB32264980620085F1CD /* OhMyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OhMyScreen.swift; sourceTree = "<group>"; };
0DB020B2268CECA50010ABC4 /* AutoShieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoShieldView.swift; sourceTree = "<group>"; };
0DB020B6268D28A10010ABC4 /* ModelFlyWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFlyWeight.swift; sourceTree = "<group>"; };
@ -741,6 +745,7 @@
0D2607D624F42C46006FDC36 /* PasteboardHelper.swift */,
0D562FD3257841FD00E843DD /* MemoUtils.swift */,
0D3E6CFD26814E5D000EF685 /* SaplingParameterDownloader+Combine.swift */,
0DAC4DC528EE3829007466F3 /* Future+async.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -748,7 +753,6 @@
0D1250F623B557E40014EE3A = {
isa = PBXGroup;
children = (
0D522AE728D4F1D4006A53B0 /* Packages */,
0D12510123B557E40014EE3A /* wallet */,
0D12511823B557E60014EE3A /* walletTests */,
0D12512323B557E60014EE3A /* walletUITests */,
@ -864,13 +868,6 @@
path = Environment;
sourceTree = "<group>";
};
0D522AE728D4F1D4006A53B0 /* Packages */ = {
isa = PBXGroup;
children = (
);
name = Packages;
sourceTree = "<group>";
};
0D5E4AA52527B41A001CB335 /* Background */ = {
isa = PBXGroup;
children = (
@ -1366,6 +1363,7 @@
0D8C3E2D23D8B37900B94438 /* ProfileScreen.swift in Sources */,
0D9E89C223DA620E00AFD118 /* ZECCWalletEnvironment.swift in Sources */,
0DD1C2D024F051EE0042C1E4 /* Foundation+Zcash.swift in Sources */,
0DAC4DC628EE3829007466F3 /* Future+async.swift in Sources */,
0DD1C2CD24F04D470042C1E4 /* BlockExplorerUrlHandling.swift in Sources */,
0D100EDB23BA91F70089DB22 /* ZcashButton.swift in Sources */,
0D4B91DA24CA390B00A0E024 /* KeyboardAdaptive.swift in Sources */,
@ -1571,6 +1569,7 @@
0D3E6D0026814E5D000EF685 /* SaplingParameterDownloader+Combine.swift in Sources */,
0DCDFFC024B6722B000F6999 /* UIKit+Extensions.swift in Sources */,
0DCDFFC124B6722B000F6999 /* ZcashButtonBackground.swift in Sources */,
0DAC4DC828EE3829007466F3 /* Future+async.swift in Sources */,
0DCDFFC224B6722B000F6999 /* ReceiveFunds.swift in Sources */,
0D82F32B261284060058B05D /* Alerts.swift in Sources */,
0D5E4AA82527B440001CB335 /* BackgroundTasks.swift in Sources */,
@ -1626,6 +1625,7 @@
0DE30352263A158400CF877A /* ZECCWalletEnvironment.swift in Sources */,
0DE30353263A158400CF877A /* Foundation+Zcash.swift in Sources */,
0DE30354263A158400CF877A /* BlockExplorerUrlHandling.swift in Sources */,
0DAC4DC728EE3829007466F3 /* Future+async.swift in Sources */,
0DE30355263A158400CF877A /* ZcashButton.swift in Sources */,
0DE30356263A158400CF877A /* KeyboardAdaptive.swift in Sources */,
0DE30358263A158400CF877A /* ZcashTextField.swift in Sources */,
@ -1917,7 +1917,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 140;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -1944,7 +1944,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2054,7 +2054,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 140;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -2081,7 +2081,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 135;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2109,7 +2109,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Testnet";
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 140;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -2138,7 +2138,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Testnet";
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 136;
CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_ASSET_PATHS = "\"wallet/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2222,8 +2222,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/zcash/ZcashLightClientKit/";
requirement = {
kind = exactVersion;
version = "0.16.11-beta";
kind = upToNextMajorVersion;
minimumVersion = "0.17.0-beta";
};
};
0D90D56D281323920097FAAD /* XCRemoteSwiftPackageReference "lottie-ios" */ = {

View File

@ -122,8 +122,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "bc4c55b9f9584f09eb971d67d956e28d08caa9d0",
"version" : "2.43.1"
"revision" : "edfceecba13d68c1c993382806e72f7e96feaa86",
"version" : "2.44.0"
}
},
{
@ -131,8 +131,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-extras.git",
"state" : {
"revision" : "6c84d247754ad77487a6f0694273b89b83efd056",
"version" : "1.14.0"
"revision" : "91dd2d61fb772e1311bb5f13b59266b579d77e42",
"version" : "1.15.0"
}
},
{
@ -140,8 +140,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-http2.git",
"state" : {
"revision" : "00576e6f1efa5c46dca2ca3081dc56dd233b402d",
"version" : "1.23.0"
"revision" : "d6656967f33ed8b368b38e4b198631fc7c484a40",
"version" : "1.23.1"
}
},
{
@ -167,8 +167,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "88c7d15e1242fdb6ecbafbc7926426a19be1e98a",
"version" : "1.20.2"
"revision" : "ab3a58b7209a17d781c0d1dbb3e1ff3da306bae8",
"version" : "1.20.3"
}
},
{
@ -183,10 +183,10 @@
{
"identity" : "zcash-light-client-ffi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi.git",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"state" : {
"revision" : "b7e8a2abab84c44046b4afe4ee4522a0fa2fcc7f",
"version" : "0.0.3"
"revision" : "fad9802b907822d5a1877584c91f3786824521b7",
"version" : "0.1.0"
}
},
{
@ -194,8 +194,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash/ZcashLightClientKit/",
"state" : {
"revision" : "fe90ef00104b752a6832f09cb50fabeafc51d1f3",
"version" : "0.16.11-beta"
"revision" : "d9b85b40ad36ac5183f44b6db9805e44171ee988",
"version" : "0.17.0-beta"
}
},
{

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1410"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D1250FE23B557E40014EE3A"
BuildableName = "ECC Wallet.app"
BlueprintName = "ECC-Wallet"
ReferencedContainer = "container:ECC-Wallet.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D1250FE23B557E40014EE3A"
BuildableName = "ECC Wallet.app"
BlueprintName = "ECC-Wallet"
ReferencedContainer = "container:ECC-Wallet.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "0D1250FE23B557E40014EE3A"
BuildableName = "ECC Wallet.app"
BlueprintName = "ECC-Wallet"
ReferencedContainer = "container:ECC-Wallet.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -14,20 +14,17 @@ struct ActionableMessage: View {
var action: (() -> Void)? = nil
let cornerRadius: CGFloat = 5
var actionView: some View {
@ViewBuilder var actionView: some View {
if let action = self.action, let text = actionText {
return AnyView(
Button(action: action) {
Text(text)
.foregroundColor(Color.zAmberGradient2)
}
)
Button(action: action) {
Text(text)
.foregroundColor(Color.zAmberGradient2)
}
} else {
return AnyView (
EmptyView()
)
EmptyView()
}
}
var body: some View {
HStack {

View File

@ -41,41 +41,62 @@ struct AddressHelperView: View {
}
}
func viewFor(_ mode: Mode) -> some View {
@ViewBuilder func viewFor(_ mode: Mode) -> some View {
let shieldingAddress = appEnvironment.synchronizer.unifiedAddress.transparentReceiver()?.stringEncoded ?? ""
switch mode {
case .lastUsed(let address):
return VStack(spacing: 0) {
VStack(spacing: 0) {
AddressHelperViewSection(title: "LAST USED") {
AddrezzHelperViewCell(shieldingAddress: appEnvironment.shieldingAddress, address: address, shielded: isValidZ(address: address),selected: self.selection == Selection.lastUsedSelection)
AddrezzHelperViewCell(
shieldingAddress: shieldingAddress,
address: address,
shielded: isValidZ(address: address),
selected: self.selection == Selection.lastUsedSelection
)
}.onTapGesture {
self.onTap(selection: Selection.lastUsedSelection, value: address)
}
}.eraseToAnyView()
}
case .both(let clipboard, let lastUsed):
return VStack(spacing: 0) {
VStack(spacing: 0) {
AddressHelperViewSection(title: "send_onclipboard".localized()) {
AddrezzHelperViewCell(shieldingAddress: appEnvironment.shieldingAddress, address: clipboard, shielded: isValidZ(address: clipboard),selected: self.selection == Selection.clipboardSelection)
AddrezzHelperViewCell(
shieldingAddress: shieldingAddress,
address: clipboard,
shielded: isValidZ(address: clipboard),
selected: self.selection == Selection.clipboardSelection
)
}
.onTapGesture {
self.onTap(selection: Selection.clipboardSelection, value: clipboard)
}
AddressHelperViewSection(title: "LAST USED") {
AddrezzHelperViewCell(shieldingAddress: appEnvironment.shieldingAddress, address: lastUsed, shielded: isValidZ(address: lastUsed ),selected: self.selection == Selection.lastUsedSelection)
AddrezzHelperViewCell(
shieldingAddress: shieldingAddress,
address: lastUsed,
shielded: isValidZ(address: lastUsed),
selected: self.selection == Selection.lastUsedSelection
)
}
.onTapGesture {
self.onTap(selection: Selection.lastUsedSelection, value: lastUsed)
}
}.eraseToAnyView()
case .clipboard(let address):
return VStack(spacing: 0) {
AddressHelperViewSection(title: "send_onclipboard".localized()) {
AddrezzHelperViewCell(shieldingAddress: appEnvironment.shieldingAddress, address: address, shielded: isValidZ(address: address),selected: self.selection == Selection.clipboardSelection)
}
.onTapGesture {
self.onTap(selection: Selection.clipboardSelection, value: address)
}
}.eraseToAnyView()
case .clipboard(let address):
VStack(spacing: 0) {
AddressHelperViewSection(title: "send_onclipboard".localized()) {
AddrezzHelperViewCell(
shieldingAddress: shieldingAddress,
address: address,
shielded: isValidZ(address: address),
selected: self.selection == Selection.clipboardSelection
)
}
.onTapGesture {
self.onTap(selection: Selection.clipboardSelection, value: address)
}
}
}
}

View File

@ -48,16 +48,12 @@ struct DetailCard: View {
var model: DetailModel
var backgroundColor: Color = .black
var shieldImage: AnyView {
let view = model.shielded ? AnyView(Image("ic_shieldtick").renderingMode(.original)) : AnyView(EmptyView())
switch model.status {
case .paid(let success):
return success ? view : AnyView(EmptyView())
default:
return view
@ViewBuilder var shieldImage: some View {
if case .paid(let success) = model.status, success {
Image("ic_shieldtick").renderingMode(.original)
} else {
EmptyView()
}
}
var zecAmount: some View {
@ -240,7 +236,7 @@ extension DetailModel {
self.date = Date(timeIntervalSince1970: pendingTransaction.createTime)
self.id = pendingTransaction.rawTransactionId?.toHexStringTxId() ?? String(pendingTransaction.createTime)
self.shielded = pendingTransaction.toAddress.isValidShieldedAddress
self.shielded = pendingTransaction.recipient.isShielded
self.status = .paid(success: submitSuccess)
self.expirationHeight = pendingTransaction.expiryHeight
self.subtitle = DetailModel.subtitle(isPending: isPending,
@ -249,7 +245,7 @@ extension DetailModel {
date: self.date.transactionDetail,
latestBlockHeight: latestBlockHeight
)
self.zAddress = pendingTransaction.toAddress
self.zAddress = pendingTransaction.recipient.stringEncodedAddress
self.amount = -pendingTransaction.value
if let memo = pendingTransaction.memo {
self.memo = memo.asZcashTransactionMemo()
@ -258,6 +254,8 @@ extension DetailModel {
}
}
extension DetailModel {
var isSubmitSuccess: Bool {
switch status {
@ -287,3 +285,35 @@ extension Zatoshi {
Zatoshi(-zatoshi.amount)
}
}
extension PendingTransactionRecipient {
var isShielded: Bool {
switch self {
case .address(let recipient):
switch recipient {
case .sapling, .unified:
return true
case .transparent:
return false
}
case .internalAccount:
return true
}
}
var stringEncodedAddress: String {
switch self {
case .address(let recipient):
switch recipient {
case .sapling(let saplingAddress):
return saplingAddress.stringEncoded.shortAddress
case .transparent(let transparentAddress):
return transparentAddress.stringEncoded.shortAddress
case .unified(let unified):
return unified.stringEncoded.shortAddress
}
case .internalAccount(let account):
return "Funds shielded account \(account)"
}
}
}

View File

@ -20,30 +20,23 @@ struct ZcashButton: View {
var fill = Color.black
var text: String
func backgroundWith(geometry: GeometryProxy, backgroundShape: BackgroundShape) -> AnyView {
@ViewBuilder func backgroundWith(geometry: GeometryProxy, backgroundShape: BackgroundShape) -> some View {
switch backgroundShape {
case .chamfered:
return AnyView (
Group {
Group {
ZcashChamferedButtonBackground(cornerTrim: min(geometry.size.height, geometry.size.width) / 4.0)
.fill(self.fill)
ZcashChamferedButtonBackground(cornerTrim: min(geometry.size.height, geometry.size.width) / 4.0)
.stroke(self.color, lineWidth: 1.0)
}
)
case .rounded:
return AnyView(
EmptyView()
)
case .roundedCorners:
return AnyView(
EmptyView()
)
}
case .rounded, .roundedCorners:
EmptyView()
}
}
var body: some View {
ZStack {

View File

@ -13,16 +13,14 @@ enum ZcashFillStyle {
case solid(color: Color)
case outline(color: Color, lineWidth: CGFloat)
func fill<S: Shape>(_ s: S) -> AnyView {
@ViewBuilder func fill<S: Shape>(_ s: S) -> some View {
switch self {
case .gradient(let g):
return AnyView (s.fill(g))
s.fill(g)
case .solid(let color):
return AnyView(s.fill(color))
s.fill(color)
case .outline(color: let color, lineWidth: let lineWidth):
return AnyView(
s.stroke(color, lineWidth: lineWidth)
)
s.stroke(color, lineWidth: lineWidth)
}
}
}
@ -40,27 +38,23 @@ struct ZcashButtonBackground: ViewModifier {
self.buttonShape = buttonShape
}
func backgroundWith(geometry: GeometryProxy, backgroundShape: BackgroundShape) -> AnyView {
@ViewBuilder func backgroundWith(
geometry: GeometryProxy,
backgroundShape: BackgroundShape
) -> some View {
switch backgroundShape {
case .chamfered(let fillStyle):
return AnyView (
fillStyle.fill( ZcashChamferedButtonBackground(cornerTrim: min(geometry.size.height, geometry.size.width) / 4.0))
)
fillStyle.fill( ZcashChamferedButtonBackground(cornerTrim: min(geometry.size.height, geometry.size.width) / 4.0))
case .rounded(let fillStyle):
return AnyView(
fillStyle.fill(
ZcashRoundedButtonBackground()
)
fillStyle.fill(
ZcashRoundedButtonBackground()
)
case .roundedCorners(let fillStyle):
return AnyView(
fillStyle.fill(
ZcashRoundCorneredButtonBackground()
)
fillStyle.fill(
ZcashRoundCorneredButtonBackground()
)
}
}

View File

@ -25,19 +25,17 @@ struct ZcashTextField: View {
@Binding var text: String
var accessoryView: AnyView {
@ViewBuilder var accessoryView: some View {
if let img = accessoryIcon, let action = action {
return AnyView(
Button(action: {
action()
}) {
img
.resizable()
}
)
Button(action: {
action()
}) {
img
.resizable()
}
} else {
return AnyView(EmptyView())
EmptyView()
}
}

View File

@ -18,17 +18,18 @@ enum AutoShieldingResult {
protocol ShieldingCapable: AnyObject {
/**
Sends zatoshi.
- Parameter spendingKey: the key that allows spends to occur.
- Parameter transparentSecretKey: the key that allows to spend transaprent funds
- Parameter spendingKey: the key that allows to spend transaprent funds from the given account
- Parameter memo: the optional memo to include as part of the transaction.
- Parameter accountIndex: the optional account id that will be used to shield your funds to. 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)
func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo
) async throws -> PendingTransactionEntity
}
protocol AutoShieldingStrategy {
var shouldAutoShield: Bool { get }
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error>
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult
}
protocol UserSession {
@ -38,11 +39,8 @@ protocol UserSession {
func markAutoShield()
}
typealias PrivateKeyAccountIndexPair = (privateKey: String, account: Int, index: Int)
protocol ShieldingKeyProviding {
func getTransparentSecretKey() throws -> PrivateKeyAccountIndexPair
func getSpendingKey() throws -> PrivateKeyAccountIndexPair
func getShieldingKey() throws -> UnifiedSpendingKey
}
protocol TransparentBalanceProviding {
@ -71,50 +69,27 @@ protocol AutoShielder: AnyObject {
var strategy: AutoShieldingStrategy { get }
var shielder: ShieldingCapable { get }
var keyDeriver: KeyDeriving { get }
func shield() -> Future<AutoShieldingResult, Error>
func shield() async throws -> AutoShieldingResult
}
extension AutoShielder {
func shield() -> Future<AutoShieldingResult, Error> {
func shield() async throws -> AutoShieldingResult {
guard strategy.shouldAutoShield else {
return Future<AutoShieldingResult,Error> { promise in
promise(.success(.notNeeded))
}
}
return Future<AutoShieldingResult, Error> {[weak self] promise in
guard let self = self else {
promise(.failure(ShieldFundsError.shieldingFailed(underlyingError: DeveloperFacingErrors.unexpectedBehavior(message: "Weak reference is nil. This is probably a programing error"))))
return
}
do {
let spendingKeyKeyPair = try self.keyProviding.getSpendingKey()
let tskKeyPair = try self.keyProviding.getTransparentSecretKey()
let fromAccount = tskKeyPair.account
let tsk = tskKeyPair.privateKey
let spendingKey = spendingKeyKeyPair.privateKey
// TODO: add parameters to vary the index and the account to shield from
let tAddress = try self.keyDeriver.deriveTransparentAddressFromPrivateKey(tsk)
self.shielder.shieldFunds(spendingKey: spendingKey, transparentSecretKey: tsk, memo: "Shielding from your t-address: \(tAddress)", from: fromAccount) { result in
switch result {
case .success(let pendingTx):
promise(.success(.shielded(pendingTx: pendingTx)))
case .failure(let error):
promise(.failure(error))
}
}
} catch {
promise(.failure(KeyDerivationErrors.derivationError(underlyingError: error)))
}
return .notNeeded
}
let usk = try self.keyProviding.getShieldingKey()
let memo = try Memo(string: "Shielding from your account: \(usk.account)")
return await .shielded(
pendingTx: try self.shielder.shieldFunds(spendingKey: usk, memo: memo)
)
}
}
class ConcreteAutoShielder: AutoShielder {
var keyDeriver: KeyDeriving
var shielder: ShieldingCapable
@ -154,9 +129,9 @@ class ThresholdDrivenAutoShielding: AutoShieldingStrategy {
self.transparentBalanceProvider = tBalance
}
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error> {
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
// this strategy attempts to shield once per session, regardless of the result.
return autoShielder.shield()
try await autoShielder.shield()
}
}
@ -165,8 +140,8 @@ class ManualShielding: AutoShieldingStrategy {
true
}
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error> {
autoShielder.shield()
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
try await autoShielder.shield()
}
}
@ -198,23 +173,11 @@ class AutoShieldingBuilder {
extension SDKSynchronizer: ShieldingCapable {}
class DefaultShieldingKeyProvider: ShieldingKeyProviding {
func getTransparentSecretKey() throws -> PrivateKeyAccountIndexPair {
func getShieldingKey() throws -> UnifiedSpendingKey {
let derivationTool = DerivationTool(networkType: ZCASH_NETWORK.networkType)
let s = try SeedManager.default.exportPhrase()
let seed = try MnemonicSeedProvider.default.toSeed(mnemonic: s)
let tsk = try derivationTool.deriveTransparentPrivateKey(seed: seed)
return (tsk, 0, 0)
}
func getSpendingKey() throws -> PrivateKeyAccountIndexPair {
let derivationTool = DerivationTool(networkType: ZCASH_NETWORK.networkType)
let s = try SeedManager.default.exportPhrase()
let seed = try MnemonicSeedProvider.default.toSeed(mnemonic: s)
let keys = try derivationTool.deriveSpendingKeys(seed: seed, numberOfAccounts: 1)
guard let key = keys.first else {
throw KeyDerivationErrors.unableToDerive
}
return (key, 0, 0)
return try derivationTool.deriveUnifiedSpendingKey(seed: seed, accountIndex: 0)
}
}

View File

@ -90,11 +90,6 @@ class CombineSynchronizer {
}
}
func latestDownloadedHeight() throws -> BlockHeight {
try self.synchronizer.latestDownloadedHeight()
}
init(initializer: Initializer) throws {
self.walletDetailsBuffer = CurrentValueSubject([DetailModel]())
self.synchronizer = try SDKSynchronizer(initializer: initializer)
@ -113,13 +108,17 @@ class CombineSynchronizer {
guard let self = self else { return }
guard let userInfo = notification.userInfo else {
logger.error("Received `.synchronizerSynced` but the userInfo is empty")
self.updatePublishers()
Task { @MainActor in
await self.updatePublishers()
}
return
}
guard let synchronizerState = userInfo[SDKSynchronizer.NotificationKeys.synchronizerState] as? SDKSynchronizer.SynchronizerState else {
logger.error("Received `.synchronizerSynced` but the userInfo is empty")
self.updatePublishers()
Task { @MainActor in
await self.updatePublishers()
}
return
}
self.updatePublishers(with: synchronizerState)
@ -210,6 +209,7 @@ class CombineSynchronizer {
return nil
}
})
.sink(receiveValue: { [weak self] status in
self?.syncStatus.send(status)
})
@ -229,22 +229,19 @@ class CombineSynchronizer {
.store(in: &cancellables)
}
func prepare() throws {
guard let uvk = self.initializer.viewingKeys.first else {
throw SynchronizerError.initFailed(message: "unable to derive unified address. this is probably a programming error")
func prepare(with seedBytes: [UInt8]?) async throws {
// TODO: handle two-step prepare
let initDbResult = try self.synchronizer.prepare(with: seedBytes)
guard initDbResult == Initializer.InitializationResult.success else {
throw SynchronizerError.initFailed(message: "Seed is require to initialize")
}
do {
let derivationTool = DerivationTool(networkType: ZCASH_NETWORK.networkType)
self.unifiedAddress = try derivationTool.deriveUnifiedAddressFromUnifiedViewingKey(uvk)
} catch {
throw SynchronizerError.initFailed(message: "unable to derive unified address: \(error.localizedDescription)")
}
try self.synchronizer.prepare()
self.unifiedAddress = await self.synchronizer.getUnifiedAddress(accountIndex: 0)
// BUGFIX: transactions history empty when synchronizer fails to connect to server
// fill with initial values
self.updatePublishers()
await self.updatePublishers()
}
func start(retry: Bool = false) throws {
@ -267,8 +264,8 @@ class CombineSynchronizer {
synchronizer.cancelSpend(transaction: pendingTransaction)
}
func rewind(_ policy: RewindPolicy) throws {
try synchronizer.rewind(policy)
func rewind(_ policy: RewindPolicy) async throws {
try await synchronizer.rewind(policy)
}
func updatePublishers(with state: SDKSynchronizer.SynchronizerState) {
@ -284,14 +281,10 @@ class CombineSynchronizer {
.store(in: &self.cancellables)
}
func updatePublishers() {
if let ua = self.unifiedAddress,
let tBalance = try? synchronizer.getTransparentBalance(address: ua.tAddress) {
self.transparentBalance.send(tBalance)
} else {
self.transparentBalance.send(WalletBalance(verified: .zero, total: .zero))
}
func updatePublishers() async {
let tBalance = (try? await synchronizer.getTransparentBalance(accountIndex: 0)) ?? WalletBalance.zero
self.transparentBalance.send(tBalance)
let shieldedVerifiedBalance: Zatoshi = synchronizer.getShieldedVerifiedBalance()
let shieldedTotalBalance: Zatoshi = synchronizer.getShieldedBalance(accountIndex: 0)
@ -312,70 +305,29 @@ class CombineSynchronizer {
}
}
func send(with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?,from account: Int) -> Future<PendingTransactionEntity,Error> {
Future<PendingTransactionEntity, Error>() { [weak self]
promise in
self?.synchronizer.sendToAddress(spendingKey: spendingKey, zatoshi: Zatoshi(zatoshi), toAddress: recipientAddress, memo: memo, from: account) { [weak self](result) in
self?.updatePublishers()
switch result {
case .failure(let error):
promise(.failure(error))
case .success(let pendingTx):
promise(.success(pendingTx))
}
}
}
func send(
with spendingKey: UnifiedSpendingKey,
zatoshi: Zatoshi,
to recipientAddress: Recipient,
memo: Memo?
) async throws -> PendingTransactionEntity {
let pendingTx = try await self.synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: zatoshi,
toAddress: recipientAddress,
memo: memo
)
await self.updatePublishers()
return pendingTx
}
public func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int) -> Future<PendingTransactionEntity, Error> {
Future<PendingTransactionEntity, Error>() { [weak self]
promise in
self?.synchronizer.shieldFunds(spendingKey: spendingKey, transparentSecretKey: transparentSecretKey, memo: memo, from: accountIndex) {[weak self] (result) in
self?.updatePublishers()
switch result {
case .failure(let error):
promise(.failure(error))
case .success(let pendingTx):
promise(.success(pendingTx))
}
}
}
}
func unshieldedBalance(for tAddress: String) -> Future<WalletBalance,Error> {
Future<WalletBalance,Error>() { [weak self]
promise in
guard let self = self else { return }
let walletBirthday = (try? SeedManager.default.exportBirthday()) ?? ZCASH_NETWORK.constants.saplingActivationHeight
self.synchronizer.refreshUTXOs(address: tAddress, from: walletBirthday, result: { [weak self] (r) in
guard let self = self else { return }
switch r {
case .success:
do {
let balance = try self.synchronizer.getTransparentBalance(address: tAddress)
promise(.success(balance))
} catch {
promise(.failure(error))
}
case .failure(let error):
promise(.failure(error))
}
})
}
}
func cachedUnshieldedBalance(for tAddress: String) -> Future<WalletBalance,Error> {
Future<WalletBalance,Error>() { [weak self] promise in
guard let self = self else { return }
do {
promise(.success(try self.synchronizer.getTransparentBalance(address: tAddress)))
} catch {
promise(.failure(error))
}
}
public func shieldFunds(
spendingKey: UnifiedSpendingKey,
memo: Memo
) async throws -> PendingTransactionEntity {
try await self.shieldFunds(spendingKey: spendingKey, memo: memo)
}
}
@ -423,29 +375,33 @@ extension CombineSynchronizer {
}
extension CombineSynchronizer {
func fullRescan() {
func fullRescan() async {
do {
try self.rewind(.birthday)
try self.start(retry: true)
try await self.rewind(.birthday)
try await MainActor.run {
try self.start(retry: true)
}
} catch {
logger.error("Full rescan failed \(error)")
}
}
func quickRescan() {
func quickRescan() async {
do {
try self.rewind(.quick)
try self.start(retry: true)
try await self.rewind(.quick)
try await MainActor.run {
try self.start(retry: true)
}
} catch {
logger.error("Quick rescan failed \(error)")
}
}
func getTransparentAddress(account: Int = 0) -> TransparentAddress? {
self.synchronizer.getTransparentAddress(accountIndex: account)
func getTransparentAddress(account: Int = 0) async -> TransparentAddress? {
await self.synchronizer.getTransparentAddress(accountIndex: account)
}
func getShieldedAddress(account: Int = 0) -> SaplingShieldedAddress? {
self.synchronizer.getShieldedAddress(accountIndex: account)
func getShieldedAddress(account: Int = 0) async -> SaplingAddress? {
await self.synchronizer.getSaplingAddress(accountIndex: account)
}
}

View File

@ -46,15 +46,19 @@ final class SendFlowEnvironment: ObservableObject {
case finished
case failed(error: UserFacingErrors)
}
static let maxMemoLength: Int = ZECCWalletEnvironment.memoLengthLimit
enum FlowError: Error {
case memoToTransparentAddress
case invalidEnvironment
case duplicateSent
case failedToDownloadParameters(message: String)
case invalidAmount(message: String)
case derivationFailed(error: Error)
case derivationFailed(message: String)
case invalidDestinationAddress(address: String)
}
static let maxMemoLength: Int = ZECCWalletEnvironment.memoLengthLimit
@Published var showScanView = false
@Published var amount: String
@ -122,7 +126,9 @@ final class SendFlowEnvironment: ObservableObject {
self.isDone = true
self.state = .failed(error: mapToUserFacingError(ZECCWalletEnvironment.mapError(error: error)))
}
func preSend() {
@MainActor
func preSend() async {
guard case FlowState.preparing = self.state else {
let message = "attempt to start a pre-send stage where status was not .preparing and was \(self.state) instead"
logger.error(message)
@ -132,25 +138,39 @@ final class SendFlowEnvironment: ObservableObject {
}
self.state = .downloadingParameters
SaplingParameterDownloader.downloadParametersIfNeeded()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
switch completion {
case .failure(let error):
self?.state = .failed(error: error.code.asUserFacingError())
self?.fail(error.code.asUserFacingError())
break
case .finished:
break
}
} receiveValue: { [weak self] _ in
self?.send()
}
.store(in: &self.diposables)
do {
let result = try await SaplingParameterDownloader.downloadParamsIfnotPresent(
spendURL: try URL.spendParamsURL(),
outputURL: try URL.outputParamsURL()
)
} catch SaplingParameterDownloader.Errors.failed(let error) {
let message = "Failed to download parameters with error: \(error.localizedDescription)"
tracker.track(
.error(severity: .critical),
properties: [
ErrorSeverity.messageKey : message
]
)
fail(FlowError.failedToDownloadParameters(message: message))
} catch SaplingParameterDownloader.Errors.invalidURL {
let message = "Invalid URL was provided"
tracker.track(
.error(severity: .critical),
properties: [
ErrorSeverity.messageKey : message
]
)
fail(FlowError.failedToDownloadParameters(message: message))
} catch {
fail(error)
}
await send()
}
func send() {
func send() async {
self.state = .sending
guard !txSent else {
let message = "attempt to send tx twice"
logger.error(message)
@ -158,7 +178,7 @@ final class SendFlowEnvironment: ObservableObject {
fail(FlowError.duplicateSent)
return
}
self.state = .sending
let environment = ZECCWalletEnvironment.shared
guard let zatoshi = doubleAmount?.toZatoshi() else {
let message = "invalid zatoshi amount: \(String(describing: doubleAmount))"
@ -170,33 +190,49 @@ final class SendFlowEnvironment: ObservableObject {
do {
let phrase = try SeedManager.default.exportPhrase()
let seedBytes = try MnemonicSeedProvider.default.toSeed(mnemonic: phrase)
guard let spendingKey = try DerivationTool(networkType: ZCASH_NETWORK.networkType).deriveSpendingKeys(seed: seedBytes, numberOfAccounts: 1).first else {
let message = "no spending key for account 1"
logger.error(message)
self.fail(FlowError.derivationFailed(message: "no spending key for account 1"))
return
}
let usk = try DerivationTool(networkType: ZCASH_NETWORK.networkType)
.deriveUnifiedSpendingKey(seed: seedBytes, accountIndex: 0)
guard let replyToAddress = environment.getShieldedAddress() else {
guard let replyToAddress = await environment.getShieldedAddress() else {
let message = "could not derive user's own address"
logger.error(message)
self.fail(FlowError.derivationFailed(message: "could not derive user's own address"))
await MainActor.run {
self.fail(FlowError.derivationFailed(message: "could not derive user's own address"))
}
return
}
UserSettings.shared.lastUsedAddress = self.address
environment.synchronizer.send(
with: spendingKey,
zatoshi: zatoshi,
to: self.address,
memo: try Self.buildMemo(
let memo: Memo?
let recipient: Recipient = try Recipient(self.address, network: ZCASH_NETWORK.networkType)
if case .transparent = recipient {
memo = nil
} else if self.includeSendingAddress {
memo = try Self.buildMemo(
recipient: recipient,
memo: self.memo,
includesMemo: self.includesMemo,
replyToAddress: self.includeSendingAddress ? replyToAddress : nil
),
from: 0
)
.receive(on: DispatchQueue.main)
replyToAddress: replyToAddress
)
} else {
memo = try Memo(string: self.memo)
}
Future(operation: {
try await environment.synchronizer.send(
with: usk,
zatoshi: Zatoshi(zatoshi),
to: recipient,
memo: memo
)
})
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { [weak self] (completion) in
guard let self = self else {
return
@ -223,10 +259,8 @@ final class SendFlowEnvironment: ObservableObject {
self.pendingTx = transaction
self.state = .finished
}.store(in: &diposables)
self.txSent = true
} catch {
logger.error("failed to send: \(error)")
self.fail(error)
@ -243,6 +277,7 @@ final class SendFlowEnvironment: ObservableObject {
var hasSucceded: Bool {
isDone && !hasErrors
}
var doubleAmount: Double? {
NumberFormatter.zecAmountFormatter.number(from: self.amount)?.doubleValue
}
@ -251,43 +286,48 @@ final class SendFlowEnvironment: ObservableObject {
NotificationCenter.default.post(name: .sendFlowClosed, object: nil)
}
static func replyToAddress(_ address: String) -> String {
"\nReply-To: \(address)"
static func replyToAddress(to: Recipient, ownAddress: UnifiedAddress) -> String? {
switch to {
case .unified:
return "\nReply-To: \(ownAddress.stringEncoded)"
case .sapling:
guard let ownSapling = ownAddress.saplingReceiver() else {
return nil
}
return "\nReply-To: \(ownSapling.stringEncoded)"
default:
return nil
}
}
static func includeReplyTo(address: String, in memo: String, charLimit: Int = SendFlowEnvironment.maxMemoLength) throws -> String {
guard let isValidZAddr = try? DerivationTool(networkType: ZCASH_NETWORK.networkType).isValidShieldedAddress(address),
isValidZAddr else {
let msg = "the provided reply-to address is invalid"
logger.error(msg)
throw SendFlowEnvironment.FlowError.derivationFailed(message: msg)
static func includeReplyTo(recipient: Recipient, ownAddress: UnifiedAddress, in memo: String, charLimit: Int = SendFlowEnvironment.maxMemoLength) throws -> Memo {
if case Recipient.transparent = recipient {
throw SendFlowEnvironment.FlowError.memoToTransparentAddress
}
let replyTo = replyToAddress(address)
guard let replyTo = replyToAddress(to: recipient, ownAddress: ownAddress) else {
throw SendFlowEnvironment.FlowError.memoToTransparentAddress
}
if (memo.count + replyTo.count) >= charLimit {
let truncatedMemo = String(memo[memo.startIndex ..< memo.index(memo.startIndex, offsetBy: (memo.count - replyTo.count))])
return truncatedMemo + replyTo
return try Memo(string: truncatedMemo + replyTo)
}
return memo + replyTo
return try Memo(string: memo + replyTo)
}
static func buildMemo(memo: String, includesMemo: Bool, replyToAddress: String?) throws -> String? {
static func buildMemo(recipient: Recipient, memo: String, includesMemo: Bool, replyToAddress: UnifiedAddress) throws -> Memo {
guard includesMemo else { return nil }
guard includesMemo else { return .empty }
if let addr = replyToAddress {
return try includeReplyTo(address: addr, in: memo)
}
guard !memo.isEmpty else { return nil }
guard !memo.isEmpty else { return nil }
return memo
return try includeReplyTo(
recipient: recipient,
ownAddress: replyToAddress,
in: memo
)
}
}

View File

@ -13,7 +13,7 @@ import ZcashLightClientKit
protocol ShieldingPowers {
var status: CurrentValueSubject<ShieldFlow.Status,Error> { get set }
func shield()
func shield() async
}
final class ShieldFlow: ShieldingPowers {
@ -22,8 +22,8 @@ final class ShieldFlow: ShieldingPowers {
Thrown when a shield flow is requested but there's one already in progress
*/
case shieldFlowAlreadyStarted
}
enum Status {
case notStarted
case shielding
@ -68,50 +68,32 @@ final class ShieldFlow: ShieldingPowers {
_currentFlow = nil
}
func shield() {
func shield() async {
self.status.send(.shielding)
SaplingParameterDownloader.downloadParametersIfNeeded()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let urlError):
self.status.send(completion: .failure(urlError.code.asUserFacingError()))
break
case .finished:
break
}
}, receiveValue: { [weak self] result in
guard let self = self else {
return
}
self.shielder.shield()
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
Session.unique.markAutoShield()
switch completion {
case .failure(let e):
logger.error("failed to shield funds \(e.localizedDescription)")
tracker.report(handledException: DeveloperFacingErrors.handledException(error: e))
self?.status.send(completion: .failure(e))
case .finished:
self?.status.send(completion: .finished)
}
} receiveValue: { [weak self] result in
Session.unique.markAutoShield()
switch result{
case .notNeeded:
logger.warn(" -- WARNING -- You shielded funds but the result was not needed. This is probably a programming error")
self?.status.send(.notNeeded)
case .shielded(let pendingTx):
logger.debug("shielded \(pendingTx)")
self?.status.send(.ended(shieldingTx: pendingTx))
}
}
.store(in: &self.cancellables)
})
.store(in: &cancellables)
do {
_ = try await SaplingParameterDownloader.downloadParamsIfnotPresent(
spendURL: try URL.spendParamsURL(),
outputURL: try URL.outputParamsURL()
)
switch try await self.shielder.shield() {
case .shielded(let pendingTx):
logger.debug("shielded \(pendingTx)")
self.status.send(.ended(shieldingTx: pendingTx))
break
case .notNeeded:
logger.warn(" -- WARNING -- You shielded funds but the result was not needed. This is probably a programming error")
self.status.send(completion: .finished)
}
self.status.send(completion: .finished)
} catch {
logger.error("failed to shield funds \(error.localizedDescription)")
tracker.report(handledException: DeveloperFacingErrors.handledException(error: error))
self.status.send(completion: .failure(error))
}
}
}

View File

@ -46,9 +46,7 @@ final class ZECCWalletEnvironment: ObservableObject {
var shouldShowAutoShieldingNotice: Bool {
shouldShowAutoShieldingNoticeScreen()
}
var shieldingAddress: String {
synchronizer.unifiedAddress.tAddress
}
#if ENABLE_LOGGING
var shouldShowFeedbackDialog: Bool { shouldShowFeedbackRequest() }
#endif
@ -112,7 +110,7 @@ final class ZECCWalletEnvironment: ObservableObject {
self.synchronizer = nil
}
func createNewWallet() throws {
func createNewWallet() async throws {
do {
let randomPhrase = try MnemonicSeedProvider.default.randomMnemonic()
@ -121,17 +119,19 @@ final class ZECCWalletEnvironment: ObservableObject {
try SeedManager.default.importBirthday(birthday)
try SeedManager.default.importPhrase(bip39: randomPhrase)
try self.initialize()
try await self.initialize()
} catch {
throw WalletError.createFailed(underlying: error)
}
}
func initialize() throws {
func initialize() async throws {
let seedPhrase = try SeedManager.default.exportPhrase()
let seedBytes = try MnemonicSeedProvider.default.toSeed(mnemonic: seedPhrase)
let viewingKeys = try DerivationTool(networkType: ZCASH_NETWORK.networkType).deriveUnifiedViewingKeysFromSeed(seedBytes, numberOfAccounts: 1)
let viewingKey = try DerivationTool(networkType: ZCASH_NETWORK.networkType)
.deriveUnifiedSpendingKey(seed: seedBytes, accountIndex: 0)
.deriveFullViewingKey()
let initializer = Initializer(
cacheDbURL: self.cacheDbURL,
@ -141,7 +141,7 @@ final class ZECCWalletEnvironment: ObservableObject {
network: ZCASH_NETWORK,
spendParamsURL: self.spendParamsURL,
outputParamsURL: self.outputParamsURL,
viewingKeys: viewingKeys,
viewingKeys: [viewingKey],
walletBirthday: try SeedManager.default.exportBirthday(),
loggerProxy: logger)
@ -151,13 +151,13 @@ final class ZECCWalletEnvironment: ObservableObject {
shielder: self.synchronizer.synchronizer,
threshold: Self.autoShieldingThresholdInZatoshi,
balanceProviding: self.synchronizer)
try self.synchronizer.prepare()
try await self.synchronizer.prepare(with: seedBytes)
self.subscribeToApplicationNotificationsPublishers()
fixPendingTransactionsIfNeeded()
try self.synchronizer.start()
try await MainActor.run {
try self.synchronizer.start()
}
}
/**
@ -211,21 +211,6 @@ final class ZECCWalletEnvironment: ObservableObject {
static func mapError(error: Error) -> WalletError {
if let walletError = error as? WalletError {
return walletError
} else if let rustError = error as? RustWeldingError {
switch rustError {
case .genericError(let message):
return WalletError.genericErrorWithMessage(message: message)
case .dataDbInitFailed(let message):
return WalletError.initializationFailed(message: message)
case .dataDbNotEmpty:
return WalletError.initializationFailed(message: "attempt to initialize a db that was not empty")
case .saplingSpendParametersNotFound:
return WalletError.createFailed(underlying: rustError)
case .malformedStringInput:
return WalletError.genericErrorWithError(error: rustError)
default:
return WalletError.genericErrorWithError(error: rustError)
}
} else if let synchronizerError = error as? SynchronizerError {
switch synchronizerError {
case .lightwalletdValidationFailed(let underlyingError):
@ -441,8 +426,8 @@ extension ZECCWalletEnvironment {
self.synchronizer.initializer.getBalance().amount
}
func getShieldedAddress() -> String? {
self.synchronizer.initializer.getAddress()
func getShieldedAddress() async -> UnifiedAddress? {
await self.synchronizer.synchronizer.getUnifiedAddress(accountIndex: 0)
}
}
@ -467,72 +452,6 @@ extension View {
environment(\.walletEnvironment, env)
}
}
extension ZECCWalletEnvironment {
func fixPendingTransactionsIfNeeded() {
// check if we need to perform the fix or leave
guard !UserSettings.shared.didRescanPendingFix else {
return
}
logger.debug("Starting to pending transaction fix")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "Starting to pending transaction fix"])
do {
// get all the pending transactions
let txs = try synchronizer.synchronizer.allPendingTransactions()
guard !txs.isEmpty else {
logger.debug("no pending txs. saving settings")
UserSettings.shared.didRescanPendingFix = true
return
}
logger.debug("found pending transactions")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "found pending transactions"])
// fetch the first one that's reported to be unmined
guard let firstUnmined = txs.filter({ !$0.isMined }).first?.transactionEntity else {
logger.debug("no unmined txs. saving settings")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "no unmined txs. saving settings"])
UserSettings.shared.didRescanPendingFix = true
return
}
logger.debug("found unmined pending transactions with expiry height: \(String(describing: firstUnmined.expiryHeight))")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "found unmined pending transactions with expiry : \(String(describing: firstUnmined.expiryHeight))"])
try self.synchronizer.rewind(.transaction(firstUnmined))
UserSettings.shared.didRescanPendingFix = true
logger.debug("rewind successfull. saving settings")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "rewind successfull. saving settings"])
} catch SynchronizerError.rewindErrorUnknownArchorHeight {
do {
try self.synchronizer.rewind(.quick)
UserSettings.shared.didRescanPendingFix = true
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "rewind successful after recovering from error SynchronizerError.rewindErrorUnknownArchorHeight. saving settings"])
} catch {
logger.error("attempt to fix pending transactions failed with error: \(error)")
tracker.track(.error(severity: .critical), properties: ["pendingTxFix" : "attempt to fix pending transactions failed with error: \(error)"])
}
} catch {
logger.error("attempt to fix pending transactions failed with error: \(error)")
tracker.track(.error(severity: .critical), properties: ["pendingTxFix" : "attempt to fix pending transactions failed with error: \(error)"])
}
do {
let latestDownloadedHeight = try self.synchronizer.synchronizer.latestDownloadedHeight()
logger.debug("rewound to height \(latestDownloadedHeight)")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "rewind successfull. saving settings"])
} catch {
logger.debug("call to latestDownloadedHeight failed with error \(error)")
tracker.track(.screen(screen: .home), properties: ["pendingTxFix" : "call to latestDownloadedHeight failed with error \(error)"])
}
}
}
extension ZECCWalletEnvironment {
func shouldShowAutoShieldingNoticeScreen() -> Bool {

View File

@ -0,0 +1,24 @@
//
// Future+async.swift
// ECC-Wallet
//
// Created by John Sundell on 10/5/22.
// source: https://www.swiftbysundell.com/articles/creating-combine-compatible-versions-of-async-await-apis/
//
import Foundation
import Combine
extension Future where Failure == Error {
convenience init(operation: @escaping () async throws -> Output) {
self.init { promise in
Task {
do {
let output = try await operation()
promise(.success(output))
} catch {
promise(.failure(error))
}
}
}
}
}

View File

@ -23,15 +23,19 @@ extension String {
}
var isValidShieldedAddress: Bool {
(try? DerivationTool(networkType: ZCASH_NETWORK.networkType).isValidShieldedAddress(self)) ?? false
DerivationTool(networkType: ZCASH_NETWORK.networkType).isValidSaplingAddress(self)
}
var isValidTransparentAddress: Bool {
(try? DerivationTool(networkType: ZCASH_NETWORK.networkType).isValidTransparentAddress(self)) ?? false
DerivationTool(networkType: ZCASH_NETWORK.networkType).isValidTransparentAddress(self)
}
var isValidAddress: Bool {
self.isValidShieldedAddress || self.isValidTransparentAddress
guard (try? Recipient(self, network: ZCASH_NETWORK.networkType)) != nil else {
return false
}
return true
}
/**
@ -43,4 +47,12 @@ extension String {
+ "..."
+ String(self[self.index(self.endIndex, offsetBy: -8) ..< self.endIndex])
}
/// This only shows an abbreviated and redacted version of the an addr for UI purposes only
var shortAddress: String {
String(self[self.startIndex ..< self.index(self.startIndex, offsetBy: 8)])
+ "..."
+ String(self[self.index(self.endIndex, offsetBy: -8) ..< self.endIndex])
}
}

View File

@ -36,8 +36,7 @@ final class AutoShieldingViewModel: ObservableObject {
default:
return true
}
} )
})
.map { status -> State in
switch status {
case .ended(let shieldingTx):
@ -62,7 +61,9 @@ final class AutoShieldingViewModel: ObservableObject {
}
.store(in: &cancellables)
shieldFlow.shield()
Task(priority: .medium) {
await shieldFlow.shield()
}
} catch {
self.state = .failed(error: error)

View File

@ -58,18 +58,20 @@ struct CreateNewWallet: View {
Spacer()
Button(action: {
do {
tracker.track(.tap(action: .landingBackupWallet), properties: [:])
try self.appEnvironment.createNewWallet()
self.destination = Destinations.createNew
} catch WalletError.createFailed(let e) {
if case SeedManager.SeedManagerError.alreadyImported = e {
self.showError = AlertType.feedback(destination: .createNew, cause: e)
} else {
fail(WalletError.createFailed(underlying: e))
Task { @MainActor in
do {
tracker.track(.tap(action: .landingBackupWallet), properties: [:])
try await self.appEnvironment.createNewWallet()
self.destination = Destinations.createNew
} catch WalletError.createFailed(let e) {
if case SeedManager.SeedManagerError.alreadyImported = e {
self.showError = AlertType.feedback(destination: .createNew, cause: e)
} else {
fail(WalletError.createFailed(underlying: e))
}
} catch {
fail(error)
}
} catch {
fail(error)
}
}) {
@ -152,45 +154,48 @@ struct CreateNewWallet: View {
}
func existingCredentialsFound(originalDestination: Destinations) -> Alert {
Alert(title: Text("Existing keys found!"),
message: Text("it appears that this device already has keys stored on it. What do you want to do?"),
primaryButton: .default(Text("Restore existing keys"),
action: {
do {
try ZECCWalletEnvironment.shared.initialize()
self.destination = .createNew
} catch {
DispatchQueue.main.async {
self.fail(error)
Alert(
title: Text("Existing keys found!"),
message: Text("it appears that this device already has keys stored on it. What do you want to do?"),
primaryButton: .default(Text("Restore existing keys"),
action: {
Task { @MainActor in
do {
try await ZECCWalletEnvironment.shared.initialize()
self.destination = .createNew
} catch {
DispatchQueue.main.async {
self.fail(error)
}
}
}
}),
secondaryButton: .destructive(Text("Discard them and continue"),
action: {
ZECCWalletEnvironment.shared.nuke(abortApplication: false)
do {
try ZECCWalletEnvironment.shared.reset()
} catch {
self.fail(error)
return
}
switch originalDestination {
case .createNew:
do {
try self.appEnvironment.createNewWallet()
self.destination = originalDestination
} catch {
self.fail(error)
}
case .restoreWallet:
self.destination = originalDestination
}
}))
}),
secondaryButton: .destructive(Text("Discard them and continue"),
action: {
Task { @MainActor in
ZECCWalletEnvironment.shared.nuke(abortApplication: false)
do {
try ZECCWalletEnvironment.shared.reset()
} catch {
self.fail(error)
return
}
switch originalDestination {
case .createNew:
do {
try await self.appEnvironment.createNewWallet()
self.destination = originalDestination
} catch {
self.fail(error)
}
case .restoreWallet:
self.destination = originalDestination
}
}
}))
}
func defaultAlert(_ error: Error? = nil) -> Alert {
guard let e = error else {
return Alert(title: Text("Error Initializing Wallet"),

View File

@ -27,7 +27,7 @@ struct DisplayAddress<AccesoryContent: View>: View {
let qrSize: CGFloat = 200
var accessoryContent: AccesoryContent
init(address: String, title: String, chips: Int = 8, badge: Image, @ViewBuilder accessoryContent: (() -> (AccesoryContent))) {
init(address: String, title: String, chips: Int = 1, badge: Image, @ViewBuilder accessoryContent: (() -> (AccesoryContent))) {
self.address = address
self.title = title
self.chips = address.slice(into: chips)
@ -36,7 +36,7 @@ struct DisplayAddress<AccesoryContent: View>: View {
}
var body: some View {
VStack(alignment: .center, spacing: 20) {
VStack(alignment: .center, spacing: 10) {
Text(title)
.foregroundColor(.white)
.font(.system(size: 21))
@ -55,13 +55,16 @@ struct DisplayAddress<AccesoryContent: View>: View {
tracker.track(.tap(action: .copyAddress), properties: [:])
}) {
VStack {
if chips.count <= 2 {
if chips.count == 1 {
Text(address)
.foregroundColor(.white)
.font(.system(size: 16))
} else if chips.count == 2 {
ForEach(0 ..< chips.count, id: \.self) { i in
AddressFragment(number: i + 1, word: self.chips[i])
.frame(height: 24)
}
self.accessoryContent
} else {
ForEach(stride(from: 0, through: chips.count - 1, by: 2).map({ i in i}), id: \.self) { i in
HStack {
@ -72,8 +75,10 @@ struct DisplayAddress<AccesoryContent: View>: View {
}
}
}
}.padding([.horizontal], 15)
Spacer()
.frame(height: 10)
self.accessoryContent
}
.frame(minHeight: 96)
}.alert(item: self.$copyItemModel) { (p) -> Alert in
@ -107,3 +112,6 @@ struct DisplayAddress<AccesoryContent: View>: View {
// DisplayAddress(address: "zs1t2scx025jsy04mqyc4x0fsyspxe86gf3t6gyfhh9qdzq2a789sc2eccslflawf2kpuvxcqfjsef")
// }
//}

View File

@ -141,6 +141,17 @@ final class HomeViewModel: ObservableObject {
.store(in: &environmentCancellables)
environment.synchronizer.syncStatus
.compactMap({ status in
switch status {
case .downloading(let progressReport):
if (progressReport.targetHeight - progressReport.progressHeight < 100) ||
(progressReport.progressHeight % 100) == 0 {
return SyncStatus.downloading(progressReport)
}
return nil
default: return status
}
})
.receive(on: DispatchQueue.main)
.assign(to: \.syncStatus, on: self)
.store(in: &environmentCancellables)
@ -267,18 +278,11 @@ struct Home: View {
case .downloading(let progress):
SyncingButton(animationType: .frameProgress(startFrame: 0, endFrame: 100, progress: 1.0, loop: true)) {
if progress.targetHeight > 0 {
Text("Downloading ")
.foregroundColor(.white)
+ Text("\(progress.progressHeight) / \(progress.targetHeight)")
.foregroundColor(.white)
.font(.system(.body, design: .default).monospacedDigit())
} else {
Text("Downloading \(Int(progress.progress * 100))%")
.foregroundColor(.white)
.font(.system(.body, design: .default).monospacedDigit())
}
Text("Downloading ")
.foregroundColor(.white)
+ Text("\(progress.progressHeight) / \(progress.targetHeight)")
.foregroundColor(.white)
.font(.system(.body, design: .default).monospacedDigit())
}
.frame(width: 100, height: buttonHeight)

View File

@ -51,10 +51,10 @@ struct ProfileScreen: View {
Button(action: {
tracker.track(.tap(action: .copyAddress),
properties: [:])
PasteboardAlertHelper.shared.copyToPasteBoard(value: self.appEnvironment.synchronizer.unifiedAddress.zAddress, notify: "feedback_addresscopied".localized())
PasteboardAlertHelper.shared.copyToPasteBoard(value: self.appEnvironment.synchronizer.unifiedAddress.stringEncoded, notify: "feedback_addresscopied".localized())
}) {
Text(self.appEnvironment.synchronizer.unifiedAddress.zAddress)
Text(self.appEnvironment.synchronizer.unifiedAddress.stringEncoded)
.lineLimit(3)
.multilineTextAlignment(.center)
.font(.system(size: 15))
@ -176,8 +176,12 @@ struct ProfileScreen: View {
}
}),
.default(Text("Quick Re-Scan"), action: {
self.appEnvironment.synchronizer.quickRescan()
self.presentationMode.wrappedValue.dismiss()
Task {
await self.appEnvironment.synchronizer.quickRescan()
await MainActor.run {
self.presentationMode.wrappedValue.dismiss()
}
}
}),
.default(Text("Dismiss".localized()))
]

View File

@ -9,10 +9,14 @@
import SwiftUI
import ZcashLightClientKit
struct ReceiveFunds: View {
enum Tabs: Int, Equatable {
case unified
case sapling
case transparent
}
let unifiedAddress: UnifiedAddress
@Environment(\.presentationMode) var presentationMode
@State var selectedTab: Int = 0
@State var selectedTab: Int = Tabs.unified.rawValue
var body: some View {
NavigationView {
@ -20,7 +24,11 @@ struct ReceiveFunds: View {
ZcashBackground()
VStack(alignment: .center, spacing: 10, content: {
TabSelector(tabs: [
(Text("Shielded")
(Text("Unified")
.font(.system(size: 18))
.frame(maxWidth: .infinity, idealHeight: 48)
,.green),
(Text("Sapling")
.font(.system(size: 18))
.frame(maxWidth: .infinity, idealHeight: 48)
,.zYellow),
@ -31,29 +39,56 @@ struct ReceiveFunds: View {
], selectedTabIndex: $selectedTab)
.padding([.horizontal], 16)
if selectedTab == 0 {
DisplayAddress(address: unifiedAddress.zAddress,
title: "address_shielded".localized(),
badge: Image("QR-zcashlogo"),
accessoryContent: { EmptyView() })
} else {
DisplayAddress(address: unifiedAddress.tAddress,
title: "address_transparent".localized(),
chips: 2,
badge: Image("t-zcash-badge"),
accessoryContent: {
VStack(alignment: .leading) {
Text("This address is for receiving only.")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
Text("Any funds received will be auto-shielded.")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
}
})
switch Tabs(rawValue: selectedTab) {
case .unified, .none:
DisplayAddress(
address: unifiedAddress.stringEncoded,
title: "address_unified".localized(),
badge: Image("QR-zcashlogo"),
accessoryContent: {
HStack(alignment: .center) {
Image("yellow_shield")
VStack(alignment: .leading) {
Text("Contains Shielded and Transparent receivers")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
Text("Any transparent funds received will be auto-shielded.")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
}
}
}
)
case .sapling:
DisplayAddress(
address: unifiedAddress.saplingReceiver()!.stringEncoded,
title: "address_sapling".localized(),
chips: 8,
badge: Image("QR-zcashlogo"),
accessoryContent: { EmptyView() }
)
case .transparent:
DisplayAddress(
address: unifiedAddress.transparentReceiver()!.stringEncoded,
title: "address_transparent".localized(),
chips: 2,
badge: Image("t-zcash-badge"),
accessoryContent: {
VStack(alignment: .leading) {
Text("This address is for receiving only.")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
Text("Any funds received will be auto-shielded.")
.lineLimit(nil)
.foregroundColor(.white)
.font(.system(size: 14))
}
}
)
}
})
}

View File

@ -115,19 +115,21 @@ struct RestoreWallet: View {
)
Spacer()
Button(action: {
do {
try self.importSeed()
try self.importBirthday()
try self.appEnvironment.initialize()
} catch {
logger.error("\(error)")
tracker.track(.error(severity: .critical), properties: [
ErrorSeverity.underlyingError : "\(error)"])
self.showError = true
return
Task { @MainActor in
do {
try self.importSeed()
try self.importBirthday()
try await self.appEnvironment.initialize()
} catch {
logger.error("\(error)")
tracker.track(.error(severity: .critical), properties: [
ErrorSeverity.underlyingError : "\(error)"])
self.showError = true
return
}
tracker.track(.tap(action: .walletImport), properties: [:])
self.proceed = true
}
tracker.track(.tap(action: .walletImport), properties: [:])
self.proceed = true
}) {
Text("Proceed")
.foregroundColor(.black)

View File

@ -60,9 +60,11 @@ struct ScanAddress: View {
.padding()
}
var torchButton: AnyView {
guard torchAvailable else { return AnyView(EmptyView()) }
return AnyView(
@ViewBuilder var torchButton: some View {
switch torchAvailable {
case false:
EmptyView()
case true:
Button(action: {
self.toggleTorch(on: !self.torchEnabled)
tracker.track(.tap(action: .scanTorch),
@ -72,7 +74,7 @@ struct ScanAddress: View {
Image("bolt")
.renderingMode(.template)
}
)
}
}
var authorized: some View {
@ -142,28 +144,26 @@ struct ScanAddress: View {
}
func viewFor(state: CameraAccessHelper.Status) -> some View {
@ViewBuilder func viewFor(state: CameraAccessHelper.Status) -> some View {
switch state {
case .authorized, .undetermined:
let auth = authorized.navigationBarTitle("send_scanQR", displayMode: .inline)
if viewModel.showCloseButton {
return AnyView(
auth.navigationBarItems(leading: torchButton, trailing: ZcashCloseButton(action: {
tracker.track(.tap(action: .scanBack), properties: [:])
self.isScanAddressShown = false
}).frame(width: 30, height: 30))
)
}
return AnyView(
} else {
auth.navigationBarItems(
trailing: torchButton
)
)
}
case .unauthorized:
return AnyView(unauthorized)
unauthorized
case .unavailable:
return AnyView(restricted)
restricted
}
}

View File

@ -115,7 +115,9 @@ struct Sending: View {
}
.onAppear() {
tracker.track(.screen(screen: .sendFinal), properties: [:])
self.flow.preSend()
Task { @MainActor in
await self.flow.preSend()
}
}
}
}

View File

@ -22,12 +22,14 @@ struct TheNoScreen: View {
}
.navigationBarHidden(true)
.onAppear() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
Task { @MainActor in
do {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
let initialState = ZECCWalletEnvironment.getInitialState()
switch initialState {
case .unprepared, .initalized:
try appEnvironment.initialize()
try await appEnvironment.initialize()
appEnvironment.state = .initalized
default:
@ -36,6 +38,7 @@ struct TheNoScreen: View {
} catch {
self.appEnvironment.state = .failure(error: error)
logger.error(error.localizedDescription)
}
}
}

View File

@ -45,14 +45,12 @@ struct HeaderFooterFactory {
}
static func accessoryArrow(sent: Bool) -> AnyView {
@ViewBuilder static func accessoryArrow(sent: Bool) -> some View {
if sent {
return Image("outgoing_confirmed")
.eraseToAnyView()
Image("outgoing_confirmed")
} else {
return Image("outgoing_confirmed")
Image("outgoing_confirmed")
.rotationEffect(Angle(degrees: 180))
.eraseToAnyView()
}
}
static func outline(success: Bool, shielded: Bool) -> Color {
@ -109,7 +107,7 @@ struct HeaderFooterFactory {
.font(Font.zoboto(size: 36))
.foregroundColor(.white),
outline: outline(success: true, shielded: shielded),
accessory: accessoryArrow(sent: sent)
accessory: accessoryArrow(sent: sent).eraseToAnyView()
)
}

View File

@ -15,39 +15,49 @@ struct SendTransaction: View {
@State var sendOk = false
@State var addressHelperSelection: AddressHelperView.Selection = .none
@State var scanViewModel = ScanAddressViewModel(shouldShowSwitchButton: false, showCloseButton: true)
var availableBalance: Bool {
ZECCWalletEnvironment.shared.availableShieldedBalance > 0
}
var addressSubtitle: String {
let environment = ZECCWalletEnvironment.shared
guard !flow.address.isEmpty else {
return "feedback_default".localized()
}
let validShielded = environment.isValidShieldedAddress(flow.address)
let validTransparent = environment.isValidTransparentAddress(flow.address)
if validShielded {
return subtextForValid(shielded: flow.address)
do {
switch try Recipient(flow.address, network: ZCASH_NETWORK.networkType) {
case .unified(let uAddr):
return subtextForValid(unified: uAddr)
case .sapling(let zAddr):
return subtextForValid(shielded: zAddr)
case .transparent(let tAddr):
return subtextForValid(transparent: tAddr)
}
} catch {
return "feedback_invalidaddress".localized()
}
if validTransparent {
return subtextForValid(transparent: flow.address)
}
return "feedback_invalidaddress".localized()
}
func subtextForValid(shielded address: String) -> String {
if ZECCWalletEnvironment.shared.synchronizer.unifiedAddress.zAddress == address {
func subtextForValid(unified address: UnifiedAddress) -> String {
if ZECCWalletEnvironment.shared.synchronizer.unifiedAddress == address {
return "feedback_sameaddress".localized()
} else {
return "feedback_shieldedaddress".localized()
}
}
func subtextForValid(shielded address: SaplingAddress) -> String {
if ZECCWalletEnvironment.shared.synchronizer.unifiedAddress.saplingReceiver() == address {
return "feedback_sameaddress".localized()
} else {
return "feedback_shieldedaddress".localized()
}
}
func subtextForValid(transparent address: String) -> String {
if ZECCWalletEnvironment.shared.synchronizer.unifiedAddress.tAddress == address {
func subtextForValid(transparent address: TransparentAddress) -> String {
if ZECCWalletEnvironment.shared.synchronizer.unifiedAddress.transparentReceiver() == address {
return "This is your Auto Shielding address".localized()
} else {
return "feedback_transparentaddress".localized()
@ -85,31 +95,35 @@ struct SendTransaction: View {
flow.memo.count >= 0 && flow.memo.count <= charLimit
}
var addressInBuffer: AnyView {
@ViewBuilder var addressInBuffer: some View {
if let clipboard = UIPasteboard.general.string,
ZECCWalletEnvironment.shared.isValidAddress(clipboard),
clipboard.shortZaddress != nil {
if let lastUsed = UserSettings.shared.lastUsedAddress {
return AddressHelperView(selection: $addressHelperSelection, mode: .both(clipboard: clipboard, lastUsed: lastUsed)).eraseToAnyView()
AddressHelperView(selection: $addressHelperSelection, mode: .both(clipboard: clipboard, lastUsed: lastUsed))
} else {
return AddressHelperView(selection: $addressHelperSelection, mode: .clipboard(address: clipboard)).eraseToAnyView()
AddressHelperView(selection: $addressHelperSelection, mode: .clipboard(address: clipboard))
}
} else if let lastUsed = UserSettings.shared.lastUsedAddress {
return AddressHelperView(selection: $addressHelperSelection, mode: .lastUsed(address: lastUsed)).eraseToAnyView()
AddressHelperView(selection: $addressHelperSelection, mode: .lastUsed(address: lastUsed))
} else {
return AnyView(EmptyView())
EmptyView()
}
}
var charLimit: Int {
if flow.includeSendingAddress {
return ZECCWalletEnvironment.memoLengthLimit - SendFlowEnvironment.replyToAddress((ZECCWalletEnvironment.shared.getShieldedAddress() ?? "")).count
if flow.includeSendingAddress,
let recipient = try? Recipient(flow.address, network: ZCASH_NETWORK.networkType),
let replyTo = SendFlowEnvironment.replyToAddress(to: recipient, ownAddress: ZECCWalletEnvironment.shared.synchronizer.unifiedAddress)
{
return ZECCWalletEnvironment.memoLengthLimit - replyTo.count
}
return ZECCWalletEnvironment.memoLengthLimit
}
var recipientActiveColor: Color {
let address = flow.address
if ZECCWalletEnvironment.shared.isValidShieldedAddress(address) {

View File

@ -15,9 +15,12 @@ class WalletDetailsViewModel: ObservableObject {
var showError = false
@Published var balance: WalletBalance = .zero
var address: UnifiedAddress
private var synchronizerEvents = Set<AnyCancellable>()
private var internalEvents = Set<AnyCancellable>()
init(){
self.address = ZECCWalletEnvironment.shared.synchronizer.unifiedAddress
subscribeToSynchonizerEvents()
}
@ -46,10 +49,6 @@ class WalletDetailsViewModel: ObservableObject {
}
synchronizerEvents.removeAll()
}
var zAddress: String {
ZECCWalletEnvironment.shared.getShieldedAddress() ?? ""
}
}
struct WalletDetails: View {
@ -59,10 +58,6 @@ struct WalletDetails: View {
@Binding var isActive: Bool
@State var selectedModel: DetailModel? = nil
var zAddress: String {
viewModel.zAddress
}
var body: some View {
ZStack {
@ -90,7 +85,7 @@ struct WalletDetails: View {
List {
WalletDetailsHeader(zAddress: zAddress)
WalletDetailsHeader(zAddress: viewModel.address.stringEncoded)
.listRowBackground(Color.zDarkGray2)
.frame(height: 100)
.padding([.trailing], 24)

View File

@ -116,7 +116,9 @@ final class WalletBalanceBreakdownViewModel: ObservableObject {
}
}.store(in: &cancellables)
shieldEnvironment.shield()
Task(priority: .userInitiated) {
await shieldEnvironment.shield()
}
} catch {
self.status = .failed(error: error)

View File

@ -111,7 +111,8 @@
//ReceiveFunds
"QR Code for %@" = "QR Code for %@";
"address_shielded" = "Your Shielded Address";
"address_unified" = "Your Unified Address";
"address_sapling" = "Your Sapling Address";
"address_transparent" = "Your Transparent Address";
"receive_title" = "Receive ZEC";

View File

@ -126,7 +126,8 @@
//ReceiveFunds
"QR Code for %@" = "Código QR para %@";
"address_shielded" = "Tu dirección blindada";
"address_unified" = "Tu dirección unificada";
"address_sapling" = "Tu dirección Sapling";
"address_transparent" = "Tu dirección transparente";
"receive_title" = "Recibir ZEC";
"label_to" = "Para";

View File

@ -60,7 +60,8 @@
// Address Screen
"address_screen" = "Tuo Indirizzo";
"address_shielded" = "Il tuo indirizzo blindato";
"address_unified" = "Il tuo indirizzo unificato";
"address_sapling" = "Il tuo indirizzo sapling";
"address_transparent" = "Il tuo indirizzo trasparente";
"feedback_addresscopied" = "Indirizzo Copiato!";
"Request ZEC" = "Richiedi ZEC";

View File

@ -62,7 +62,8 @@
// Address Screen
"address_screen" = "당신의 주소";
"address_shielded" = "당신의 쉴드된 주소";
"address_unified" = "통합 주소";
"address_sapling" = "묘목 주소";
"address_transparent" = "당신의 투명 주소";
"feedback_addresscopied" = "주소 복사";
"Request ZEC" = "ZEC 부탁";

View File

@ -34,7 +34,8 @@
// Address Screen
"address_screen" = "Ваш адрес";
"address_shielded" = "Ваш защищённый адрес";
"address_unified" = "Ваш единый адрес";
"address_sapling" = "Ваш sapling адрес";
"address_transparent" = "Ваш прозрачный адрес";
"feedback_addresscopied" = "Адрес скопирован!";
"Request ZEC" = "Запросить сумму в ZEC";

View File

@ -31,7 +31,8 @@
// Address Screen
"button_wallethistory" = "钱包历史";
"address_screen" = "您的地址";
"address_shielded" = "您的隐蔽地址";
"address_unified" = "您的统一地址";
"address_sapling" = "你的树苗地址";
"address_transparent" = "您的透明地址";
"feedback_addresscopied" = "已复制地址";
"Request ZEC" = "请求 ZEC";

View File

@ -8,106 +8,72 @@
import XCTest
import Combine
@testable import ZcashLightClientKit
@testable import ECC_Wallet_Testnet
class AutoShieldingTests: XCTestCase {
var cancellables = [AnyCancellable]()
func testAutoShield() throws {
let mockShielder = MockShielder(strategy: MockFailedManualStrategy(),
shielder: MockSuccessfulShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving())
let expectation = XCTestExpectation(description: "Shield Expectation")
mockShielder.shield()
.receive(on: DispatchQueue.main)
.sink { completion in
expectation.fulfill()
switch completion {
case .failure(let error):
XCTFail("failed with error: \(error)")
case .finished:
break
}
} receiveValue: { result in
expectation.fulfill()
switch result {
case .notNeeded:
XCTFail("manual shielding is always needed")
case .shielded:
XCTAssertTrue(true)
}
func testAutoShield() async throws {
let mockShielder = MockShielder(
strategy: MockFailedManualStrategy(),
shielder: MockSuccessfulShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving()
)
do {
switch try await mockShielder.shield() {
case .notNeeded:
XCTFail("manual shielding is always needed")
case .shielded:
XCTAssertTrue(true)
}
.store(in: &cancellables)
wait(for: [expectation], timeout: 4)
} catch {
XCTFail("failed with error: \(error)")
return
}
}
func testAutoShieldFails() throws {
let mockShielder = MockShielder(strategy: MockFailedManualStrategy(),
shielder: MockFailureShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving())
func testAutoShieldFails() async throws {
let mockShielder = MockShielder(
strategy: MockFailedManualStrategy(),
shielder: MockFailureShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving()
)
let expectation = XCTestExpectation(description: "Shield Expectation")
mockShielder.shield()
.receive(on: DispatchQueue.main)
.sink { completion in
expectation.fulfill()
switch completion {
case .failure(let error):
switch error {
case ShieldFundsError.insuficientTransparentFunds:
XCTAssertTrue(true)
default:
XCTFail("failed with error: \(error)")
}
case .finished:
break
}
} receiveValue: { result in
expectation.fulfill()
switch result {
case .notNeeded:
XCTFail("manual shielding is always needed")
case .shielded:
XCTFail("this test should have failed")
}
do {
switch try await mockShielder.shield() {
case .notNeeded:
XCTFail("manual shielding is always needed")
case .shielded:
XCTFail("this test should have failed")
}
.store(in: &cancellables)
wait(for: [expectation], timeout: 4)
} catch ShieldFundsError.insuficientTransparentFunds {
XCTAssertTrue(true)
} catch {
XCTFail("failed with error: \(error)")
}
}
func testAutoShieldNonNeeded() {
let mockShielder = MockShielder(strategy: MockFailedManualStrategy(),
shielder: MockSuccessfulShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving())
let expectation = XCTestExpectation(description: "Shield Expectation")
mockShielder.shield()
.receive(on: DispatchQueue.main)
.sink { completion in
expectation.fulfill()
switch completion {
case .failure(let error):
XCTFail("failed with error: \(error)")
case .finished:
break
}
} receiveValue: { result in
expectation.fulfill()
switch result {
case .notNeeded:
XCTAssertTrue(true)
case .shielded:
XCTFail("this test should have failed")
}
func testAutoShieldNonNeeded() async throws {
let mockShielder = MockShielder(
strategy: MockShieldNotNeeded(),
shielder: MockSuccessfulShieldingCapable(),
keyProviding: MockKeyProviding(),
keyDeriver: MockKeyDeriving()
)
do {
switch try await mockShielder.shield() {
case .notNeeded:
XCTAssertTrue(true)
case .shielded:
XCTFail("this test should have failed")
}
.store(in: &cancellables)
wait(for: [expectation], timeout: 4)
} catch {
XCTFail("failed with error: \(error)")
}
}
}
@ -130,10 +96,9 @@ class MockShielder: AutoShielder {
}
class MockShieldNotNeeded: AutoShieldingStrategy {
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error> {
Future<AutoShieldingResult, Error> { promise in
promise(.success(AutoShieldingResult.notNeeded))
}
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
try await Task.sleep(nanoseconds: NSEC_PER_MSEC)
return .notNeeded
}
var shouldAutoShield: Bool {
@ -149,41 +114,50 @@ class MockShieldNotNeeded: AutoShieldingStrategy {
}
}
class MockSuccessfulManualStrategy: AutoShieldingStrategy {
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error> {
Future<AutoShieldingResult,Error> { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: {
promise(.success(.shielded(pendingTx: MockPendingTx())))
})
}
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
return .shielded(pendingTx: MockPendingTx())
}
var shouldAutoShield: Bool {
true
}
}
class MockFailedManualStrategy: AutoShieldingStrategy {
/**
throws ShieldFundsError.insuficientTransparentFunds) after 2 seconds
*/
func shield(autoShielder: AutoShielder) -> Future<AutoShieldingResult, Error> {
Future<AutoShieldingResult,Error> { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 2, execute: {
promise(.failure(ShieldFundsError.insuficientTransparentFunds))
})
}
class MockShieldingNotNeededStrategy: AutoShieldingStrategy {
/// throws ShieldFundsError.insuficientTransparentFunds) after 2 seconds
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
return AutoShieldingResult.notNeeded
}
var shouldAutoShield: Bool {
false
}
}
import ZcashLightClientKit
class MockFailedManualStrategy: AutoShieldingStrategy {
/// throws ShieldFundsError.insuficientTransparentFunds) after 2 seconds
func shield(autoShielder: AutoShielder) async throws -> AutoShieldingResult {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * 2)
throw ShieldFundsError.insuficientTransparentFunds
}
var shouldAutoShield: Bool {
true
}
}
struct MockPendingTx: PendingTransactionEntity {
var toAddress = "ztestsapling1vsrxjdmfpwz4yn8y8ux72je2hjqc82u28a5ahycsdldtd95d4mfepfmptqk22tsqxcelzmur6rr"
var fee: ZcashLightClientKit.Zatoshi? = Zatoshi(1000)
var recipient = PendingTransactionRecipient.address(
try! Recipient(
"ztestsapling1vsrxjdmfpwz4yn8y8ux72je2hjqc82u28a5ahycsdldtd95d4mfepfmptqk22tsqxcelzmur6rr",
network: .testnet
)
)
var value = Zatoshi(120000)
var accountIndex: Int = 0
@ -211,8 +185,6 @@ struct MockPendingTx: PendingTransactionEntity {
var id: Int? = 1
var value: Int = 120000
var memo: Data? = nil
var rawTransactionId: Data? = Data()
@ -220,31 +192,23 @@ struct MockPendingTx: PendingTransactionEntity {
}
class MockSuccessfulShieldingCapable: ShieldingCapable {
func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2, execute: {
resultBlock(.success(MockPendingTx()))
})
func shieldFunds(spendingKey: UnifiedSpendingKey, memo: Memo) async throws -> PendingTransactionEntity {
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 2)
return MockPendingTx()
}
}
class MockFailureShieldingCapable: ShieldingCapable {
func shieldFunds(spendingKey: String, transparentSecretKey: String, memo: String?, from accountIndex: Int, resultBlock: @escaping (Result<PendingTransactionEntity, Error>) -> Void) {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2, execute: {
resultBlock(.failure(ShieldFundsError.insuficientTransparentFunds))
})
func shieldFunds(spendingKey: UnifiedSpendingKey, memo: Memo) async throws -> PendingTransactionEntity {
try await Task.sleep(nanoseconds: NSEC_PER_MSEC * 2)
throw ShieldFundsError.insuficientTransparentFunds
}
}
class MockKeyProviding: ShieldingKeyProviding {
func getTransparentSecretKey() throws -> PrivateKeyAccountIndexPair {
("someFakeKey", 0, 0)
}
func getSpendingKey() throws -> PrivateKeyAccountIndexPair {
("someFakeSpendingKey", 0, 0)
func getShieldingKey() throws -> ZcashLightClientKit.UnifiedSpendingKey {
UnifiedSpendingKey(network: .testnet, bytes: [0,0,0], account: 0)
}
}
@ -252,47 +216,19 @@ enum MockError: Error {
case notImplemented
}
class MockKeyDeriving: KeyDeriving {
func deriveViewingKeys(seed: [UInt8], numberOfAccounts: Int) throws -> [String] {
func deriveUnifiedSpendingKey(seed: [UInt8], accountIndex: Int) throws -> ZcashLightClientKit.UnifiedSpendingKey {
throw MockError.notImplemented
}
func deriveViewingKey(spendingKey: String) throws -> String {
static func saplingReceiver(from unifiedAddress: ZcashLightClientKit.UnifiedAddress) throws -> ZcashLightClientKit.SaplingAddress? {
throw MockError.notImplemented
}
func deriveSpendingKeys(seed: [UInt8], numberOfAccounts: Int) throws -> [String] {
static func transparentReceiver(from unifiedAddress: ZcashLightClientKit.UnifiedAddress) throws -> ZcashLightClientKit.TransparentAddress? {
throw MockError.notImplemented
}
func deriveShieldedAddress(seed: [UInt8], accountIndex: Int) throws -> String {
throw MockError.notImplemented
}
func deriveShieldedAddress(viewingKey: String) throws -> String {
throw MockError.notImplemented
}
func deriveTransparentAddress(seed: [UInt8], account: Int, index: Int) throws -> String {
throw MockError.notImplemented
}
func deriveTransparentPrivateKey(seed: [UInt8], account: Int, index: Int) throws -> String {
throw MockError.notImplemented
}
func deriveTransparentAddressFromPrivateKey(_ tsk: String) throws -> String {
"tMockAddressfldkfjarqwer3oiufal"
}
func deriveTransparentAddressFromPublicKey(_ pubkey: String) throws -> String {
throw MockError.notImplemented
}
func deriveUnifiedViewingKeysFromSeed(_ seed: [UInt8], numberOfAccounts: Int) throws -> [UnifiedViewingKey] {
throw MockError.notImplemented
}
func deriveUnifiedAddressFromUnifiedViewingKey(_ uvk: UnifiedViewingKey) throws -> UnifiedAddress {
static func receiverTypecodesFromUnifiedAddress(_ address: ZcashLightClientKit.UnifiedAddress) throws -> [ZcashLightClientKit.UnifiedAddress.ReceiverTypecodes] {
throw MockError.notImplemented
}
}

View File

@ -12,50 +12,41 @@ import MnemonicSwift
@testable import ZcashLightClientKit
class walletTests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testReplyToMemo() {
func testReplyToMemo() throws {
let memo = "Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments!"
let replyTo = "testsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let replyToMemo = SendFlowEnvironment.includeReplyTo(address: replyTo, in: memo)
let expected = memo + "\nReply-To: \(replyTo)"
XCTAssertTrue(replyToMemo.count <= SendFlowEnvironment.maxMemoLength)
XCTAssertEqual(replyToMemo, expected)
let replyTo = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
XCTAssertNoThrow(try SendFlowEnvironment.includeReplyTo(recipient: try Recipient(replyTo, network: .testnet), ownAddress: UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm"), in: memo))
}
func testOnlyReplyToMemo() {
func testOnlyReplyToMemo() throws {
let memo = ""
let replyTo = "testsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let replyToMemo = SendFlowEnvironment.buildMemo(memo: memo, includesMemo: true, replyToAddress: replyTo)
let expected = memo + "\nReply-To: \(replyTo)"
guard replyToMemo != nil else {
XCTFail("memo nil when it shouldn't be")
return }
XCTAssertTrue(replyToMemo!.count <= SendFlowEnvironment.maxMemoLength)
XCTAssertEqual(replyToMemo, expected)
let replyTo = UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm")
// the recipient address is just to determine the type that must be included.
let replyToMemo = try SendFlowEnvironment.buildMemo(recipient: try Recipient("u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm", network: .mainnet), memo: memo, includesMemo: true, replyToAddress: replyTo)
let expected = memo + "\nReply-To: \(replyTo.stringEncoded)"
if case .text(let memoText) = replyToMemo {
XCTAssertEqual(memoText.string, expected)
} else {
XCTFail("Memo is not `.text`")
}
}
func testReplyToHugeMemo() {
func testReplyToHugeMemo() throws {
let memo = "Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments!"
let replyTo = "testsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let replyToMemo = SendFlowEnvironment.includeReplyTo(address: replyTo, in: memo)
let replyTo = "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm"
let replyToMemo = try SendFlowEnvironment.includeReplyTo(recipient: try Recipient("u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm", network: .mainnet), ownAddress: UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm"), in: memo)
let trimmedExpected = "Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have "
let trimmedExpected = "Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Happy Birthday! Have fun spending these ZEC! visit https://paywithz.cash to know all the places that take ZEC payments! Ha"
let expected = trimmedExpected + "\nReply-To: \(replyTo)"
XCTAssertTrue(replyToMemo.count <= SendFlowEnvironment.maxMemoLength)
XCTAssertEqual(replyToMemo, expected)
// XCTAssertEqual(trimmedExpected, replyToMemo.)
if case .text(let memoText) = replyToMemo {
XCTAssertEqual(memoText.string, expected)
} else {
XCTFail("Memo is not `.text`")
}
}
func testKeyPadDecimalLimit() {
@ -110,31 +101,22 @@ class walletTests: XCTestCase {
XCTAssertEqual(try MnemonicSeedProvider.default.toSeed(mnemonic: words).hexString, hex)
}
// func testAlmostIncludesReplyTo() {
// let memo = "this is a test memo"
// let addr = "nowhere"
// let expected = "\(memo)\nReply-To: \(addr)"
// XCTAssertFalse(expected.includesReplyTo)
// XCTAssertNil(expected.replyToAddress)
// }
//
// func testIncludesReplyTo() {
// let memo = "this is a test memo"
// let addr = "zs1gn2ah0zqhsxnrqwuvwmgxpl5h3ha033qexhsz8tems53fw877f4gug353eefd6z8z3n4zxty65c"
// let expected = "\(memo)\nReply-To: \(addr)"
// XCTAssertTrue(expected.includesReplyTo)
// XCTAssertNotNil(expected.replyToAddress)
// }
func testBuildMemo() {
func testBuildMemo() throws {
let memo = "this is a test memo"
let addr = "zs1gn2ah0zqhsxnrqwuvwmgxpl5h3ha033qexhsz8tems53fw877f4gug353eefd6z8z3n4zxty65c"
let expected = "\(memo)\nReply-To: \(addr)"
XCTAssertEqual(expected, SendFlowEnvironment.buildMemo(memo: memo, includesMemo: true, replyToAddress: addr))
XCTAssertEqual(nil, SendFlowEnvironment.buildMemo(memo: "", includesMemo: true, replyToAddress: nil))
XCTAssertEqual(nil, SendFlowEnvironment.buildMemo(memo: memo, includesMemo: false, replyToAddress: addr))
let addr = UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm")
let expected = "\(memo)\nReply-To: \(addr.stringEncoded)"
let replyToMemo = try SendFlowEnvironment.buildMemo(recipient: try Recipient("u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm", network: .mainnet), memo: memo, includesMemo: true, replyToAddress: UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm"))
if case .text(let memoText) = replyToMemo {
XCTAssertEqual(expected, memoText.string)
} else {
XCTFail("Memo is not `.text`")
}
XCTAssertEqual(.empty, try SendFlowEnvironment.buildMemo(recipient: try Recipient("u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm", network: .mainnet), memo: "", includesMemo: false, replyToAddress: UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm")))
XCTAssertEqual(.empty, try SendFlowEnvironment.buildMemo(recipient: try Recipient("u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm", network: .mainnet), memo: memo, includesMemo: false, replyToAddress: UnifiedAddress(validatedEncoding: "u1z9vyk0d0h2k2jwuuk2gfvh5p65qsagkwcgqm6lvh8ratkzjau7stq5snlnkl0eutr687f3wcyn8a0m3n3462c0e4t4cs7m3lvumj2ddm")))
}
func testBlockExplorerUrl() {