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:
parent
09d2879198
commit
31033b4d08
|
@ -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
|
||||
|
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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" */ = {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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")
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()))
|
||||
]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -115,7 +115,9 @@ struct Sending: View {
|
|||
}
|
||||
.onAppear() {
|
||||
tracker.track(.screen(screen: .sendFinal), properties: [:])
|
||||
self.flow.preSend()
|
||||
Task { @MainActor in
|
||||
await self.flow.preSend()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -62,7 +62,8 @@
|
|||
|
||||
// Address Screen
|
||||
"address_screen" = "당신의 주소";
|
||||
"address_shielded" = "당신의 쉴드된 주소";
|
||||
"address_unified" = "통합 주소";
|
||||
"address_sapling" = "묘목 주소";
|
||||
"address_transparent" = "당신의 투명 주소";
|
||||
"feedback_addresscopied" = "주소 복사";
|
||||
"Request ZEC" = "ZEC 부탁";
|
||||
|
|
|
@ -34,7 +34,8 @@
|
|||
|
||||
// Address Screen
|
||||
"address_screen" = "Ваш адрес";
|
||||
"address_shielded" = "Ваш защищённый адрес";
|
||||
"address_unified" = "Ваш единый адрес";
|
||||
"address_sapling" = "Ваш sapling адрес";
|
||||
"address_transparent" = "Ваш прозрачный адрес";
|
||||
"feedback_addresscopied" = "Адрес скопирован!";
|
||||
"Request ZEC" = "Запросить сумму в ZEC";
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
// Address Screen
|
||||
"button_wallethistory" = "钱包历史";
|
||||
"address_screen" = "您的地址";
|
||||
"address_shielded" = "您的隐蔽地址";
|
||||
"address_unified" = "您的统一地址";
|
||||
"address_sapling" = "你的树苗地址";
|
||||
"address_transparent" = "您的透明地址";
|
||||
"feedback_addresscopied" = "已复制地址";
|
||||
"Request ZEC" = "请求 ZEC";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue