diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.pbxproj b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.pbxproj index bf2ebe29..246232ae 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.pbxproj +++ b/Example/ZcashLightClientSample/ZcashLightClientSample.xcodeproj/project.pbxproj @@ -7,10 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 0D2343EE238C91B900606F71 /* sapling-output.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343EC238C91B900606F71 /* sapling-output.params */; }; - 0D2343EF238C91B900606F71 /* sapling-spend.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343ED238C91B900606F71 /* sapling-spend.params */; }; 0D49A18C241698A800CC0649 /* SampleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D49A18B241698A800CC0649 /* SampleLogger.swift */; }; 0D4EBA312396CFD70041B507 /* SendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D4EBA302396CFD70041B507 /* SendViewController.swift */; }; + 0D6CE8BD252E3C4A0005D707 /* SaplingParametersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6CE8BC252E3C4A0005D707 /* SaplingParametersViewController.swift */; }; 0D756A94236C761E009B041B /* GetAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D756A93236C761E009B041B /* GetAddressViewController.swift */; }; 0D7A4A83236CCD88001F4DD8 /* SyncBlocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7A4A82236CCD88001F4DD8 /* SyncBlocksViewController.swift */; }; 0D7C85E523AD5A9B006878FC /* SampleStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7C85E423AD5A9B006878FC /* SampleStorage.swift */; }; @@ -30,8 +29,6 @@ 0D8BB46223B1DA0700D5E2A1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D907F1E2322CC5B00D641FE /* LaunchScreen.storyboard */; }; 0D8BB46323B1DA0700D5E2A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D907F1C2322CC5B00D641FE /* Assets.xcassets */; }; 0D8BB46423B1DA0700D5E2A1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D907F192322CC5900D641FE /* Main.storyboard */; }; - 0D8BB46623B1DA0700D5E2A1 /* sapling-output.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343EC238C91B900606F71 /* sapling-output.params */; }; - 0D8BB46723B1DA0700D5E2A1 /* sapling-spend.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343ED238C91B900606F71 /* sapling-spend.params */; }; 0D907F162322CC5900D641FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D907F152322CC5900D641FE /* AppDelegate.swift */; }; 0D907F182322CC5900D641FE /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D907F172322CC5900D641FE /* ViewController.swift */; }; 0D907F1B2322CC5900D641FE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D907F192322CC5900D641FE /* Main.storyboard */; }; @@ -70,10 +67,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0D2343EC238C91B900606F71 /* sapling-output.params */ = {isa = PBXFileReference; lastKnownFileType = file; name = "sapling-output.params"; path = "../../../ZcashLightClientKitTests/sapling-output.params"; sourceTree = ""; }; - 0D2343ED238C91B900606F71 /* sapling-spend.params */ = {isa = PBXFileReference; lastKnownFileType = file; name = "sapling-spend.params"; path = "../../../ZcashLightClientKitTests/sapling-spend.params"; sourceTree = ""; }; 0D49A18B241698A800CC0649 /* SampleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleLogger.swift; sourceTree = ""; }; 0D4EBA302396CFD70041B507 /* SendViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendViewController.swift; sourceTree = ""; }; + 0D6CE8BC252E3C4A0005D707 /* SaplingParametersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaplingParametersViewController.swift; sourceTree = ""; }; 0D756A93236C761E009B041B /* GetAddressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAddressViewController.swift; sourceTree = ""; }; 0D7A4A82236CCD88001F4DD8 /* SyncBlocksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBlocksViewController.swift; sourceTree = ""; }; 0D7C85E423AD5A9B006878FC /* SampleStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleStorage.swift; sourceTree = ""; }; @@ -157,6 +153,14 @@ path = Send; sourceTree = ""; }; + 0D6CE8BB252E3C1A0005D707 /* Sapling Parameters */ = { + isa = PBXGroup; + children = ( + 0D6CE8BC252E3C4A0005D707 /* SaplingParametersViewController.swift */, + ); + path = "Sapling Parameters"; + sourceTree = ""; + }; 0D756A92236C75FE009B041B /* Get Address */ = { isa = PBXGroup; children = ( @@ -200,13 +204,12 @@ 0D907F142322CC5900D641FE /* ZcashLightClientSample */ = { isa = PBXGroup; children = ( + 0D6CE8BB252E3C1A0005D707 /* Sapling Parameters */, 0DBF8F9323A80F0E0010B85F /* Transaction Detail */, 0DF53E6523A438BA00D7249C /* Paginated Transactions */, 0DA58B922397DDBC004596EA /* List Transactions */, 0D4EBA2F2396CFBE0041B507 /* Send */, 0DCD3DC5238D888B00DD3EC4 /* Get Balance */, - 0D2343EC238C91B900606F71 /* sapling-output.params */, - 0D2343ED238C91B900606F71 /* sapling-spend.params */, 0D7A4A81236CCCDB001F4DD8 /* Sync Blocks */, 0D756A92236C75FE009B041B /* Get Address */, 0DDFB33A236B733700AED892 /* Latest Block Height */, @@ -441,8 +444,6 @@ 0D8BB46223B1DA0700D5E2A1 /* LaunchScreen.storyboard in Resources */, 0D8BB46323B1DA0700D5E2A1 /* Assets.xcassets in Resources */, 0D8BB46423B1DA0700D5E2A1 /* Main.storyboard in Resources */, - 0D8BB46623B1DA0700D5E2A1 /* sapling-output.params in Resources */, - 0D8BB46723B1DA0700D5E2A1 /* sapling-spend.params in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -453,8 +454,6 @@ 0D907F202322CC5B00D641FE /* LaunchScreen.storyboard in Resources */, 0D907F1D2322CC5B00D641FE /* Assets.xcassets in Resources */, 0D907F1B2322CC5900D641FE /* Main.storyboard in Resources */, - 0D2343EE238C91B900606F71 /* sapling-output.params in Resources */, - 0D2343EF238C91B900606F71 /* sapling-spend.params in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -668,6 +667,7 @@ 0D7A4A83236CCD88001F4DD8 /* SyncBlocksViewController.swift in Sources */, 0DDFB33E236B844900AED892 /* DemoAppConfig.swift in Sources */, 0D907F162322CC5900D641FE /* AppDelegate.swift in Sources */, + 0D6CE8BD252E3C4A0005D707 /* SaplingParametersViewController.swift in Sources */, 0D7C85E523AD5A9B006878FC /* SampleStorage.swift in Sources */, 0D49A18C241698A800CC0649 /* SampleLogger.swift in Sources */, 0DA58B962397F2CB004596EA /* TransactionsDataSource.swift in Sources */, diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift index 3485e499..62f7b63b 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/AppDelegate.swift @@ -167,14 +167,15 @@ func __pendingDbURL() throws -> URL { } func __spendParamsURL() throws -> URL { - Bundle.main.url(forResource: "sapling-spend", withExtension: ".params")! + try __documentsDirectory().appendingPathComponent("sapling-spend.params") } func __outputParamsURL() throws -> URL { - Bundle.main.url(forResource: "sapling-output", withExtension: ".params")! + try __documentsDirectory().appendingPathComponent("sapling-output.params") } + public extension NotificationBubble { static func sucessOptions(animation: NotificationBubble.Animation) -> [NotificationBubble.Style] { return [ NotificationBubble.Style.animation(animation), diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard b/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard index 5f1cfc31..968843ce 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard @@ -1,9 +1,11 @@ - + - + + + @@ -14,7 +16,7 @@ - + @@ -238,6 +240,26 @@ + + + + + + + + + + + + + + @@ -250,7 +272,7 @@ - - + + @@ -729,7 +752,6 @@ - @@ -762,7 +784,7 @@ - + + - @@ -799,6 +821,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -812,7 +909,7 @@ - + @@ -821,15 +918,15 @@ @@ -852,7 +949,7 @@ - - + @@ -883,14 +980,14 @@ - + + - @@ -928,7 +1025,8 @@ - + + @@ -937,7 +1035,6 @@ - @@ -1011,14 +1108,14 @@ - + + - @@ -1035,4 +1132,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift new file mode 100644 index 00000000..3aa0f94e --- /dev/null +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Sapling Parameters/SaplingParametersViewController.swift @@ -0,0 +1,122 @@ +// +// SaplingParametersViewController.swift +// ZcashLightClientSample +// +// Created by Francisco Gindre on 10/7/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import UIKit +import ZcashLightClientKit +class SaplingParametersViewController: UIViewController { + @IBOutlet weak var outputPath: UILabel! + @IBOutlet weak var spendPath: UILabel! + @IBOutlet weak var downloadButton: UIButton! + @IBOutlet weak var deleteButton: UIButton! + override func viewDidLoad() { + super.viewDidLoad() + let spendParamPath = try! __spendParamsURL().path + let outputParamPath = try! __outputParamsURL().path + // Do any additional setup after loading the view. + self.spendPath.text = spendParamPath + self.outputPath.text = outputParamPath + self.updateColor() + self.spendPath.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(spendPathTapped(_:)))) + self.outputPath.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(outputPathTapped(_:)))) + self.outputPath.isUserInteractionEnabled = true + self.spendPath.isUserInteractionEnabled = true + + self.updateButtons() + } + func updateButtons() { + let spendParamPath = try! __spendParamsURL().path + let outputParamPath = try! __outputParamsURL().path + self.downloadButton.isHidden = fileExists(outputParamPath) && fileExists(spendParamPath) + self.deleteButton.isHidden = !(fileExists(outputParamPath) || fileExists(spendParamPath)) + } + func updateColor() { + let spendParamPath = try! __spendParamsURL().path + let outputParamPath = try! __outputParamsURL().path + self.spendPath.textColor = fileExists(spendParamPath) ? UIColor.green : UIColor.red + self.outputPath.textColor = fileExists(outputParamPath) ? UIColor.green : UIColor.red + } + @objc func spendPathTapped(_ gesture: UIGestureRecognizer) { + loggerProxy.event("copied to clipboard:\(self.spendPath.text ?? "")") + UIPasteboard.general.string = self.spendPath.text + let alert = UIAlertController(title: "", message: "Path Copied to clipboard", preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + + @objc func outputPathTapped(_ gesture: UIGestureRecognizer) { + loggerProxy.event("copied to clipboard:\(self.outputPath.text ?? "")") + UIPasteboard.general.string = self.outputPath.text + let alert = UIAlertController(title: "", message: "Path Copied to clipboard", preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + + @IBAction func download(_ sender: Any) { + let outputParameter = try! __outputParamsURL() + let spendParameter = try! __spendParamsURL() + if !FileManager.default.isReadableFile(atPath: outputParameter.absoluteString) { + SaplingParameterDownloader.downloadOutputParameter(outputParameter) { [weak self] result in + guard let self = self else { return } + DispatchQueue.main.async { + switch result{ + case .success: + self.updateButtons() + self.updateColor() + case .failure(let error): + self.showError(error) + } + } + } + } + + if !FileManager.default.isReadableFile(atPath: spendParameter.absoluteString) { + SaplingParameterDownloader.downloadSpendParameter(try! __spendParamsURL()) { [weak self] result in + guard let self = self else { return } + DispatchQueue.main.async { + switch result{ + case .success: + self.updateButtons() + self.updateColor() + case .failure(let error): + self.showError(error) + } + } + } + } + } + + func fileExists(_ path: String) -> Bool { + (try? FileManager.default.attributesOfItem(atPath: path)) != nil + } + + func showError(_ error: Error) { + let alert = UIAlertController(title: "Download Failed", message: "\(error)", preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + + @IBAction func deleteFiles(_ sender: Any) { + let spendParamURL = try! __spendParamsURL() + let outputParamURL = try! __outputParamsURL() + + try? FileManager.default.removeItem(at: spendParamURL) + try? FileManager.default.removeItem(at: outputParamURL) + self.updateColor() + self.updateButtons() + } + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/ZcashLightClientKit/Initializer.swift b/ZcashLightClientKit/Initializer.swift index 9b1c4c09..3fe0a05b 100644 --- a/ZcashLightClientKit/Initializer.swift +++ b/ZcashLightClientKit/Initializer.swift @@ -225,6 +225,14 @@ public class Initializer { public func blockProcessor() -> CompactBlockProcessor? { self.processor } + + func isSpendParameterPresent() -> Bool { + FileManager.default.isReadableFile(atPath: self.spendParamsURL.absoluteString) + } + + func isOutputParameterPresent() -> Bool { + FileManager.default.isExecutableFile(atPath: self.outputParamsURL.absoluteString) + } } class CompactBlockProcessorBuilder { diff --git a/ZcashLightClientKit/Stencil/WalletBirthday+saplingtree.stencil b/ZcashLightClientKit/Stencil/WalletBirthday+saplingtree.stencil index 43a40590..80ee02ef 100644 --- a/ZcashLightClientKit/Stencil/WalletBirthday+saplingtree.stencil +++ b/ZcashLightClientKit/Stencil/WalletBirthday+saplingtree.stencil @@ -229,13 +229,20 @@ public extension WalletBirthday { time: 1599946198, tree: "01d9e6147caab719ae68cb20d976c78437634e2c999ef3a09c6ba35086d443703d00120001019d135be7b1db088c68bd76703ec2b45066bb1761619745362e61dcf55f644601d0c8f296479a73722c2e2a260ab7017b9a9e6d084b651289cfe6d3c7a00ff54e01f853ab39dbfc81e2aabefd231d3374ff794028168c725ad465e61205692fef4b014f13b6e4475cbd004b4d95aa8205ed7338224e13627ecbb19afd1937dcbc0818000185b4f1ddee3199cd1f7913b223c01c4623cd9d0e1b47df4e36aaca7717b6331f011d6c8ec914cc312ef0962d52240308b22a647f4cbd2d7c2fd420ad5fbcae5619011e43cbb05b8efc885531367e5f611fe7ce7514131be892cce3adad02e151f72b01f0d7e0d589c7e5f8fff0bdd5037aeb5d5d818d413262758c9915ded705e40f70000101d26ff60e77e23fb86a52da565c22d76f81df7f25d543ec0e58a0d692d4be2700000110b2bfd32a99e0b982a41a6dbaebf783bdb9d6af795f5f20056ce7317d15ce1101f1c57245fff8dbc2d3efe5a0953eafdedeb06e18a3ad4f1e4042ee76623f803200011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625" ) - default: - return WalletBirthday( + case 980000 ..< 998500: + return WalletBirthday( height: 980000, hash: "00000000005de60d31b653cdf1637f1bad62af844c6c51f38557a4e8bb74e2d7", time: 1600700163, tree: "0184330bda72e9596256847a9597d7d9476aa3d69d9dad2149314751e708da206601cd8a1a61df1e80514cd2c0a7faac8b8d7ce27d6d96bb63cb7c61c1f33e7c654312014098788b75f26108d93f0429202ffffb6cf9ffffd7278383e4d8ad2af642264600011c035ed934a11c1b48e24b6be9b2d483e7747dd082f06abf75f9a092b34cd33c01be00394a99bd33304fc4343cd928857ae7c09c176452f40d9815f9ff3ba3865d013bd7218072e1588aea0568198d37e0d83ba75f155c863355974c4eb864de0103019ed27335bc5452e320ec22a30cfe61508929016157ff2a555181a9a0623e725801328208bea2c5c83487effab780cdb36b4b82e6e7290b06d98817a160f3d79d2800019ed6779a1724a107807baf4dda9481fb940f50d85db701dda43a0989c2d62535000001d1e806194dbe171d4ad1ef8c73c1a469130caced0e24b04b8acef91c42be7a56000107771e04f7d6371bfda40ef9e04419a25c6563dcd359c85bd501de28c3c7f3250110b2bfd32a99e0b982a41a6dbaebf783bdb9d6af795f5f20056ce7317d15ce1101f1c57245fff8dbc2d3efe5a0953eafdedeb06e18a3ad4f1e4042ee76623f803200011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625" ) + default: + return WalletBirthday( + height: 998500, + hash: "00000000014cf0915c4f105140e846c62ca6f2e321f4f717cf6762a35c6e8eb4", + time: 1602093677, + tree: "01b86ad8964b68a9a316a3038af14ac9e557a6eed614ee5d337c9fdb8887e9dc6001b71fbc815cc014a7f68648fcad5b9b72920b3d21cce406b5f593a0eab6d91f1212000001aede58c0641825b7531a8b296b9fdc7393090b87588893e8dd5160bd59b2e16300000000014fa30e43b641cf67c84cef81083b81bec3bd677f55def5c7bd948298e64690560134ac55ded091faef2ae8a1043704dfec41b1a95c4cedf1818574752bc40f2033000103ee02ae59c6688dcaadf1c4ff95e7b1a902837e4989a4c4994dce7dac6ecb20014ff8c0fe6bce02ac4ad684996bfa931d61c724015d797642819361d611ebd61201c7ae83949d9502b0eff10618124d335f046e4aae52c19ccad5567feceb342a5200000001b7fc5791e3650729b7e1e38ee8c4ea9da612a07b4bf412cefaffbab7ac74c547011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625" + ) } } } diff --git a/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift b/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift new file mode 100644 index 00000000..da5d4abc --- /dev/null +++ b/ZcashLightClientKit/Utils/SaplingParameterDownloader.swift @@ -0,0 +1,57 @@ +// +// SaplingParameterDownloader.swift +// ZcashLightClientKit +// +// Created by Francisco Gindre on 10/7/20. +// + +import Foundation + +public class SaplingParameterDownloader { + public enum Errors: Error { + case invalidURL(url: String) + case failed(error: Error) + } + + public static func downloadSpendParameter(_ at: URL, result: @escaping (Result) -> Void) { + + guard let url = URL(string: spendParamsURLString) else { + result(.failure(Errors.invalidURL(url: spendParamsURLString))) + return + } + downloadFileWithRequest(URLRequest(url: url), at: at, result: result) + } + + public static func downloadOutputParameter(_ at: URL, result: @escaping (Result) -> Void) { + guard let url = URL(string: outputParamsURLString) else { + result(.failure(Errors.invalidURL(url: outputParamsURLString))) + return + } + downloadFileWithRequest(URLRequest(url: url), at: at, result: result) + } + + private static func downloadFileWithRequest(_ request: URLRequest, at destination: URL, result: @escaping (Result) -> Void) { + let task = URLSession.shared.downloadTask(with: request) { (url, _, error) in + if let e = error { + result(.failure(Errors.failed(error: e))) + return + } else if let localUrl = url { + do { + try FileManager.default.moveItem(at: localUrl, to: destination) + result(.success(destination)) + } catch { + result(.failure(error)) + } + } + } + task.resume() + } + + static var spendParamsURLString: String { + return ZcashSDK.CLOUD_PARAM_DIR_URL + ZcashSDK.SPEND_PARAM_FILE_NAME + } + + static var outputParamsURLString: String { + return ZcashSDK.CLOUD_PARAM_DIR_URL + ZcashSDK.OUTPUT_PARAM_FILE_NAME + } +} diff --git a/ZcashLightClientKitTests/utils/Tests+Utils.swift b/ZcashLightClientKitTests/utils/Tests+Utils.swift index 7f317234..3b179ddd 100644 --- a/ZcashLightClientKitTests/utils/Tests+Utils.swift +++ b/ZcashLightClientKitTests/utils/Tests+Utils.swift @@ -67,11 +67,11 @@ func __dataDbURL() throws -> URL { } func __spendParamsURL() throws -> URL { - URL(string: Bundle.testBundle.url(forResource: "sapling-spend", withExtension: "params")!.path)! + try __documentsDirectory().appendingPathComponent("sapling-spend.params") } func __outputParamsURL() throws -> URL { - URL(string: Bundle.testBundle.url(forResource: "sapling-output", withExtension: "params")!.path)! + try __documentsDirectory().appendingPathComponent("sapling-output.params") } func copyParametersToDocuments() throws -> (spend: URL, output: URL) {