Skip to content

Commit

Permalink
Merge pull request #667 from Syn-McJ/feat/coinjoin-network-switch
Browse files Browse the repository at this point in the history
feat(coinjoin): network switching
  • Loading branch information
Syn-McJ authored Oct 3, 2024
2 parents 3eee4e8 + 4d6fe66 commit 75d0c86
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 83 deletions.
14 changes: 10 additions & 4 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,8 @@
753130922B47EE920069C9B7 /* UpholdCapability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753130902B47EE920069C9B7 /* UpholdCapability.swift */; };
753130972B4944130069C9B7 /* UpholdError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753130962B4944130069C9B7 /* UpholdError.swift */; };
753130982B4944130069C9B7 /* UpholdError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753130962B4944130069C9B7 /* UpholdError.swift */; };
753FD7E22CA44BDD00B7751F /* CoinJoinProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753FD7E12CA44BDD00B7751F /* CoinJoinProgress.swift */; };
753FD7E32CA44BDD00B7751F /* CoinJoinProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753FD7E12CA44BDD00B7751F /* CoinJoinProgress.swift */; };
753FDBEA2AEA422F0005EEC3 /* VotingPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753FDBE92AEA422F0005EEC3 /* VotingPrefs.swift */; };
753FDBEC2AECF4CC0005EEC3 /* VotingHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 753FDBEB2AECF4CC0005EEC3 /* VotingHeaderView.xib */; };
753FDBEE2AECF52B0005EEC3 /* UsernameVoting.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 753FDBED2AECF52B0005EEC3 /* UsernameVoting.storyboard */; };
Expand Down Expand Up @@ -2435,6 +2437,7 @@
7531308C2B47EC910069C9B7 /* UpholdClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpholdClient.swift; sourceTree = "<group>"; };
753130902B47EE920069C9B7 /* UpholdCapability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpholdCapability.swift; sourceTree = "<group>"; };
753130962B4944130069C9B7 /* UpholdError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpholdError.swift; sourceTree = "<group>"; };
753FD7E12CA44BDD00B7751F /* CoinJoinProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinJoinProgress.swift; sourceTree = "<group>"; };
753FDBE92AEA422F0005EEC3 /* VotingPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingPrefs.swift; sourceTree = "<group>"; };
753FDBEB2AECF4CC0005EEC3 /* VotingHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VotingHeaderView.xib; sourceTree = "<group>"; };
753FDBED2AECF52B0005EEC3 /* UsernameVoting.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = UsernameVoting.storyboard; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6013,6 +6016,7 @@
isa = PBXGroup;
children = (
7503643D2C89D49A0029EC0D /* CoinJoinService.swift */,
753FD7E12CA44BDD00B7751F /* CoinJoinProgress.swift */,
);
path = CoinJoin;
sourceTree = "<group>";
Expand Down Expand Up @@ -8811,6 +8815,7 @@
754BEA122C0B6BD700E8C93C /* HomeViewModel.swift in Sources */,
2A4430E722CBB6EC009BAF7F /* AppDelegate.m in Sources */,
C917023F29D44E0B008C034D /* SendReceivePageController.swift in Sources */,
753FD7E22CA44BDD00B7751F /* CoinJoinProgress.swift in Sources */,
2A7A7BD92348CB7300451078 /* DWSettingsMenuModel.m in Sources */,
478A2C7228DC909C00AD1420 /* BaseNavigationController.swift in Sources */,
47CDEECC294A2BAD008AE06D /* UIViewController+Coinbase.swift in Sources */,
Expand Down Expand Up @@ -9418,6 +9423,7 @@
C943B4F52A40A54600AF23C5 /* DWDPIncomingRequestObject.m in Sources */,
C943B4BA2A40A54600AF23C5 /* DWRequestsModel.m in Sources */,
C9D2C7982A320AA000D15901 /* BaseResponse.swift in Sources */,
753FD7E32CA44BDD00B7751F /* CoinJoinProgress.swift in Sources */,
C9D2C7992A320AA000D15901 /* DWDemoAdvancedSecurityViewController.m in Sources */,
C943B3212A408CED00AF23C5 /* DWImgurItemView.m in Sources */,
C9D2C79C2A320AA000D15901 /* TxWithinTimePeriod.swift in Sources */,
Expand Down Expand Up @@ -10747,7 +10753,7 @@
CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7;
CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59;
CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements;
CURRENT_PROJECT_VERSION = 165;
CURRENT_PROJECT_VERSION = 170;
DEVELOPMENT_TEAM = 44RJ69WHFF;
EXCLUDED_ARCHS = "";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
Expand Down Expand Up @@ -10883,7 +10889,7 @@
CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7;
CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59;
CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements;
CURRENT_PROJECT_VERSION = 165;
CURRENT_PROJECT_VERSION = 170;
DEVELOPMENT_TEAM = 44RJ69WHFF;
EXCLUDED_ARCHS = "";
GCC_PRECOMPILE_PREFIX_HEADER = YES;
Expand Down Expand Up @@ -11018,7 +11024,7 @@
CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7;
CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59;
CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements;
CURRENT_PROJECT_VERSION = 165;
CURRENT_PROJECT_VERSION = 170;
DEVELOPMENT_TEAM = 44RJ69WHFF;
EXCLUDED_ARCHS = "";
GCC_PRECOMPILE_PREFIX_HEADER = YES;
Expand Down Expand Up @@ -11163,7 +11169,7 @@
CLIENT_ID = 0c38beb67db0c68191326be347d7ec0abd7d77adb02a79db1abeba343f16a0f7;
CLIENT_SECRET = cc980185754f905e24250f877792817c03540b3d0e0959721df291c816797e59;
CODE_SIGN_ENTITLEMENTS = dashwallet/dashwallet.entitlements;
CURRENT_PROJECT_VERSION = 165;
CURRENT_PROJECT_VERSION = 170;
DEVELOPMENT_TEAM = 44RJ69WHFF;
EXCLUDED_ARCHS = "";
GCC_PRECOMPILE_PREFIX_HEADER = YES;
Expand Down
22 changes: 22 additions & 0 deletions DashWallet/Sources/Models/CoinJoin/CoinJoinProgress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Created by Andrei Ashikhmin
// Copyright © 2024 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

