diff --git a/Mixin/UserInterface/Controllers/Common/LoneBackButtonNavigationController.swift b/Mixin/UserInterface/Controllers/Common/LoneBackButtonNavigationController.swift index e97b5266d1..f522f812f9 100644 --- a/Mixin/UserInterface/Controllers/Common/LoneBackButtonNavigationController.swift +++ b/Mixin/UserInterface/Controllers/Common/LoneBackButtonNavigationController.swift @@ -52,6 +52,13 @@ class LoneBackButtonNavigationController: UINavigationController { return super.popToRootViewController(animated: animated) } + override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + defer { + updateBackButtonAlpha(animated: animated) + } + super.setViewControllers(viewControllers, animated: animated) + } + @objc func backAction(sender: Any) { popViewController(animated: true) } diff --git a/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift b/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift index fc74ebbc18..7b2d0e7fae 100644 --- a/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift +++ b/Mixin/UserInterface/Controllers/Setting/Diagnose/TIPDiagnosticViewController.swift @@ -7,9 +7,11 @@ class TIPDiagnosticViewController: SettingsTableViewController { private let dataSource = SettingsDataSource(sections: [ SettingsSection(header: "Failure Tests", rows: [ SettingsRow(title: "Fail Last Sign Once", accessory: .switch(isOn: TIPDiagnostic.failLastSignerOnce, isEnabled: true)), - SettingsRow(title: "Fail PIN Update Once", accessory: .switch(isOn: TIPDiagnostic.failPINUpdateOnce, isEnabled: true)), + SettingsRow(title: "Fail PIN Update Server Once", accessory: .switch(isOn: TIPDiagnostic.failPINUpdateServerSideOnce, isEnabled: true)), + SettingsRow(title: "Fail PIN Update Client Once", accessory: .switch(isOn: TIPDiagnostic.failPINUpdateClientSideOnce, isEnabled: true)), SettingsRow(title: "Fail Watch Once", accessory: .switch(isOn: TIPDiagnostic.failCounterWatchOnce, isEnabled: true)), SettingsRow(title: "Crash After PIN Update", accessory: .switch(isOn: TIPDiagnostic.crashAfterUpdatePIN, isEnabled: true)), + SettingsRow(title: "Invalid Nonce Once", accessory: .switch(isOn: TIPDiagnostic.invalidNonceOnce, isEnabled: true)), ]), SettingsSection(header: "UI Test", rows: [ SettingsRow(title: "UI Test On", accessory: .switch(isOn: TIPDiagnostic.uiTestOnly, isEnabled: true)), @@ -37,11 +39,15 @@ class TIPDiagnosticViewController: SettingsTableViewController { case dataSource.sections[0].rows[0]: TIPDiagnostic.failLastSignerOnce.toggle() case dataSource.sections[0].rows[1]: - TIPDiagnostic.failPINUpdateOnce.toggle() + TIPDiagnostic.failPINUpdateServerSideOnce.toggle() case dataSource.sections[0].rows[2]: - TIPDiagnostic.failCounterWatchOnce.toggle() + TIPDiagnostic.failPINUpdateClientSideOnce.toggle() case dataSource.sections[0].rows[3]: + TIPDiagnostic.failCounterWatchOnce.toggle() + case dataSource.sections[0].rows[4]: TIPDiagnostic.crashAfterUpdatePIN.toggle() + case dataSource.sections[0].rows[5]: + TIPDiagnostic.invalidNonceOnce.toggle() case dataSource.sections[1].rows[0]: TIPDiagnostic.uiTestOnly.toggle() default: diff --git a/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift b/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift index 77bb1b1db5..1df6c73819 100644 --- a/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/TIPActionViewController.swift @@ -63,6 +63,9 @@ class TIPActionViewController: UIViewController { } private func performAction() { + guard let accountCounterBefore = LoginManager.shared.account?.tipCounter else { + return + } switch action { case let .create(pin): titleLabel.text = R.string.localizable.create_pin() @@ -84,7 +87,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } case let .change(old, new): @@ -119,7 +122,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } case let .migrate(pin): @@ -142,7 +145,7 @@ class TIPActionViewController: UIViewController { finish() } } catch { - await handle(error: error) + await handle(error: error, accountCounterBefore: accountCounterBefore) } } } @@ -165,22 +168,28 @@ class TIPActionViewController: UIViewController { } } - private func handle(error: Error) async { + private func handle(error: Error, accountCounterBefore: UInt64) async { Logger.tip.error(category: "TIPAction", message: "Failed with: \(error)") do { - guard let account = LoginManager.shared.account else { - return - } - guard let context = try await TIP.checkCounter(with: account) else { + if let context = try await TIP.checkCounter() { await MainActor.run { - Logger.tip.error(category: "TIPAction", message: "No interruption is detected") - finish() + let intro = TIPIntroViewController(context: context) + navigationController?.setViewControllers([intro], animated: true) + } + } else { + try await MainActor.run { + guard let accountCounterAfter = LoginManager.shared.account?.tipCounter else { + throw MixinAPIError.unauthorized + } + if accountCounterAfter == accountCounterBefore { + Logger.tip.error(category: "TIPAction", message: "Nothing changed") + let intro = TIPIntroViewController(action: action, changedNothingWith: error) + tipNavigationController?.setViewControllers([intro], animated: true) + } else { + Logger.tip.warn(category: "TIPAction", message: "No interruption is detected") + finish() + } } - return - } - await MainActor.run { - let intro = TIPIntroViewController(context: context) - navigationController?.setViewControllers([intro], animated: true) } } catch { await MainActor.run { @@ -234,7 +243,7 @@ class TIPActionViewController: UIViewController { } } DispatchQueue.main.asyncAfter(deadline: .now() + 13) { - if TIPDiagnostic.failLastSignerOnce || TIPDiagnostic.failPINUpdateOnce { + if TIPDiagnostic.failLastSignerOnce || TIPDiagnostic.failPINUpdateServerSideOnce { let action: TIP.Action switch self.action { case .create: @@ -249,8 +258,8 @@ class TIPActionViewController: UIViewController { if TIPDiagnostic.failLastSignerOnce { TIPDiagnostic.failLastSignerOnce = false situation = .pendingSign([]) - } else if TIPDiagnostic.failPINUpdateOnce { - TIPDiagnostic.failPINUpdateOnce = false + } else if TIPDiagnostic.failPINUpdateServerSideOnce { + TIPDiagnostic.failPINUpdateServerSideOnce = false situation = .pendingUpdate } else { fatalError() diff --git a/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift b/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift index b2e801a482..b56406e4ba 100644 --- a/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/TIPIntroViewController.swift @@ -7,7 +7,8 @@ class TIPIntroViewController: UIViewController { enum Interruption { case unknown case none - case confirmed(TIP.InterruptionContext) + case inputNeeded(TIP.InterruptionContext) + case noInputNeeded(TIPActionViewController.Action, Error) } private enum Status { @@ -26,6 +27,15 @@ class TIPIntroViewController: UIViewController { @IBOutlet weak var noticeTextViewHeightConstraint: NSLayoutConstraint! + var isDismissAllowed: Bool { + switch interruption { + case .none, .noInputNeeded: + return true + case .unknown, .inputNeeded: + return false + } + } + private let intent: TIP.Action private let checkCounterTimeoutInterval: TimeInterval = 5 @@ -35,26 +45,41 @@ class TIPIntroViewController: UIViewController { navigationController as? TIPNavigationViewController } - init(intent: TIP.Action) { + convenience init(intent: TIP.Action) { Logger.tip.info(category: "TIPIntro", message: "Init with intent: \(intent)") - self.intent = intent - self.interruption = .unknown - let nib = R.nib.tipIntroView - super.init(nibName: nib.name, bundle: nib.bundle) + self.init(intent: intent, interruption: .unknown) } - init(context: TIP.InterruptionContext) { + convenience init(context: TIP.InterruptionContext) { Logger.tip.info(category: "TIPIntro", message: "Init with context: \(context)") - self.intent = context.action - self.interruption = .confirmed(context) - let nib = R.nib.tipIntroView - super.init(nibName: nib.name, bundle: nib.bundle) + self.init(intent: context.action, interruption: .inputNeeded(context)) + } + + convenience init(action: TIPActionViewController.Action, changedNothingWith error: Error) { + Logger.tip.info(category: "TIPIntro", message: "Init with action: \(action.debugDescription), error: \(error)") + let intent: TIP.Action + switch action { + case .create: + intent = .create + case .change: + intent = .change + case .migrate: + intent = .migrate + } + self.init(intent: intent, interruption: .noInputNeeded(action, error)) } required init?(coder: NSCoder) { fatalError("Storyboard is not supported") } + private init(intent: TIP.Action, interruption: Interruption) { + self.intent = intent + self.interruption = interruption + let nib = R.nib.tipIntroView + super.init(nibName: nib.name, bundle: nib.bundle) + } + override func viewDidLoad() { super.viewDidLoad() contentStackView.setCustomSpacing(24, after: iconImageView) @@ -67,7 +92,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_creation_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.creating_wallet_terminated_unexpectedly() } setNoticeHidden(false) @@ -76,7 +101,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.changing_pin_terminated_unexpectedly() } setNoticeHidden(false) @@ -85,7 +110,7 @@ class TIPIntroViewController: UIViewController { switch interruption { case .unknown, .none: description = R.string.localizable.tip_introduction() - case .confirmed: + case .inputNeeded, .noInputNeeded: description = R.string.localizable.upgrading_tip_terminated_unexpectedly() } setNoticeHidden(false) @@ -103,11 +128,11 @@ class TIPIntroViewController: UIViewController { case .unknown: checkCounter() descriptionTextLabel.additionalLinksMap = linksMap - case .confirmed: - updateNextButtonAndStatusLabel(with: .waitingForUser) case .none: descriptionTextLabel.additionalLinksMap = linksMap updateNextButtonAndStatusLabel(with: .waitingForUser) + case .inputNeeded, .noInputNeeded: + updateNextButtonAndStatusLabel(with: .waitingForUser) } } @@ -148,7 +173,7 @@ class TIPIntroViewController: UIViewController { })) present(validator, animated: true) } - case .confirmed(let context): + case .inputNeeded(let context): switch context.action { case .migrate: let validator = TIPPopupInputViewController(action: .migrate({ pin in @@ -165,6 +190,9 @@ class TIPIntroViewController: UIViewController { })) present(validator, animated: true) } + case let .noInputNeeded(action, _): + let viewController = TIPActionViewController(action: action) + navigationController?.setViewControllers([viewController], animated: true) } } @@ -217,14 +245,11 @@ extension TIPIntroViewController { } private func checkCounter() { - guard let account = LoginManager.shared.account else { - return - } updateNextButtonAndStatusLabel(with: .checkingCounter) Logger.tip.info(category: "TIPIntro", message: "Checking counter") Task { do { - let context = try await TIP.checkCounter(with: account, timeoutInterval: checkCounterTimeoutInterval) + let context = try await TIP.checkCounter(timeoutInterval: checkCounterTimeoutInterval) await MainActor.run { Logger.tip.info(category: "TIPIntro", message: "Got context: \(String(describing: context))") if let context = context { @@ -232,6 +257,7 @@ extension TIPIntroViewController { navigationController?.setViewControllers([intro], animated: true) } else { interruption = .none + tipNavigationController?.updateBackButtonAlpha(animated: true) updateNextButtonAndStatusLabel(with: .waitingForUser) } } @@ -260,11 +286,20 @@ extension TIPIntroViewController { switch interruption { case .unknown, .none: setNextButtonTitleByIntent() - case .confirmed: + actionDescriptionLabel.text = nil + case .inputNeeded: nextButton.setTitle(R.string.localizable.continue(), for: .normal) + actionDescriptionLabel.text = nil + case let .noInputNeeded(_, error): + nextButton.setTitle(R.string.localizable.retry(), for: .normal) + if let error = error as? TIPNode.Error { + actionDescriptionLabel.text = error.description + } else { + actionDescriptionLabel.text = error.localizedDescription + } + actionDescriptionLabel.textColor = .mixinRed } nextButton.isBusy = false - actionDescriptionLabel.text = nil } } diff --git a/Mixin/UserInterface/Controllers/Wallet/TIPNavigationViewController.swift b/Mixin/UserInterface/Controllers/Wallet/TIPNavigationViewController.swift index fe7925d3fb..9467a149f6 100644 --- a/Mixin/UserInterface/Controllers/Wallet/TIPNavigationViewController.swift +++ b/Mixin/UserInterface/Controllers/Wallet/TIPNavigationViewController.swift @@ -52,9 +52,9 @@ class TIPNavigationViewController: LoneBackButtonNavigationController { if viewControllers.last is TIPActionViewController { backButtonAlpha = 0 dismissButtonAlpha = 0 - } else if viewControllers.last is TIPIntroViewController { + } else if let intro = viewControllers.last as? TIPIntroViewController { backButtonAlpha = 0 - dismissButtonAlpha = 1 + dismissButtonAlpha = intro.isDismissAllowed ? 1 : 0 } else { backButtonAlpha = 1 dismissButtonAlpha = 0 diff --git a/Mixin/UserInterface/Windows/UrlWindow.swift b/Mixin/UserInterface/Windows/UrlWindow.swift index b31166aab3..e31291e42b 100644 --- a/Mixin/UserInterface/Windows/UrlWindow.swift +++ b/Mixin/UserInterface/Windows/UrlWindow.swift @@ -414,7 +414,7 @@ class UrlWindow { guard let assetId = query["asset"], let amount = query["amount"], let traceId = query["trace"], let addressId = query["address"] else { return false } - guard !assetId.isEmpty && UUID(uuidString: assetId) != nil && !traceId.isEmpty && UUID(uuidString: traceId) != nil && !addressId.isEmpty && UUID(uuidString: addressId) != nil && !amount.isEmpty else { + guard !assetId.isEmpty && UUID(uuidString: assetId) != nil && !traceId.isEmpty && UUID(uuidString: traceId) != nil && !addressId.isEmpty && UUID(uuidString: addressId) != nil && !amount.isEmpty && AmountFormatter.isValid(amount) else { return false } var memo = query["memo"] @@ -484,7 +484,7 @@ class UrlWindow { showAutoHiddenHud(style: .error, text: R.string.localizable.invalid_payment_link()) return true } - guard !recipientId.isEmpty && UUID(uuidString: recipientId) != nil && !assetId.isEmpty && UUID(uuidString: assetId) != nil && !amount.isEmpty && amount.isGenericNumber else { + guard !recipientId.isEmpty && UUID(uuidString: recipientId) != nil && !assetId.isEmpty && UUID(uuidString: assetId) != nil && !amount.isEmpty && amount.isGenericNumber && AmountFormatter.isValid(amount) else { Logger.general.error(category: "PayURL", message: "Invalid URL: \(url)") showAutoHiddenHud(style: .error, text: R.string.localizable.invalid_payment_link()) return true diff --git a/MixinServices/MixinServices/Foundation/AmountFormatter.swift b/MixinServices/MixinServices/Foundation/AmountFormatter.swift index 11c4e93649..3edb85b1da 100644 --- a/MixinServices/MixinServices/Foundation/AmountFormatter.swift +++ b/MixinServices/MixinServices/Foundation/AmountFormatter.swift @@ -24,4 +24,15 @@ public enum AmountFormatter { } } + public static func isValid(_ amount: String) -> Bool { + let parts = amount.components(separatedBy: ".") + if parts.count == 1 { + return true + } else if parts.count == 2 { + return parts[1].count <= 8 + } else { + return false + } + } + } diff --git a/MixinServices/MixinServices/Services/API/AccountAPI.swift b/MixinServices/MixinServices/Services/API/AccountAPI.swift index 874ae2832a..c18cbdfeac 100644 --- a/MixinServices/MixinServices/Services/API/AccountAPI.swift +++ b/MixinServices/MixinServices/Services/API/AccountAPI.swift @@ -70,6 +70,12 @@ public final class AccountAPI: MixinAPI { request(method: .get, path: Path.me, completion: completion) } + public static func me() async throws -> Account { + try await withCheckedThrowingContinuation { continuation in + me(completion: continuation.resume(with:)) + } + } + @discardableResult public static func sendCode(to phoneNumber: String, captchaToken: CaptchaToken?, purpose: VerificationPurpose, completion: @escaping (MixinAPI.Result) -> Void) -> Request? { var param = ["phone": phoneNumber, diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift index 72a9f2fcc1..ed01e26692 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIP.swift @@ -161,15 +161,19 @@ extension TIP { let request = PINRequest(pin: new, oldPIN: oldEncryptedPIN, timestamp: nil) #if DEBUG try await MainActor.run { - if TIPDiagnostic.failPINUpdateOnce { - TIPDiagnostic.failPINUpdateOnce = false + if TIPDiagnostic.failPINUpdateServerSideOnce { + TIPDiagnostic.failPINUpdateServerSideOnce = false throw MixinAPIError.httpTransport(.sessionTaskFailed(error: URLError(.badServerResponse))) } } #endif let account = try await AccountAPI.updatePIN(request: request) #if DEBUG - await MainActor.run { + try await MainActor.run { + if TIPDiagnostic.failPINUpdateClientSideOnce { + TIPDiagnostic.failPINUpdateClientSideOnce = false + throw MixinAPIError.httpTransport(.sessionTaskFailed(error: URLError(.badServerResponse))) + } if TIPDiagnostic.crashAfterUpdatePIN { abort() } @@ -240,18 +244,24 @@ extension TIP { let oldPIN = try encryptTIPPIN(tipPriv: aggSig, target: timestamp) let newEncryptPIN = try encryptPIN(key: pinToken, code: pub + (counter + 1).data(endianness: .big)) let request = PINRequest(pin: newEncryptPIN, oldPIN: oldPIN, timestamp: nil) + AppGroupKeychain.tipPriv = nil + Logger.tip.info(category: "TIP", message: "TIP Priv is removed") #if DEBUG try await MainActor.run { - if TIPDiagnostic.failPINUpdateOnce { - TIPDiagnostic.failPINUpdateOnce = false + if TIPDiagnostic.failPINUpdateServerSideOnce { + TIPDiagnostic.failPINUpdateServerSideOnce = false throw MixinAPIError.httpTransport(.sessionTaskFailed(error: URLError(.badServerResponse))) } } #endif - AppGroupKeychain.tipPriv = nil - Logger.tip.info(category: "TIP", message: "TIP Priv is removed") let account = try await AccountAPI.updatePIN(request: request) #if DEBUG + try await MainActor.run { + if TIPDiagnostic.failPINUpdateClientSideOnce { + TIPDiagnostic.failPINUpdateClientSideOnce = false + throw MixinAPIError.httpTransport(.sessionTaskFailed(error: URLError(.badServerResponse))) + } + } await MainActor.run { if TIPDiagnostic.crashAfterUpdatePIN { abort() @@ -268,7 +278,16 @@ extension TIP { return aggSig } - public static func checkCounter(with account: Account, timeoutInterval: TimeInterval = 15) async throws -> InterruptionContext? { + public static func checkCounter(with freshAccount: Account? = nil, timeoutInterval: TimeInterval = 15) async throws -> InterruptionContext? { + let account: Account + if let freshAccount { + account = freshAccount + } else { + account = try await AccountAPI.me() + await MainActor.run { + LoginManager.shared.setAccount(account) + } + } guard let pinToken = AppGroupKeychain.pinToken else { throw Error.missingPINToken } diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIPConfig.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIPConfig.swift index 101dfcba1e..ecf8e0822c 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIPConfig.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIPConfig.swift @@ -2,17 +2,54 @@ import Foundation public struct TIPConfig: Decodable { - public static let current: TIPConfig = { - let json = raw.data(using: .utf8)! - let config = try! JSONDecoder.default.decode(TIPConfig.self, from: json) - return config - }() + public static let current = TIPConfig( + commitments: [ + "5HYSNEcudZqucSo8tjVkkkuz6QiTQPSCjSxdGY9gZ1V3JKGtJ5bfZXS3QbB8AnBPLVrJQLEyJaucw4S6MmJhkwTpqCpTiovjiiaab4PUZsS8NBUBFharae9R3QUMR9ouPTgFxqmqMGGXSAqrRziSqneTcEkgLymx5oahxGfSeovgTN1FDgKiAt", + "5Jr6mXiEF1xtR7jJ8o2923vsGcqnjnegK9eVybDfXCnFXKhRVaJDCMHvGuHLo92TWSuBVrwDxYM8nHDiqYVb7csPQHHTyPjrGMPZiPe3PFhBYpz5nsDeVxjknZ8C9fPYYi67qyBy4fy1T6U4itXQEzjCTBdtjw2TXrNKe5oYPvJWx3X6ZpygCi", + "5JTnSpeBG9NyBAEkXL6KxFW3fYMhM4rgBqLMXynrBxHcshuQViedG2H4UumcdLZzMjkyyAdsGmEAf4MKiULmN93aaNvqCXiHH7MYnyvGp96s9bXs2M61HgH1KKZ4Tvk6yBfnVmvwmGVnnsQMd7fTky37VZGqn56SkQa7ANEz52Bwfs7uBBVZzV", + "5JDXsF4qPcf9cwKuAL2H4scnaMEz3GYnyM3koVx5AE7rDvaLCeqWZ8ksXES9eQooKtfQHZyxhhFZumrQqkMqsbxatYSvZJFSWtTESoBqGSqb9F2pvQboik1uJyw7VNrDFUkvVj64JXY6cThHvWQpK96zqELurNjfEPjNRB79c5ESqqfK4FXtce", + "5Jr8U6gPzoi9iYLRY4bVJBbYfsk95xM7AUyMhrsXzfG16nzkzcR2LpEsPFeYwS75WCDMGVta55SDWjb1cRPmWVKrKvr9A8RKRJkF3yop9gc2Kia4bccYqH1QcnNZAGoDLcqHiLSgbWwvjb1NaS2vVtUHsfnbKAQ64jX96gSMJis39UEh9HJiiA" + ], + signers: [ + TIPSigner( + identity: "5HXAHFh8khYBGWA2V3oUUvXAX4aWnQsEyNzKoA3LnJkxtKQNhcWSh4Swt72a1bw7aG8uTg9F31ybzSJyuNHENUBtGobUfHbKNPUYYkHnhuPtWszaCuNJ3nBxZ4Crt8Q8AmJ2fZznLx3EDM2Eqf63drNmW6VVmmzBQUc4N2JaXzFtt4HFFWtvUk", + index: 0, + api: URL(string: "https://api.mixin.one/external/tip/mercury")! + ), + TIPSigner( + identity: "5HrtnCWMLkh2R82iwztEorvRhdZkZwhE77ohPekJKumbko2gZ4RJ4HDiP9VRp1ZvjJi1CR7UB5WCxPGwcQS5oapXb2gtC7X37YhJ1TonFMfpiMy1a8w4VbrHsva3HyLeukUNvQ9vwn8eShRkqGXDhs83GGPcMjvpMWE7BrQuowCuzh5Rh1A3Zm", + index: 1, + api: URL(string: "https://api.mixin.one/external/tip/saturn")! + ), + TIPSigner( + identity: "5JPtzVXJB5qPf4hZ8PgMFDyrqpjhUkYFSeLyME8mc61z7RErZxKj9To7CBhCpFPtt6LyfPH8D6knWjA8LgcAUjjS1EjbPzGNJ8GWFMP5ZtTh1HLgtLEnT5eJXvPHataxruYTmXMmuxZZiMKWvXf9crHggZBLPTaAxfgiis3JdwUUXYXhN91vi3", + index: 2, + api: URL(string: "https://api.mixin.one/external/tip/jupiter")! + ), + TIPSigner( + identity: "5JZegBrWEedonzKwhWYhKjVuDd2ADWnFnnGWp7yjfAJCQSwwSFU7K22hwFeGtyHy9r8Zn7M5R6rX7WMYDQQupKP2NbqxsfSggxj3YrjC5msA4TdWavdLjFFPjNMdcCQHhUQhexkvxenDwjBgBFtsnHMMbSUTPk4nDQEjiF4J1ihZ6bwY3trTVD", + index: 3, + api: URL(string: "https://api.mixin.one/external/tip/mars")! + ), + TIPSigner( + identity: "5JaU587MEXez1onCX7RkRyNkswfpdQ979Nsyv6niBFgUKJQmRTmoCy8phdUhMtwfzoJ8fu3zt8Mae1VVoTM5iNL4gP9zWkT8JscsNJSG5vnT6soM511V8exwmgUeXDfugNshJjqaskhyAAih4FfLxtV3NcBZk1zMDxnctwGQaM7z1G2L9Tf9H1", + index: 4, + api: URL(string: "https://api.mixin.one/external/tip/moon")! + ), + TIPSigner( + identity: "5JaUby8CxXdEhYcEH2VEbkhdj6zDd1fj3wGdyTjiRQtres4yVoLhkHMf8wZ4qsNMhLvJgk9Mgae1Rs9cm6rY41yCTEzQQRA3a3D8FDDdv4dQms735noeqgxxzZs15utTnu7S5XFDiTUcMhue1Re7DZvvATKFpCbHzwoymU9yhXZBxsYCaHbULa", + index: 5, + api: URL(string: "https://api.mixin.one/external/tip/venus")! + ), + TIPSigner( + identity: "5JrHfpJ6ML88u3nbvsB4dej7aHXdvShTEfxNF3mZ8no8wxsdhLTP4sh2Kt6AKboCcBRWGkFPky85e6DkMZsT3WSZ1q8V7gNF4Bdyipw7aS7TP8vtgG7USRJhVe36gNa8LJktf2a2chsWRT6egeB59wEyCHmgiNZpZSPwaezwS32ADTgCSDkzKY", + index: 6, + api: URL(string: "https://api.mixin.one/external/tip/sun")! + ) + ] + ) public let commitments: [String] public let signers: [TIPSigner] } - -fileprivate let raw = """ -{"commitments":["5HYSNEcudZqucSo8tjVkkkuz6QiTQPSCjSxdGY9gZ1V3JKGtJ5bfZXS3QbB8AnBPLVrJQLEyJaucw4S6MmJhkwTpqCpTiovjiiaab4PUZsS8NBUBFharae9R3QUMR9ouPTgFxqmqMGGXSAqrRziSqneTcEkgLymx5oahxGfSeovgTN1FDgKiAt","5Jr6mXiEF1xtR7jJ8o2923vsGcqnjnegK9eVybDfXCnFXKhRVaJDCMHvGuHLo92TWSuBVrwDxYM8nHDiqYVb7csPQHHTyPjrGMPZiPe3PFhBYpz5nsDeVxjknZ8C9fPYYi67qyBy4fy1T6U4itXQEzjCTBdtjw2TXrNKe5oYPvJWx3X6ZpygCi","5JTnSpeBG9NyBAEkXL6KxFW3fYMhM4rgBqLMXynrBxHcshuQViedG2H4UumcdLZzMjkyyAdsGmEAf4MKiULmN93aaNvqCXiHH7MYnyvGp96s9bXs2M61HgH1KKZ4Tvk6yBfnVmvwmGVnnsQMd7fTky37VZGqn56SkQa7ANEz52Bwfs7uBBVZzV","5JDXsF4qPcf9cwKuAL2H4scnaMEz3GYnyM3koVx5AE7rDvaLCeqWZ8ksXES9eQooKtfQHZyxhhFZumrQqkMqsbxatYSvZJFSWtTESoBqGSqb9F2pvQboik1uJyw7VNrDFUkvVj64JXY6cThHvWQpK96zqELurNjfEPjNRB79c5ESqqfK4FXtce","5Jr8U6gPzoi9iYLRY4bVJBbYfsk95xM7AUyMhrsXzfG16nzkzcR2LpEsPFeYwS75WCDMGVta55SDWjb1cRPmWVKrKvr9A8RKRJkF3yop9gc2Kia4bccYqH1QcnNZAGoDLcqHiLSgbWwvjb1NaS2vVtUHsfnbKAQ64jX96gSMJis39UEh9HJiiA"],"signers":[{"identity":"5HXAHFh8khYBGWA2V3oUUvXAX4aWnQsEyNzKoA3LnJkxtKQNhcWSh4Swt72a1bw7aG8uTg9F31ybzSJyuNHENUBtGobUfHbKNPUYYkHnhuPtWszaCuNJ3nBxZ4Crt8Q8AmJ2fZznLx3EDM2Eqf63drNmW6VVmmzBQUc4N2JaXzFtt4HFFWtvUk","index":0,"api":"https://api.mixin.one/external/tip/mercury"},{"identity":"5HrtnCWMLkh2R82iwztEorvRhdZkZwhE77ohPekJKumbko2gZ4RJ4HDiP9VRp1ZvjJi1CR7UB5WCxPGwcQS5oapXb2gtC7X37YhJ1TonFMfpiMy1a8w4VbrHsva3HyLeukUNvQ9vwn8eShRkqGXDhs83GGPcMjvpMWE7BrQuowCuzh5Rh1A3Zm","index":1,"api":"https://api.mixin.one/external/tip/saturn"},{"identity":"5JPtzVXJB5qPf4hZ8PgMFDyrqpjhUkYFSeLyME8mc61z7RErZxKj9To7CBhCpFPtt6LyfPH8D6knWjA8LgcAUjjS1EjbPzGNJ8GWFMP5ZtTh1HLgtLEnT5eJXvPHataxruYTmXMmuxZZiMKWvXf9crHggZBLPTaAxfgiis3JdwUUXYXhN91vi3","index":2,"api":"https://api.mixin.one/external/tip/jupiter"},{"identity":"5JZegBrWEedonzKwhWYhKjVuDd2ADWnFnnGWp7yjfAJCQSwwSFU7K22hwFeGtyHy9r8Zn7M5R6rX7WMYDQQupKP2NbqxsfSggxj3YrjC5msA4TdWavdLjFFPjNMdcCQHhUQhexkvxenDwjBgBFtsnHMMbSUTPk4nDQEjiF4J1ihZ6bwY3trTVD","index":3,"api":"https://api.mixin.one/external/tip/mars"},{"identity":"5JaU587MEXez1onCX7RkRyNkswfpdQ979Nsyv6niBFgUKJQmRTmoCy8phdUhMtwfzoJ8fu3zt8Mae1VVoTM5iNL4gP9zWkT8JscsNJSG5vnT6soM511V8exwmgUeXDfugNshJjqaskhyAAih4FfLxtV3NcBZk1zMDxnctwGQaM7z1G2L9Tf9H1","index":4,"api":"https://api.mixin.one/external/tip/moon"},{"identity":"5JaUby8CxXdEhYcEH2VEbkhdj6zDd1fj3wGdyTjiRQtres4yVoLhkHMf8wZ4qsNMhLvJgk9Mgae1Rs9cm6rY41yCTEzQQRA3a3D8FDDdv4dQms735noeqgxxzZs15utTnu7S5XFDiTUcMhue1Re7DZvvATKFpCbHzwoymU9yhXZBxsYCaHbULa","index":5,"api":"https://api.mixin.one/external/tip/venus"},{"identity":"5JrHfpJ6ML88u3nbvsB4dej7aHXdvShTEfxNF3mZ8no8wxsdhLTP4sh2Kt6AKboCcBRWGkFPky85e6DkMZsT3WSZ1q8V7gNF4Bdyipw7aS7TP8vtgG7USRJhVe36gNa8LJktf2a2chsWRT6egeB59wEyCHmgiNZpZSPwaezwS32ADTgCSDkzKY","index":6,"api":"https://api.mixin.one/external/tip/sun"}]} -""" diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift index 83234fb7b9..7dbef4803a 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIPDiagnostic.swift @@ -11,21 +11,21 @@ public enum TIPDiagnostic { } @MainActor - public static var failPINUpdateOnce = false { + public static var failPINUpdateServerSideOnce = false { didSet { updateDashboard() } } @MainActor - public static var failCounterWatchOnce = false { + public static var failPINUpdateClientSideOnce = false { didSet { updateDashboard() } } @MainActor - public static var uiTestOnly = false { + public static var failCounterWatchOnce = false { didSet { updateDashboard() } @@ -38,6 +38,20 @@ public enum TIPDiagnostic { } } + @MainActor + public static var invalidNonceOnce = false { + didSet { + updateDashboard() + } + } + + @MainActor + public static var uiTestOnly = false { + didSet { + updateDashboard() + } + } + private static let dashboardLabel: UITextView = { let textView = UITextView() textView.alpha = 0.45 @@ -69,9 +83,11 @@ public enum TIPDiagnostic { private static func updateDashboard() { Self.dashboardLabel.text = """ Fail Last Sign: \(failLastSignerOnce ? "ONCE" : " OFF") - Fail PIN Update: \(failPINUpdateOnce ? "ONCE" : " OFF") + Fail PIN Update Server: \(failPINUpdateServerSideOnce ? "ONCE" : " OFF") + Fail PIN Update Client: \(failPINUpdateClientSideOnce ? "ONCE" : " OFF") Fail Watch: \(failCounterWatchOnce ? "ONCE" : " OFF") Crash After PIN Update: \(crashAfterUpdatePIN ? " ON" : " OFF") + Invalid Nonce: \(invalidNonceOnce ? "ONCE" : " OFF") UI Test: \(uiTestOnly ? " ON" : " OFF") """ } diff --git a/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift b/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift index 95c7fb2271..71f13f82be 100644 --- a/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift +++ b/MixinServices/MixinServices/Services/Crypto/TIP/TIPNode.swift @@ -3,7 +3,7 @@ import Tip import Alamofire fileprivate let ephemeralGrace = 128 * UInt64(secondsPerDay) * UInt64(NSEC_PER_SEC) -fileprivate let maximumRetries = 2 +fileprivate let maximumRetries: UInt64 = 2 public enum TIPNode { @@ -39,11 +39,11 @@ public enum TIPNode { private actor Accumulator { - private let maxValue: Int + private let maxValue: UInt64 - private var value: Int = 0 + private(set) var value: UInt64 = 0 - init(maxValue: Int) { + init(maxValue: UInt64) { self.maxValue = maxValue } @@ -206,7 +206,18 @@ public enum TIPNode { assignee: Data?, progressHandler: (@MainActor (TIP.Progress) -> Void)? ) async throws -> [TIPSignResponseData] { +#if DEBUG + let nonce: UInt64 = await MainActor.run { + if TIPDiagnostic.invalidNonceOnce { + TIPDiagnostic.invalidNonceOnce = false + return 20 + } else { + return UInt64(Date().timeIntervalSince1970) + } + } +#else let nonce = UInt64(Date().timeIntervalSince1970) +#endif let grace = ephemeralGrace return await withTaskGroup(of: Result.self) { group in let retries = Accumulator(maxValue: maximumRetries) @@ -231,7 +242,7 @@ public enum TIPNode { signer: signer, ephemeral: ephemeral, watcher: watcher, - nonce: nonce, + nonce: nonce + retries.value, grace: grace, assignee: assignee) Logger.tip.info(category: "TIPNode", message: "Node \(signer.index) sign succeed") diff --git a/MixinServices/MixinServicesTests/Services/ServicesTests.swift b/MixinServices/MixinServicesTests/Services/ServicesTests.swift index a3e1d19648..def479a986 100644 --- a/MixinServices/MixinServicesTests/Services/ServicesTests.swift +++ b/MixinServices/MixinServicesTests/Services/ServicesTests.swift @@ -48,4 +48,15 @@ class ServicesTests: XCTestCase { assignee: nil) } + func testAmountFormatter() { + XCTAssertEqual(AmountFormatter.formattedAmount("100.000"), "100") + XCTAssertEqual(AmountFormatter.formattedAmount("100.00100"), "100.001") + XCTAssertEqual(AmountFormatter.formattedAmount("1.1E-4"), "0.00011") + XCTAssertEqual(AmountFormatter.formattedAmount("-1.100E-5"), "-0.000011") + XCTAssertEqual(AmountFormatter.formattedAmount("01.010"), "1.01") + XCTAssertEqual(AmountFormatter.formattedAmount("0"), "0") + XCTAssertEqual(AmountFormatter.formattedAmount("0.00000001"), "0.00000001") + XCTAssertEqual(AmountFormatter.formattedAmount("0.00000009"), "0.00000009") + } + }