struct CoinJoinProgress: Equatable {
let progress: Double
let totalBalance: UInt64
let coinJoinBalance: UInt64
}
136 changes: 102 additions & 34 deletions DashWallet/Sources/Models/CoinJoin/CoinJoinService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,27 +60,35 @@ private let kDefaultRounds: Int32 = 4
private let kDefaultSessions: Int32 = 6
private let kDefaultDenominationGoal: Int32 = 50
private let kDefaultDenominationHardcap: Int32 = 300
private let kCoinJoinMode = "coinJoinModeKey"
private let kCoinJoinMainnetMode = "coinJoinModeMainnetKey"
private let kCoinJoinTestnetMode = "coinJoinModeTestnetKey"

class CoinJoinService: NSObject {
static let shared: CoinJoinService = {
return CoinJoinService()
}()

private var permanentBag = Set<AnyCancellable>()
private var cancellableBag = Set<AnyCancellable>()
private let updateMutex = NSLock()
private let updateMixingStateMutex = NSLock()
private var coinJoinManager: DSCoinJoinManager? = nil
private var hasAnonymizableBalance: Bool = false
private var networkStatus: NetworkStatus = .online
private var workingChain: ChainType

private var chainModeKey: String {
get {
DWEnvironment.sharedInstance().currentChain.isMainnet() ? kCoinJoinMainnetMode : kCoinJoinTestnetMode
}
}

private var _savedMode: Int? = nil
private var savedMode: Int {
get { _savedMode ?? UserDefaults.standard.integer(forKey: kCoinJoinMode) }
set(value) {
_savedMode = value
UserDefaults.standard.set(value, forKey: kCoinJoinMode)
get {
let key = chainModeKey
return UserDefaults.standard.integer(forKey: key)
}
set(value) { UserDefaults.standard.set(value, forKey: chainModeKey) }
}

@Published private(set) var mode: CoinJoinMode = .none {
Expand All @@ -90,32 +98,34 @@ class CoinJoinService: NSObject {
}

@Published var mixingState: MixingStatus = .notStarted
@Published private(set) var progress: Double = 0.0
@Published private(set) var totalBalance: UInt64 = 0
@Published private(set) var coinJoinBalance: UInt64 = 0
@Published private(set) var progress = CoinJoinProgress(progress: 0.0, totalBalance: 0, coinJoinBalance: 0)
@Published private(set) var activeSessions: Int = 0

override init() {
workingChain = DWEnvironment.sharedInstance().currentChain.chainType
super.init()

NotificationCenter.default.publisher(for: NSNotification.Name.DSWalletBalanceDidChange)
.sink { [weak self] _ in self?.updateBalance(balance: DWEnvironment.sharedInstance().currentAccount.balance) }
.store(in: &cancellableBag)

let mode = CoinJoinMode(rawValue: savedMode) ?? .none
NotificationCenter.default.publisher(for: NSNotification.Name.DWCurrentNetworkDidChange)
.sink { [weak self] _ in
DSLogger.log("[SW] CoinJoin: change of network to \(DWEnvironment.sharedInstance().currentChain.chainType.tag), resetting")
self?.workingChain = DWEnvironment.sharedInstance().currentChain.chainType
self?.restoreMode()
}
.store(in: &permanentBag)

if mode != .none {
updateMode(mode: mode)
}
restoreMode()
}

func updateMode(mode: CoinJoinMode) {
self.coinJoinManager?.updateOptions(withEnabled: mode != .none)
let account = DWEnvironment.sharedInstance().currentAccount
let balance = account.balance

if (mode != .none && self.mode == .none) {
if mode != .none && self.mode == .none {
configureMixing(amount: balance)
configureObservers()
} else if mode == .none {
removeObservers()
}

updateBalance(balance: balance)
Expand All @@ -126,6 +136,7 @@ class CoinJoinService: NSObject {
private func prepareMixing() {
guard let coinJoinManager = self.coinJoinManager ?? createCoinJoinManager() else { return }

coinJoinManager.managerDelegate = self
coinJoinManager.setStopOnNothingToDo(true)
coinJoinManager.start()
}
Expand All @@ -138,9 +149,7 @@ class CoinJoinService: NSObject {
} else {
coinJoinManager.refreshUnusedKeys()
coinJoinManager.initMasternodeGroup()
coinJoinManager.doAutomaticDenominating()

DSLogger.log("[SW] CoinJoin: Mixing \(coinJoinManager.startMixing() ? "started successfully" : "start failed, will retry")") // TODO: failed statuses: \(coinJoinManager.statuses)
coinJoinManager.doAutomaticDenominating(withReport: true)
}
}

Expand Down Expand Up @@ -170,15 +179,13 @@ class CoinJoinService: NSObject {
let anonymizedBalance = coinJoinBalance.anonymized

DispatchQueue.main.async {
self.progress = progress
self.totalBalance = totalBalance
self.coinJoinBalance = anonymizedBalance
self.progress = CoinJoinProgress(progress: progress, totalBalance: totalBalance, coinJoinBalance: anonymizedBalance)
}
}
}

private func createCoinJoinManager() -> DSCoinJoinManager? {
self.coinJoinManager = DSCoinJoinManager.sharedInstance(for: DWEnvironment().currentChain)
self.coinJoinManager = DSCoinJoinManager.sharedInstance(for: DWEnvironment.sharedInstance().currentChain)
coinJoinManager?.managerDelegate = self
return self.coinJoinManager
}
Expand Down Expand Up @@ -234,8 +241,12 @@ class CoinJoinService: NSObject {
networkStatus: NetworkStatus,
chain: DSChain
) {
if !recheckCurrentChain() {
return
}

synchronized(self.updateMutex) {
DSLogger.log("[SW] CoinJoin: \(mode), \(timeSkew) ms, \(hasAnonymizableBalance), \(networkStatus), synced: \(chain.chainManager!.isSynced)")
DSLogger.log("[SW] CoinJoin: \(mode), \(timeSkew) ms, \(hasAnonymizableBalance), \(networkStatus), synced: \(SyncingActivityMonitor.shared.state == .syncDone)")

self.networkStatus = networkStatus
self.hasAnonymizableBalance = hasAnonymizableBalance
Expand All @@ -248,7 +259,7 @@ class CoinJoinService: NSObject {
configureMixing(amount: balance)

if hasAnonymizableBalance {
if networkStatus == .online && chain.chainManager!.isSynced {
if networkStatus == .online && SyncingActivityMonitor.shared.state == .syncDone {
updateMixingState(state: .mixing)
} else {
updateMixingState(state: .paused)
Expand Down Expand Up @@ -283,6 +294,36 @@ class CoinJoinService: NSObject {
}
}
}

private func restoreMode() {
self.stopMixing()
self.coinJoinManager = nil
self.hasAnonymizableBalance = false
self.mode = .none
let mode = CoinJoinMode(rawValue: savedMode) ?? .none
updateMode(mode: mode)
}

private func configureObservers() {
NotificationCenter.default.publisher(for: NSNotification.Name.DSWalletBalanceDidChange)
.sink { [weak self] _ in
self?.updateBalance(balance: DWEnvironment.sharedInstance().currentAccount.balance)
}
.store(in: &cancellableBag)

SyncingActivityMonitor.shared.add(observer: self)
}

private func removeObservers() {
cancellableBag.removeAll()
SyncingActivityMonitor.shared.remove(observer: self)
}

private func synchronized(_ lock: NSLock, closure: () -> Void) {
lock.lock()
defer { lock.unlock() }
closure()
}
}

extension CoinJoinService: DSCoinJoinManagerDelegate {
Expand All @@ -296,11 +337,17 @@ extension CoinJoinService: DSCoinJoinManagerDelegate {

func mixingStarted() { }

func mixingComplete(_ withError: Bool) {
func mixingComplete(_ withError: Bool, isInterrupted: Bool) {
if isInterrupted {
DSLogger.log("[SW] CoinJoin: Mixing Interrupted. \(progress)")
updateMixingState(state: .notStarted)
return
}

if withError {
DSLogger.log("[SW] CoinJoin: Mixing Error. \(progress)% mixed")
DSLogger.log("[SW] CoinJoin: Mixing Error. \(progress)")
} else {
DSLogger.log("[SW] CoinJoin: Mixing Complete. \(progress)% mixed")
DSLogger.log("[SW] CoinJoin: Mixing Complete. \(progress)")
}

self.updateMixingState(state: withError ? .error : .finished) // TODO: paused?
Expand All @@ -319,10 +366,31 @@ extension CoinJoinService: DSCoinJoinManagerDelegate {
DSLogger.log("[SW] CoinJoin: Active sessions: \(activeSessions)")
}

private func synchronized(_ lock: NSLock, closure: () -> Void) {
lock.lock()
defer { lock.unlock() }
closure()
private func recheckCurrentChain() -> Bool {
let chainType = DWEnvironment.sharedInstance().currentChain.chainType

if self.workingChain.tag != chainType.tag {
DSLogger.log("[SW] CoinJoin: reset chain after recheck to type \(chainType.tag)")
self.workingChain = chainType
restoreMode()
return false
}

return true
}
}

extension CoinJoinService: SyncingActivityMonitorObserver {
func syncingActivityMonitorProgressDidChange(_ progress: Double) { }

func syncingActivityMonitorStateDidChange(previousState: SyncingActivityMonitor.State, state: SyncingActivityMonitor.State) {
self.updateState(
balance: DWEnvironment.sharedInstance().currentAccount.balance,
mode: self.mode,
timeSkew: TimeInterval(0), // TODO
hasAnonymizableBalance: self.hasAnonymizableBalance,
networkStatus: self.networkStatus,
chain: DWEnvironment.sharedInstance().currentChain
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,6 @@ extension Transaction {
tx.txHashData
}

var currentBlockHeight: UInt64 {
let chain = DWEnvironment.sharedInstance().currentChain
let lastHeight = chain.lastTerminalBlockHeight
return UInt64(lastHeight)
}

var isCoinbaseTransaction: Bool {
tx is DSCoinbaseTransaction
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ extension CoinJoinLevelsViewController {
return
}

if viewModel.selectedMode == .none {
if viewModel.selectedMode == .none || viewModel.mixingState == .notStarted {
viewModel.selectedMode = mode
} else {
confirmFor(mode)
Expand Down
13 changes: 0 additions & 13 deletions DashWallet/Sources/UI/DashPay/CoinJoin/CoinJoinViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ class CoinJoinViewModel: ObservableObject {

@Published var selectedMode: CoinJoinMode = .none
@Published private(set) var mixingState: MixingStatus = .notStarted
@Published private(set) var progress: Double = 0.0
@Published private(set) var totalBalance: UInt64 = 0
@Published private(set) var coinJoinBalance: UInt64 = 0

private var _infoShown: Bool? = nil
var infoShown: Bool {
Expand All @@ -62,16 +59,6 @@ class CoinJoinViewModel: ObservableObject {
self?.mixingState = state
}
.store(in: &cancellableBag)

coinJoinService.$progress
.receive(on: DispatchQueue.main)
.sink { [weak self] progress in
guard let self = self else { return }
self.progress = progress
self.totalBalance = coinJoinService.totalBalance
self.coinJoinBalance = coinJoinService.coinJoinBalance
}
.store(in: &cancellableBag)
}

func startMixing() {
Expand Down
Loading

0 comments on commit 75d0c86

Please sign in to comment.