diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/XXMessengerClient.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/XXMessengerClient.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..665c4644aed93c75db826b6b8969769c2c203fe3
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/XXMessengerClient.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1340"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "XXMessengerClient"
+               BuildableName = "XXMessengerClient"
+               BlueprintName = "XXMessengerClient"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "XXMessengerClientTests"
+               BuildableName = "XXMessengerClientTests"
+               BlueprintName = "XXMessengerClientTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
+      </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">
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "XXMessengerClient"
+            BuildableName = "XXMessengerClient"
+            BlueprintName = "XXMessengerClient"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift.xcscheme
index c0fcfabe31600c05dbbb6e33fcd972945a0d23d8..223297f5afd6ce419d4485fb8500ed96aeacbe18 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift.xcscheme
@@ -20,6 +20,20 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "XXMessengerClient"
+               BuildableName = "XXMessengerClient"
+               BlueprintName = "XXMessengerClient"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </BuildActionEntry>
       </BuildActionEntries>
    </BuildAction>
    <TestAction
@@ -39,6 +53,16 @@
                ReferencedContainer = "container:">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "XXMessengerClientTests"
+               BuildableName = "XXMessengerClientTests"
+               BlueprintName = "XXMessengerClientTests"
+               ReferencedContainer = "container:">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
    </TestAction>
    <LaunchAction
diff --git a/Docs/XXClient-quick-start-guide.md b/Docs/XXClient-quick-start-guide.md
new file mode 100644
index 0000000000000000000000000000000000000000..503bc2d032666b8dab1734bf074d538657e195d2
--- /dev/null
+++ b/Docs/XXClient-quick-start-guide.md
@@ -0,0 +1,116 @@
+# XXClient Quick Start Guide
+
+Add `XXClient` library as a dependency to your project using Swift Package Manager.
+
+## ▶️ Instantiating cMix
+
+You can use a convenient `CMixManager` wrapper to manage cMix stored on disk:
+
+```swift
+let cMixManager: CMixManager = .live(
+  passwordStorage: .init(
+    save: { password in
+      // securely save provided password
+    },
+    load: {
+      // load securely stored password
+    }
+  )
+)
+
+let cMix: CMix
+if cMixManager.hasStorage() {
+  cMix = try cMixManager.load()
+} else {
+  cMix = try cMixManager.create()
+}
+```
+
+Check out included example iOS application for the `PasswordStorage` implementation that uses the iOS keychain.
+
+## ▶️ Connecting to the network
+
+Start network follower:
+
+```swift
+try cMix.startNetworkFollower(timeoutMS: 10_000)
+```
+
+Wait until connected:
+
+```swift
+let isNetworkHealthy = try cMix.waitForNetwork(timeoutMS: 30_000)
+```
+
+## ▶️ Making a new reception identity
+
+Use the cMix to make a new reception identity:
+
+```swift
+let myIdentity = try cMix.makeReceptionIdentity()
+```
+
+## ▶️ Create new E2E
+
+```swift
+let login: Login = .live
+let e2e = try login(
+  cMixId: cMix.getId(),
+  identity: myIdentity
+)
+```
+
+## ▶️ Connecting to remote
+
+Perform auth key negotiation with the given recipient to get the `Connection`:
+
+```swift
+let connection = try cMix.connect(
+  withAuthentication: false,
+  e2eId: e2e.getId(),
+  recipientContact: ...
+)
+```
+
+Pass `true` for the `withAuthentication` parameter if you want to prove id ownership to remote as well.
+
+## ▶️ Sending messages
+
+Send a message to the connection's partner:
+
+```swift
+let sendReport = try connection.send(
+  messageType: 1,
+  payload: ...
+)
+```
+
+Check if the round succeeded:
+
+```swift
+try cMix.waitForRoundResult(
+  roundList: try sendReport.encode(),
+  timeoutMS: 30_000,
+  callback: .init { result in
+    switch result {
+    case .delivered(let roundResults):
+      ...
+    case .notDelivered(let timedOut):
+      ...
+    }
+  }
+)
+```
+
+## ▶️ Receiving messages
+
+Use connection's message listener to receive messages from partner:
+
+```swift
+try connection.registerListener(
+  messageType: 1,
+  listener: .init { message in
+    ...
+  }
+)
+```
\ No newline at end of file
diff --git a/Docs/XXMessengerClient.md b/Docs/XXMessengerClient.md
new file mode 100644
index 0000000000000000000000000000000000000000..7b29ad5714f6dd32a59f83b3a6673380b55a4978
--- /dev/null
+++ b/Docs/XXMessengerClient.md
@@ -0,0 +1,78 @@
+# XXMessengerClient
+
+`XXMessengerClient` is a client wrapper library for use in xx-messenger application.
+
+## ▶️ Instantiate messenger
+
+Example:
+
+```swift
+// setup environment:
+var environment: MessengerEnvironment = .live()
+
+// change cMix NDF environment if needed:
+environment.ndfEnvironment = ...
+
+// use alternative user-discovery if needed:
+environment.udAddress = ...
+environment.udCert = ...
+environment.udContact = ...
+
+// instantiate messenger:
+let messenger: Messenger = .live(environment)
+```
+
+## 🚀 Start messenger
+
+Example:
+
+```
+func start(messenger: Messenger) throws {
+  // check if messenger is loaded:
+  if messenger.isLoaded() == false {
+    // check if messenger is created and stored on disk:
+    if messenger.isCreated() == false {
+      // create new messenger and store it on disk:
+      try messenger.create()
+    }
+    // load messenger stored on disk:
+    try messenger.load()
+  }
+
+  // start messenger's network follower:
+  try messenger.start()
+
+  // check if messenger is connected:
+  if messenger.isConnected() == false {
+    // start end-to-end connection:
+    try messenger.connect()
+  }
+
+  // check if messenger is logged in with user-discovery:
+  if messenger.isLoggedIn() == false {
+    // check if messenger is registered with user-discovery:
+    if try messenger.isRegistered() == false {
+      // register new user with user-discovery:
+      try messenger.register(username: "new-username")
+    } else {
+      // login previously registered user with user-discovery:
+      try messenger.logIn()
+    }
+  }
+}
+```
+
+## 🛠 Use client components directly
+
+Example:
+
+```swift
+// get cMix:
+let cMix = messenger.cMix()
+
+// get E2E:
+let e2e = messenger.e2e()
+
+// get UserDicovery:
+let ud = messenger.ud()
+```
\ No newline at end of file
diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/ExampleApp (iOS).xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/ExampleApp (iOS).xcscheme
index 57df4eaac7e1b2bcda0ab78bc6f21b6b61a287bf..6ae359a987e276d0151fecdeff0fc8854e31bea1 100644
--- a/Example/Example.xcodeproj/xcshareddata/xcschemes/ExampleApp (iOS).xcscheme	
+++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/ExampleApp (iOS).xcscheme	
@@ -26,8 +26,49 @@
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "YES">
       <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "AppFeatureTests"
+               BuildableName = "AppFeatureTests"
+               BlueprintName = "AppFeatureTests"
+               ReferencedContainer = "container:example-app">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "ErrorFeatureTests"
+               BuildableName = "ErrorFeatureTests"
+               BlueprintName = "ErrorFeatureTests"
+               ReferencedContainer = "container:example-app">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "LandingFeatureTests"
+               BuildableName = "LandingFeatureTests"
+               BlueprintName = "LandingFeatureTests"
+               ReferencedContainer = "container:example-app">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "SessionFeatureTests"
+               BuildableName = "SessionFeatureTests"
+               BlueprintName = "SessionFeatureTests"
+               ReferencedContainer = "container:example-app">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
    </TestAction>
    <LaunchAction
diff --git a/Package.swift b/Package.swift
index dc48fcd529e629c9e08e8ca0e05e8a6e0a831355..0024dab56e3d9aa08441134301edf535ef878da4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -20,6 +20,7 @@ let package = Package(
   ],
   products: [
     .library(name: "XXClient", targets: ["XXClient"]),
+    .library(name: "XXMessengerClient", targets: ["XXMessengerClient"]),
   ],
   dependencies: [
     .package(
@@ -30,6 +31,10 @@ let package = Package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       .upToNextMajor(from: "0.4.0")
     ),
+    .package(
+      url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
+      .upToNextMajor(from: "4.2.2")
+    ),
   ],
   targets: [
     .target(
@@ -48,6 +53,23 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "XXMessengerClient",
+      dependencies: [
+        .target(name: "XXClient"),
+        .product(name: "KeychainAccess", package: "KeychainAccess"),
+        .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "XXMessengerClientTests",
+      dependencies: [
+        .target(name: "XXMessengerClient"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .binaryTarget(
       name: "Bindings",
       path: "Frameworks/Bindings.xcframework"
diff --git a/README.md b/README.md
index 5c0f3d59e1b01968e89e695ea0ce43bf99248313..a311b5801b5a2b9d5cf53877047b28e862348a85 100644
--- a/README.md
+++ b/README.md
@@ -3,132 +3,14 @@
 ![Swift 5.6](https://img.shields.io/badge/swift-5.6-orange.svg)
 ![platform iOS](https://img.shields.io/badge/platform-iOS-blue.svg)
 
-## 📱 Demo
-
-Refer to this [demo](https://git.xx.network/elixxir/shielded-help-demo/elixxir-dapp-demo) to see an example of how to build an app with the SDK to send `E2E` messages and send `RestLike` messsage.
-
-Also you can checkout included example iOS application.
-
 ## 📖 Documentation 
 
-You can find full documentation with step by step guide [here](https://xxdk-dev.xx.network/mobile%20docs/ios-sdk)
-
-## 🚀 Quick Start
-
-Add `XXClient` library as a dependency to your project using Swift Package Manager.
-
-### ▶️ Instantiating cMix
-
-You can use a convenient `CMixManager` wrapper to manage cMix stored on disk:
-
-```swift
-let cMixManager: CMixManager = .live(
-  passwordStorage: .init(
-    save: { password in
-      // securely save provided password
-    },
-    load: {
-      // load securely stored password
-    }
-  )
-)
-
-let cMix: CMix
-if cMixManager.hasStorage() {
-  cMix = try cMixManager.load()
-} else {
-  cMix = try cMixManager.create()
-}
-```
-
-Check out included example iOS application for the `PasswordStorage` implementation that uses the iOS keychain.
-
-### ▶️ Connecting to the network
-
-Start network follower:
-
-```swift
-try cMix.startNetworkFollower(timeoutMS: 10_000)
-```
-
-Wait until connected:
-
-```swift
-let isNetworkHealthy = try cMix.waitForNetwork(timeoutMS: 30_000)
-```
-
-### ▶️ Making a new reception identity
-
-Use the cMix to make a new reception identity:
+- [XXClient Quick Start Guide](Docs/XXClient-quick-start-guide.md)
+- [XXMessengerClient](Docs/XXMessengerClient.md)
 
-```swift
-let myIdentity = try cMix.makeReceptionIdentity()
-```
-
-### ▶️ Create new E2E
-
-```swift
-let login: Login = .live
-let e2e = try login(
-  cMixId: cMix.getId(),
-  identity: myIdentity
-)
-```
-
-### ▶️ Connecting to remote
-
-Perform auth key negotiation with the given recipient to get the `Connection`:
-
-```swift
-let connection = try cMix.connect(
-  withAuthentication: false,
-  e2eId: e2e.getId(),
-  recipientContact: ...
-)
-```
-
-Pass `true` for the `withAuthentication` parameter if you want to prove id ownership to remote as well.
-
-### ▶️ Sending messages
-
-Send a message to the connection's partner:
-
-```swift
-let sendReport = try connection.send(
-  messageType: 1,
-  payload: ...
-)
-```
-
-Check if the round succeeded:
-
-```swift
-try cMix.waitForRoundResult(
-  roundList: try sendReport.encode(),
-  timeoutMS: 30_000,
-  callback: .init { result in
-    switch result {
-    case .delivered(let roundResults):
-      ...
-    case .notDelivered(let timedOut):
-      ...
-    }
-  }
-)
-```
-
-### ▶️ Receiving messages
-
-Use connection's message listener to receive messages from partner:
+## 📱 Demo
 
-```swift
-try connection.registerListener(
-  messageType: 1,
-  listener: .init { message in
-    ...
-  }
-)
-```
+Checkout included example iOS application.
 
 ## 🛠 Development
 
@@ -139,7 +21,8 @@ Open `ElixxirDAppsSDK.xcworkspace` in Xcode (≥13.4).
 ```
 ElixxirDAppsSDK [Xcode Workspace]
  ├─ elixxir-dapps-sdk-swift [Swift Package]
- |   └─ XXClient [Library]
+ |   ├─ XXClient [Library]
+ |   └─ XXMessengerClient [Library]
  └─ Example [Xcode Project]
      ├─ ExampleApp (iOS) [iOS App Target]
      ├─ example-app [Swift Package]
@@ -157,7 +40,7 @@ ElixxirDAppsSDK [Xcode Workspace]
 - Use `example-app` scheme to build and test the example app package with all contained libraries.
 - Use `ExampleAppIcon` scheme with macOS target to build and preview the example app icon.
 - Use `example-app-icon-export` scheme with macOS target to build and update the example app icon.
-- Use other schemes, like `AppFeature`, for building and testing individual libraries in isolation.
+- Use other schemes, like `XXClient`, for building and testing individual libraries in isolation.
 
 ## 📄 License
 
diff --git a/Sources/XXClient/CMixManager/CMixManager.swift b/Sources/XXClient/CMixManager/CMixManager.swift
index 1e1b38e59f92927b6c34fe97ea8f9a9246ff82cc..a0df95ffe66de0998f46141b68c202b2ff3bcd28 100644
--- a/Sources/XXClient/CMixManager/CMixManager.swift
+++ b/Sources/XXClient/CMixManager/CMixManager.swift
@@ -16,7 +16,7 @@ extension CMixManager {
       .appendingPathComponent("xx.network.client")
       .path,
     fileManager: FileManager = .default,
-    environment: Environment = .mainnet,
+    ndfEnvironment: NDFEnvironment = .mainnet,
     downloadNDF: DownloadAndVerifySignedNdf = .live,
     generateSecret: GenerateSecret = .live,
     passwordStorage: PasswordStorage,
@@ -31,7 +31,7 @@ extension CMixManager {
         fileManager: fileManager
       ),
       create: .live(
-        environment: environment,
+        ndfEnvironment: ndfEnvironment,
         downloadNDF: downloadNDF,
         generateSecret: generateSecret,
         passwordStorage: passwordStorage,
@@ -42,7 +42,7 @@ extension CMixManager {
         loadCMix: loadCMix
       ),
       restore: .live(
-        environment: environment,
+        ndfEnvironment: ndfEnvironment,
         downloadNDF: downloadNDF,
         generateSecret: generateSecret,
         passwordStorage: passwordStorage,
diff --git a/Sources/XXClient/CMixManager/Functors/CMixManagerCreate.swift b/Sources/XXClient/CMixManager/Functors/CMixManagerCreate.swift
index a0d543f8fa8c87f621b3a636dacd65b454f15f0b..0dc4946729ae76e3d265ef80aee3ce8b16e8df60 100644
--- a/Sources/XXClient/CMixManager/Functors/CMixManagerCreate.swift
+++ b/Sources/XXClient/CMixManager/Functors/CMixManagerCreate.swift
@@ -11,7 +11,7 @@ public struct CMixManagerCreate {
 
 extension CMixManagerCreate {
   public static func live(
-    environment: Environment,
+    ndfEnvironment: NDFEnvironment,
     downloadNDF: DownloadAndVerifySignedNdf,
     generateSecret: GenerateSecret,
     passwordStorage: PasswordStorage,
@@ -22,7 +22,7 @@ extension CMixManagerCreate {
     loadCMix: LoadCMix
   ) -> CMixManagerCreate {
     CMixManagerCreate {
-      let ndfData = try downloadNDF(environment)
+      let ndfData = try downloadNDF(ndfEnvironment)
       let password = generateSecret()
       try passwordStorage.save(password)
       try? fileManager.removeItem(atPath: directoryPath)
diff --git a/Sources/XXClient/CMixManager/Functors/CMixManagerRestore.swift b/Sources/XXClient/CMixManager/Functors/CMixManagerRestore.swift
index 466684b8c0c901b3613511ccfdd8307eb8e40677..de8b6e1ac156009d7178f2c5a4ed4d4412f39d36 100644
--- a/Sources/XXClient/CMixManager/Functors/CMixManagerRestore.swift
+++ b/Sources/XXClient/CMixManager/Functors/CMixManagerRestore.swift
@@ -14,7 +14,7 @@ public struct CMixManagerRestore {
 
 extension CMixManagerRestore {
   public static func live(
-    environment: Environment,
+    ndfEnvironment: NDFEnvironment,
     downloadNDF: DownloadAndVerifySignedNdf,
     generateSecret: GenerateSecret,
     passwordStorage: PasswordStorage,
@@ -23,7 +23,7 @@ extension CMixManagerRestore {
     newCMixFromBackup: NewCMixFromBackup
   ) -> CMixManagerRestore {
     CMixManagerRestore { backup, passphrase in
-      let ndfData = try downloadNDF(environment)
+      let ndfData = try downloadNDF(ndfEnvironment)
       let password = generateSecret()
       try passwordStorage.save(password)
       try? fileManager.removeItem(atPath: directoryPath)
diff --git a/Sources/XXClient/Functors/DownloadAndVerifySignedNdf.swift b/Sources/XXClient/Functors/DownloadAndVerifySignedNdf.swift
index 2c6ba075d0a6cbea1bf7482372fc46773e8f4e44..c5395780914e3befe6e9f1786a4f81921f863a3a 100644
--- a/Sources/XXClient/Functors/DownloadAndVerifySignedNdf.swift
+++ b/Sources/XXClient/Functors/DownloadAndVerifySignedNdf.swift
@@ -2,9 +2,9 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct DownloadAndVerifySignedNdf {
-  public var run: (Environment) throws -> Data
+  public var run: (NDFEnvironment) throws -> Data
 
-  public func callAsFunction(_ env: Environment) throws -> Data {
+  public func callAsFunction(_ env: NDFEnvironment) throws -> Data {
     try run(env)
   }
 }
diff --git a/Sources/XXClient/Functors/NewOrLoadUd.swift b/Sources/XXClient/Functors/NewOrLoadUd.swift
index 3b1599751d57555cbf2b419e37e1d368834c1884..cdb7f011b17e67a90dcb56d1c02f53f46fc5c354 100644
--- a/Sources/XXClient/Functors/NewOrLoadUd.swift
+++ b/Sources/XXClient/Functors/NewOrLoadUd.swift
@@ -2,10 +2,9 @@ import Bindings
 import XCTestDynamicOverlay
 
 public struct NewOrLoadUd {
-  public struct Params {
+  public struct Params: Equatable {
     public init(
       e2eId: Int,
-      follower: UdNetworkStatus,
       username: String?,
       registrationValidationSignature: Data?,
       cert: Data,
@@ -13,7 +12,6 @@ public struct NewOrLoadUd {
       address: String
     ) {
       self.e2eId = e2eId
-      self.follower = follower
       self.username = username
       self.registrationValidationSignature = registrationValidationSignature
       self.cert = cert
@@ -22,7 +20,6 @@ public struct NewOrLoadUd {
     }
 
     public var e2eId: Int
-    public var follower: UdNetworkStatus
     public var username: String?
     public var registrationValidationSignature: Data?
     public var cert: Data
@@ -30,19 +27,22 @@ public struct NewOrLoadUd {
     public var address: String
   }
 
-  public var run: (Params) throws -> UserDiscovery
+  public var run: (Params, UdNetworkStatus) throws -> UserDiscovery
 
-  public func callAsFunction(_ params: Params) throws -> UserDiscovery {
-    try run(params)
+  public func callAsFunction(
+    params: Params,
+    follower: UdNetworkStatus
+  ) throws -> UserDiscovery {
+    try run(params, follower)
   }
 }
 
 extension NewOrLoadUd {
-  public static let live = NewOrLoadUd { params in
+  public static let live = NewOrLoadUd { params, follower in
     var error: NSError?
     let bindingsUD = BindingsNewOrLoadUd(
       params.e2eId,
-      params.follower.makeBindingsUdNetworkStatus(),
+      follower.makeBindingsUdNetworkStatus(),
       params.username,
       params.registrationValidationSignature,
       params.cert,
diff --git a/Sources/XXClient/Models/Environment.swift b/Sources/XXClient/Models/NDFEnvironment.swift
similarity index 90%
rename from Sources/XXClient/Models/Environment.swift
rename to Sources/XXClient/Models/NDFEnvironment.swift
index 92df7275de565f0953458ed05f6d32f2e04c9031..701a144c7ce8360fdb296e15b1725f13c1d42dd8 100644
--- a/Sources/XXClient/Models/Environment.swift
+++ b/Sources/XXClient/Models/NDFEnvironment.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-public struct Environment: Equatable {
+public struct NDFEnvironment: Equatable {
   public init(url: URL, cert: String) {
     self.url = url
     self.cert = cert
@@ -10,8 +10,8 @@ public struct Environment: Equatable {
   public var cert: String
 }
 
-extension Environment {
-  public static let mainnet = Environment(
+extension NDFEnvironment {
+  public static let mainnet = NDFEnvironment(
     url: URL(string: "https://elixxir-bins.s3.us-west-1.amazonaws.com/ndf/mainnet.json")!,
     cert: """
       -----BEGIN CERTIFICATE-----
@@ -50,3 +50,10 @@ extension Environment {
       """
   )
 }
+
+extension NDFEnvironment {
+  public static let unimplemented = NDFEnvironment(
+    url: URL(fileURLWithPath: "unimplemented"),
+    cert: "unimplemented"
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8cebb73a648773e63137a86a4481142fe2f5b173
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerConnect.swift
@@ -0,0 +1,35 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerConnect {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+  }
+
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerConnect {
+  public static func live(_ env: MessengerEnvironment) -> MessengerConnect {
+    MessengerConnect {
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+      env.e2e.set(try env.login(
+        cMixId: cMix.getId(),
+        identity: try cMix.makeLegacyReceptionIdentity(),
+        e2eParamsJSON: env.getE2EParams()
+      ))
+    }
+  }
+}
+
+extension MessengerConnect {
+  public static let unimplemented = MessengerConnect(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b36b55b447956d139f227887a154bec71d109312
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerCreate.swift
@@ -0,0 +1,35 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerCreate {
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerCreate {
+  public static func live(_ env: MessengerEnvironment) -> MessengerCreate {
+    MessengerCreate {
+      let ndfData = try env.downloadNDF(env.ndfEnvironment)
+      let password = env.generateSecret()
+      try env.passwordStorage.save(password)
+      let storageDir = env.storageDir
+      try env.fileManager.removeDirectory(storageDir)
+      try env.fileManager.createDirectory(storageDir)
+      try env.newCMix(
+        ndfJSON: String(data: ndfData, encoding: .utf8)!,
+        storageDir: storageDir,
+        password: password,
+        registrationCode: nil
+      )
+    }
+  }
+}
+
+extension MessengerCreate {
+  public static let unimplemented = MessengerCreate(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c30437faeb6b4cbc8b0a66df811bc55bd5618e28
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsConnected.swift
@@ -0,0 +1,25 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsConnected {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsConnected {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsConnected {
+    MessengerIsConnected {
+      env.e2e() != nil
+    }
+  }
+}
+
+extension MessengerIsConnected {
+  public static let unimplemented = MessengerIsConnected(
+    run: XCTUnimplemented()
+  )
+}
+
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c848518c2483e49acbdbfe3d6d0b9e0c7fc2ce32
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsCreated.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsCreated {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsCreated {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsCreated {
+    MessengerIsCreated {
+      env.fileManager.isDirectoryEmpty(env.storageDir) == false
+    }
+  }
+}
+
+extension MessengerIsCreated {
+  public static let unimplemented = MessengerIsCreated(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift
new file mode 100644
index 0000000000000000000000000000000000000000..dc0b42165efe5b39071674a5019a7e11860d53f5
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoaded.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsLoaded {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsLoaded {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsLoaded {
+    MessengerIsLoaded {
+      env.cMix() != nil
+    }
+  }
+}
+
+extension MessengerIsLoaded {
+  public static let unimplemented = MessengerIsLoaded(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7c54c5785fc472c4815f3a91f626575881cf2dcd
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsLoggedIn.swift
@@ -0,0 +1,24 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsLoggedIn {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsLoggedIn {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsLoggedIn {
+    MessengerIsLoggedIn {
+      env.ud() != nil
+    }
+  }
+}
+
+extension MessengerIsLoggedIn {
+  public static let unimplemented = MessengerIsLoggedIn(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bf3cafa541fa67847f749325419c683e9edd34c6
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerIsRegistered.swift
@@ -0,0 +1,31 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerIsRegistered {
+  public enum Error: Swift.Error, Equatable {
+    case notConnected
+  }
+
+  public var run: () throws -> Bool
+
+  public func callAsFunction() throws -> Bool {
+    try run()
+  }
+}
+
+extension MessengerIsRegistered {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsRegistered {
+    MessengerIsRegistered {
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      return try env.isRegisteredWithUD(e2eId: e2e.getId())
+    }
+  }
+}
+
+extension MessengerIsRegistered {
+  public static let unimplemented = MessengerIsRegistered(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift
new file mode 100644
index 0000000000000000000000000000000000000000..29f281688d0cb6e4611cbf50a5ee2f7844df07f1
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerLoad.swift
@@ -0,0 +1,28 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerLoad {
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerLoad {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLoad {
+    MessengerLoad {
+      env.cMix.set(try env.loadCMix(
+        storageDir: env.storageDir,
+        password: try env.passwordStorage.load(),
+        cMixParamsJSON: env.getCMixParams()
+      ))
+    }
+  }
+}
+
+extension MessengerLoad {
+  public static let unimplemented = MessengerLoad(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift
new file mode 100644
index 0000000000000000000000000000000000000000..d0f86d8bcfa8ea7f7c0251d573a5dc591b59ddc0
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerLogIn.swift
@@ -0,0 +1,47 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerLogIn {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+    case notConnected
+  }
+
+  public var run: () throws -> Void
+
+  public func callAsFunction() throws {
+    try run()
+  }
+}
+
+extension MessengerLogIn {
+  public static func live(_ env: MessengerEnvironment) -> MessengerLogIn {
+    MessengerLogIn {
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      env.ud.set(try env.newOrLoadUd(
+        params: .init(
+          e2eId: e2e.getId(),
+          username: nil,
+          registrationValidationSignature: nil,
+          cert: env.udCert ?? e2e.getUdCertFromNdf(),
+          contactFile: env.udContact ?? (try e2e.getUdContactFromNdf()),
+          address: env.udAddress ?? e2e.getUdAddressFromNdf()
+        ),
+        follower: .init {
+          cMix.networkFollowerStatus().rawValue
+        }
+      ))
+    }
+  }
+}
+
+extension MessengerLogIn {
+  public static let unimplemented = MessengerLogIn(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6f4296318c4eccf771ccb505cb584db458b74f8b
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerRegister.swift
@@ -0,0 +1,49 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerRegister {
+  public enum Error: Swift.Error, Equatable {
+    case notLoaded
+    case notConnected
+  }
+
+  public var run: (String) throws -> Void
+
+  public func callAsFunction(
+    username: String
+  ) throws {
+    try run(username)
+  }
+}
+
+extension MessengerRegister {
+  public static func live(_ env: MessengerEnvironment) -> MessengerRegister {
+    MessengerRegister { username in
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+      guard let e2e = env.e2e() else {
+        throw Error.notConnected
+      }
+      env.ud.set(try env.newOrLoadUd(
+        params: .init(
+          e2eId: e2e.getId(),
+          username: username,
+          registrationValidationSignature: cMix.getReceptionRegistrationValidationSignature(),
+          cert: env.udCert ?? e2e.getUdCertFromNdf(),
+          contactFile: env.udContact ?? (try e2e.getUdContactFromNdf()),
+          address: env.udAddress ?? e2e.getUdAddressFromNdf()
+        ),
+        follower: .init {
+          cMix.networkFollowerStatus().rawValue
+        }
+      ))
+    }
+  }
+}
+
+extension MessengerRegister {
+  public static let unimplemented = MessengerRegister(
+    run: XCTUnimplemented()
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerStart.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerStart.swift
new file mode 100644
index 0000000000000000000000000000000000000000..cc3363ec543ee2c3ed6c2414ac79d1ceaa0ccc33
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerStart.swift
@@ -0,0 +1,36 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerStart {
+  public enum Error: Swift.Error {
+    case notLoaded
+  }
+
+  public var run: (Int) throws -> Void
+
+  public func callAsFunction(
+    timeoutMS: Int = 30_000
+  ) throws {
+    try run(timeoutMS)
+  }
+}
+
+extension MessengerStart {
+  public static func live(_ env: MessengerEnvironment) -> MessengerStart {
+    MessengerStart { timeoutMS in
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+      guard cMix.networkFollowerStatus() != .running else {
+        return
+      }
+      try cMix.startNetworkFollower(timeoutMS: timeoutMS)
+    }
+  }
+}
+
+extension MessengerStart {
+  public static let unimplemented = MessengerStart(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNetwork.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNetwork.swift
new file mode 100644
index 0000000000000000000000000000000000000000..8fe8cf3e4750f1d3bff8a7191574a143639466c7
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNetwork.swift
@@ -0,0 +1,36 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerWaitForNetwork {
+  public enum Error: Swift.Error {
+    case notLoaded
+    case timeout
+  }
+
+  public var run: (Int) throws -> Void
+
+  public func callAsFunction(
+    timeoutMS: Int = 30_000
+  ) throws {
+    try run(timeoutMS)
+  }
+}
+
+extension MessengerWaitForNetwork {
+  public static func live(_ env: MessengerEnvironment) -> MessengerWaitForNetwork {
+    MessengerWaitForNetwork { timeoutMS in
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+      guard cMix.waitForNetwork(timeoutMS: timeoutMS) else {
+        throw Error.timeout
+      }
+    }
+  }
+}
+
+extension MessengerWaitForNetwork {
+  public static let unimplemented = MessengerWaitForNetwork(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b87acc2f13caf51d2998d3a864a6484f897dc528
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
@@ -0,0 +1,53 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerWaitForNodes {
+  public typealias Progress = (NodeRegistrationReport) -> Void
+
+  public enum Error: Swift.Error {
+    case notLoaded
+    case timeout
+  }
+
+  public var run: (Double, Int, Int, @escaping Progress) throws -> Void
+
+  public func callAsFunction(
+    targetRatio: Double = 0.8,
+    sleepMS: Int = 1_000,
+    retries: Int = 10,
+    onProgress: @escaping Progress = { _ in }
+  ) throws {
+    try run(targetRatio, sleepMS, retries, onProgress)
+  }
+}
+
+extension MessengerWaitForNodes {
+  public static func live(_ env: MessengerEnvironment) -> MessengerWaitForNodes {
+    MessengerWaitForNodes { targetRatio, sleepMS, retries, onProgress in
+      guard let cMix = env.cMix() else {
+        throw Error.notLoaded
+      }
+
+      var report = try cMix.getNodeRegistrationStatus()
+      var retries = retries
+      onProgress(report)
+
+      while report.ratio < targetRatio && retries > 0 {
+        env.sleep(sleepMS)
+        report = try cMix.getNodeRegistrationStatus()
+        retries -= 1
+        onProgress(report)
+      }
+
+      if report.ratio < targetRatio {
+        throw Error.timeout
+      }
+    }
+  }
+}
+
+extension MessengerWaitForNodes {
+  public static let unimplemented = MessengerWaitForNodes(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
new file mode 100644
index 0000000000000000000000000000000000000000..30d63efdfac2716a0d3efc7f0a614eafeaf2e852
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -0,0 +1,64 @@
+import XXClient
+
+public struct Messenger {
+  public var cMix: Stored<CMix?>
+  public var e2e: Stored<E2E?>
+  public var ud: Stored<UserDiscovery?>
+  public var isCreated: MessengerIsCreated
+  public var create: MessengerCreate
+  public var isLoaded: MessengerIsLoaded
+  public var load: MessengerLoad
+  public var start: MessengerStart
+  public var isConnected: MessengerIsConnected
+  public var connect: MessengerConnect
+  public var isRegistered: MessengerIsRegistered
+  public var register: MessengerRegister
+  public var isLoggedIn: MessengerIsLoggedIn
+  public var logIn: MessengerLogIn
+  public var waitForNetwork: MessengerWaitForNetwork
+  public var waitForNodes: MessengerWaitForNodes
+}
+
+extension Messenger {
+  public static func live(_ env: MessengerEnvironment) -> Messenger {
+    Messenger(
+      cMix: env.cMix,
+      e2e: env.e2e,
+      ud: env.ud,
+      isCreated: .live(env),
+      create: .live(env),
+      isLoaded: .live(env),
+      load: .live(env),
+      start: .live(env),
+      isConnected: .live(env),
+      connect: .live(env),
+      isRegistered: .live(env),
+      register: .live(env),
+      isLoggedIn: .live(env),
+      logIn: .live(env),
+      waitForNetwork: .live(env),
+      waitForNodes: .live(env)
+    )
+  }
+}
+
+extension Messenger {
+  public static let unimplemented = Messenger(
+    cMix: .unimplemented(),
+    e2e: .unimplemented(),
+    ud: .unimplemented(),
+    isCreated: .unimplemented,
+    create: .unimplemented,
+    isLoaded: .unimplemented,
+    load: .unimplemented,
+    start: .unimplemented,
+    isConnected: .unimplemented,
+    connect: .unimplemented,
+    isRegistered: .unimplemented,
+    register: .unimplemented,
+    isLoggedIn: .unimplemented,
+    logIn: .unimplemented,
+    waitForNetwork: .unimplemented,
+    waitForNodes: .unimplemented
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
new file mode 100644
index 0000000000000000000000000000000000000000..0918142a1f92dd6bbc5392bb562113464f349b20
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -0,0 +1,84 @@
+import Foundation
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerEnvironment {
+  public var cMix: Stored<CMix?>
+  public var downloadNDF: DownloadAndVerifySignedNdf
+  public var e2e: Stored<E2E?>
+  public var fileManager: MessengerFileManager
+  public var generateSecret: GenerateSecret
+  public var getCMixParams: GetCMixParams
+  public var getE2EParams: GetE2EParams
+  public var isRegisteredWithUD: IsRegisteredWithUD
+  public var loadCMix: LoadCMix
+  public var login: Login
+  public var ndfEnvironment: NDFEnvironment
+  public var newCMix: NewCMix
+  public var newOrLoadUd: NewOrLoadUd
+  public var passwordStorage: PasswordStorage
+  public var sleep: (Int) -> Void
+  public var storageDir: String
+  public var ud: Stored<UserDiscovery?>
+  public var udAddress: String?
+  public var udCert: Data?
+  public var udContact: Data?
+}
+
+extension MessengerEnvironment {
+  public static let defaultStorageDir = FileManager.default
+    .urls(for: .applicationSupportDirectory, in: .userDomainMask)
+    .first!
+    .appendingPathComponent("xx.network.client")
+    .path
+
+  public static func live() -> MessengerEnvironment {
+    MessengerEnvironment(
+      cMix: .inMemory(),
+      downloadNDF: .live,
+      e2e: .inMemory(),
+      fileManager: .live(),
+      generateSecret: .live,
+      getCMixParams: .liveDefault,
+      getE2EParams: .liveDefault,
+      isRegisteredWithUD: .live,
+      loadCMix: .live,
+      login: .live,
+      ndfEnvironment: .mainnet,
+      newCMix: .live,
+      newOrLoadUd: .live,
+      passwordStorage: .keychain,
+      sleep: { Foundation.sleep(UInt32($0)) },
+      storageDir: MessengerEnvironment.defaultStorageDir,
+      ud: .inMemory(),
+      udAddress: nil,
+      udCert: nil,
+      udContact: nil
+    )
+  }
+}
+
+extension MessengerEnvironment {
+  public static let unimplemented = MessengerEnvironment(
+    cMix: .unimplemented(),
+    downloadNDF: .unimplemented,
+    e2e: .unimplemented(),
+    fileManager: .unimplemented,
+    generateSecret: .unimplemented,
+    getCMixParams: .unimplemented,
+    getE2EParams: .unimplemented,
+    isRegisteredWithUD: .unimplemented,
+    loadCMix: .unimplemented,
+    login: .unimplemented,
+    ndfEnvironment: .unimplemented,
+    newCMix: .unimplemented,
+    newOrLoadUd: .unimplemented,
+    passwordStorage: .unimplemented,
+    sleep: XCTUnimplemented("\(Self.self).sleep"),
+    storageDir: "unimplemented",
+    ud: .unimplemented(),
+    udAddress: nil,
+    udCert: nil,
+    udContact: nil
+  )
+}
diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
new file mode 100644
index 0000000000000000000000000000000000000000..4ff49c5fe155e612852860d1d7276124729017d3
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift
@@ -0,0 +1,40 @@
+import Foundation
+import XCTestDynamicOverlay
+
+public struct MessengerFileManager {
+  public var isDirectoryEmpty: (String) -> Bool
+  public var removeDirectory: (String) throws -> Void
+  public var createDirectory: (String) throws -> Void
+}
+
+extension MessengerFileManager {
+  public static func live(
+    fileManager: FileManager = .default
+  ) -> MessengerFileManager {
+    MessengerFileManager(
+      isDirectoryEmpty: { path in
+        let contents = try? fileManager.contentsOfDirectory(atPath: path)
+        return contents?.isEmpty ?? true
+      },
+      removeDirectory: { path in
+        if fileManager.fileExists(atPath: path) {
+          try fileManager.removeItem(atPath: path)
+        }
+      },
+      createDirectory: { path in
+        try fileManager.createDirectory(
+          atPath: path,
+          withIntermediateDirectories: true
+        )
+      }
+    )
+  }
+}
+
+extension MessengerFileManager {
+  public static let unimplemented = MessengerFileManager(
+    isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty"),
+    removeDirectory: XCTUnimplemented("\(Self.self).removeDirectory"),
+    createDirectory: XCTUnimplemented("\(Self.self).createDirectory")
+  )
+}
diff --git a/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift b/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift
new file mode 100644
index 0000000000000000000000000000000000000000..0a062cc628cf037491a37ba8639e1f2529f209e8
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/PasswordStorage+Keychain.swift
@@ -0,0 +1,21 @@
+import KeychainAccess
+import XXClient
+
+extension PasswordStorage {
+  public static let keychain: PasswordStorage = {
+    let keychain = KeychainAccess.Keychain(
+      service: "xx.network.client.messenger"
+    )
+    return PasswordStorage(
+      save: { password in
+        keychain[data: "password"] = password
+      },
+      load: {
+        guard let password = keychain[data: "password"] else {
+          throw MissingPasswordError()
+        }
+        return password
+      }
+    )
+  }()
+}
diff --git a/Sources/XXMessengerClient/Utils/Stored.swift b/Sources/XXMessengerClient/Utils/Stored.swift
new file mode 100644
index 0000000000000000000000000000000000000000..56b5e97ef9db3fcc3d2ab7b8452b8fbb434acce6
--- /dev/null
+++ b/Sources/XXMessengerClient/Utils/Stored.swift
@@ -0,0 +1,41 @@
+import XCTestDynamicOverlay
+
+public struct Stored<Value> {
+  public var get: () -> Value
+  public var set: (Value) -> Void
+
+  public func callAsFunction() -> Value {
+    get()
+  }
+}
+
+extension Stored {
+  public static func inMemory(_ value: Value) -> Stored<Value> {
+    let memory = Memory(value)
+    return Stored(
+      get: { memory.value },
+      set: { memory.value = $0 }
+    )
+  }
+
+  public static func inMemory<V>() -> Stored<Optional<V>> where Value == Optional<V> {
+    inMemory(nil)
+  }
+}
+
+private final class Memory<Value> {
+  init(_ value: Value) {
+    self.value = value
+  }
+
+  var value: Value
+}
+
+extension Stored {
+  public static func unimplemented() -> Stored<Value> {
+    Stored<Value>(
+      get: XCTUnimplemented("\(Self.self).get"),
+      set: XCTUnimplemented("\(Self.self).set")
+    )
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fb780353631b32f3f4bcf686bf60f3c3e38703c3
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerConnectTests.swift
@@ -0,0 +1,118 @@
+import CustomDump
+import XXClient
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerConnectTests: XCTestCase {
+  func testConnect() throws {
+    struct DidLogIn: Equatable {
+      var ephemeral: Bool
+      var cMixId: Int
+      var authCallbacksProvided: Bool
+      var identity: ReceptionIdentity
+      var e2eParamsJSON: Data
+    }
+
+    var didLogIn: [DidLogIn] = []
+    var didSetE2E: [E2E?] = []
+
+    let cMixId = 1234
+    let receptionId = ReceptionIdentity.stub
+    let e2eParams = "e2e-params".data(using: .utf8)!
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getId.run = { cMixId }
+      cMix.makeLegacyReceptionIdentity.run = { receptionId }
+      return cMix
+    }
+    env.e2e.set = { didSetE2E.append($0) }
+    env.getE2EParams.run = { e2eParams }
+    env.login.run = { ephemeral, cMixId, authCallbacks, identity, e2eParamsJSON in
+      didLogIn.append(.init(
+        ephemeral: ephemeral,
+        cMixId: cMixId,
+        authCallbacksProvided: authCallbacks != nil,
+        identity: identity,
+        e2eParamsJSON: e2eParamsJSON
+      ))
+      return .unimplemented
+    }
+    let connect: MessengerConnect = .live(env)
+
+    try connect()
+
+    XCTAssertNoDifference(didLogIn, [
+      DidLogIn(
+        ephemeral: false,
+        cMixId: 1234,
+        authCallbacksProvided: false,
+        identity: .stub,
+        e2eParamsJSON: e2eParams
+      )
+    ])
+    XCTAssertEqual(didSetE2E.compactMap { $0 }.count, 1)
+  }
+
+  func testConnectWithoutCMix() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { error in
+      XCTAssertEqual(
+        error as? MessengerConnect.Error,
+        MessengerConnect.Error.notLoaded
+      )
+    }
+  }
+
+  func testMakeLegacyReceptionIdentityFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getId.run = { 1234 }
+      cMix.makeLegacyReceptionIdentity.run = { throw error }
+      return cMix
+    }
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testLoginFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getId.run = { 1234 }
+      cMix.makeLegacyReceptionIdentity.run = { .stub }
+      return cMix
+    }
+    env.getE2EParams.run = { "e2e-params".data(using: .utf8)! }
+    env.login.run = { _, _, _, _, _ in throw error }
+    let connect: MessengerConnect = .live(env)
+
+    XCTAssertThrowsError(try connect()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
+
+private extension ReceptionIdentity {
+  static let stub = ReceptionIdentity(
+    id: "id".data(using: .utf8)!,
+    rsaPrivatePem: "rsaPrivatePem".data(using: .utf8)!,
+    salt: "salt".data(using: .utf8)!,
+    dhKeyPrivate: "dhKeyPrivate".data(using: .utf8)!,
+    e2eGrp: "e2eGrp".data(using: .utf8)!
+  )
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2180dda3db578b74a892b2166e66a9beb20357dd
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerCreateTests.swift
@@ -0,0 +1,157 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerCreateTests: XCTestCase {
+  func testCreate() throws {
+    struct DidNewCMix: Equatable {
+      var ndfJSON: String
+      var storageDir: String
+      var password: Data
+      var registrationCode: String?
+    }
+
+    var didDownloadNDF: [NDFEnvironment] = []
+    var didGenerateSecret: [Int] = []
+    var didSavePassword: [Data] = []
+    var didRemoveDirectory: [String] = []
+    var didCreateDirectory: [String] = []
+    var didNewCMix: [DidNewCMix] = []
+
+    let ndf = "ndf".data(using: .utf8)!
+    let password = "password".data(using: .utf8)!
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { ndfEnvironment in
+      didDownloadNDF.append(ndfEnvironment)
+      return ndf
+    }
+    env.generateSecret.run = { numBytes in
+      didGenerateSecret.append(numBytes)
+      return password
+    }
+    env.passwordStorage.save = { password in
+      didSavePassword.append(password)
+    }
+    env.storageDir = storageDir
+    env.fileManager.removeDirectory = { path in
+      didRemoveDirectory.append(path)
+    }
+    env.fileManager.createDirectory = { path in
+      didCreateDirectory.append(path)
+    }
+    env.newCMix.run = { ndfJSON, storageDir, password, registrationCode in
+      didNewCMix.append(.init(
+        ndfJSON: ndfJSON,
+        storageDir: storageDir,
+        password: password,
+        registrationCode: registrationCode
+      ))
+    }
+    let create: MessengerCreate = .live(env)
+
+    try create()
+
+    XCTAssertNoDifference(didDownloadNDF, [.unimplemented])
+    XCTAssertNoDifference(didGenerateSecret, [32])
+    XCTAssertNoDifference(didSavePassword, [password])
+    XCTAssertNoDifference(didRemoveDirectory, [storageDir])
+    XCTAssertNoDifference(didCreateDirectory, [storageDir])
+    XCTAssertNoDifference(didNewCMix, [.init(
+      ndfJSON: String(data: ndf, encoding: .utf8)!,
+      storageDir: storageDir,
+      password: password,
+      registrationCode: nil
+    )])
+  }
+
+  func testDownloadNDFFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testSavePasswordFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testRemoveDirectoryFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = "storage-dir"
+    env.fileManager.removeDirectory = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testCreateDirectoryFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = "storage-dir"
+    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.createDirectory = { _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewCMixFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ndfEnvironment = .unimplemented
+    env.downloadNDF.run = { _ in "ndf".data(using: .utf8)! }
+    env.generateSecret.run = { _ in "password".data(using: .utf8)! }
+    env.passwordStorage.save = { _ in }
+    env.storageDir = "storage-dir"
+    env.fileManager.removeDirectory = { _ in }
+    env.fileManager.createDirectory = { _ in }
+    env.newCMix.run = { _, _, _, _ in throw error }
+    let create: MessengerCreate = .live(env)
+
+    XCTAssertThrowsError(try create()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c7c5de8bc78a79257ce08725a32524a5ce78da50
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsConnectedTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsConnectedTests: XCTestCase {
+  func testWithE2E() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { .unimplemented }
+    let isConnected: MessengerIsConnected = .live(env)
+
+    XCTAssertTrue(isConnected())
+  }
+
+  func testWithoutE2E() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let isConnected: MessengerIsConnected = .live(env)
+
+    XCTAssertFalse(isConnected())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..246be9d3be6c04c58748ce024149da79fa8b63f7
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsCreatedTests.swift
@@ -0,0 +1,37 @@
+import CustomDump
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsCreatedTests: XCTestCase {
+  func testStorageDirNotEmpty() {
+    var didIsDirectoryEmpty: [String] = []
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = storageDir
+    env.fileManager.isDirectoryEmpty = { path in
+      didIsDirectoryEmpty.append(path)
+      return false
+    }
+    let isCreated: MessengerIsCreated = .live(env)
+
+    XCTAssertTrue(isCreated())
+    XCTAssertNoDifference(didIsDirectoryEmpty, [storageDir])
+  }
+
+  func testStorageDirEmpty() {
+    var didIsDirectoryEmpty: [String] = []
+    let storageDir = "storage-dir"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = storageDir
+    env.fileManager.isDirectoryEmpty = { path in
+      didIsDirectoryEmpty.append(path)
+      return true
+    }
+    let isCreated: MessengerIsCreated = .live(env)
+
+    XCTAssertFalse(isCreated())
+    XCTAssertNoDifference(didIsDirectoryEmpty, [storageDir])
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6f0591e264cbac1a12cf707b462b0be12d65aa74
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoadedTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsLoadedTests: XCTestCase {
+  func testWithCMix() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { .unimplemented }
+    let isLoaded: MessengerIsLoaded = .live(env)
+
+    XCTAssertTrue(isLoaded())
+  }
+
+  func testWithoutCMix() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let isLoaded: MessengerIsLoaded = .live(env)
+
+    XCTAssertFalse(isLoaded())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c6ea87e64f2c300f9ab7090b8b56fc7e730ee765
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsLoggedInTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsLoggedInTests: XCTestCase {
+  func testWithUD() {
+    var env: MessengerEnvironment = .unimplemented
+    env.ud.get = { .unimplemented }
+    let isLoggedIn: MessengerIsLoggedIn = .live(env)
+
+    XCTAssertTrue(isLoggedIn())
+  }
+
+  func testWithoutUD() {
+    var env: MessengerEnvironment = .unimplemented
+    env.ud.get = { nil }
+    let isLoggedIn: MessengerIsLoggedIn = .live(env)
+
+    XCTAssertFalse(isLoggedIn())
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b6dca3e9e4a09b5cc78b84b3d9a56ea12b032e3a
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerIsRegisteredTests.swift
@@ -0,0 +1,68 @@
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerIsRegisteredTests: XCTestCase {
+  func testRegistered() throws {
+    var didIsRegisteredWithUD: [Int] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      return e2e
+    }
+    env.isRegisteredWithUD.run = { e2eId in
+      didIsRegisteredWithUD.append(e2eId)
+      return true
+    }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertTrue(try isRegistered())
+    XCTAssertEqual(didIsRegisteredWithUD, [1234])
+  }
+
+  func testNotRegistered() throws {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      return e2e
+    }
+    env.isRegisteredWithUD.run = { _ in false }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertFalse(try isRegistered())
+  }
+
+  func testWithoutE2E() {
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = { nil }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertThrowsError(try isRegistered()) { err in
+      XCTAssertEqual(
+        err as? MessengerIsRegistered.Error,
+        MessengerIsRegistered.Error.notConnected
+      )
+    }
+  }
+
+  func testIsRegisteredWithUDFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      return e2e
+    }
+    env.isRegisteredWithUD.run = { _ in throw error }
+    let isRegistered: MessengerIsRegistered = .live(env)
+
+    XCTAssertThrowsError(try isRegistered()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5eb90e3ae5379959c4891a5c0b36032827b8eae7
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLoadTests.swift
@@ -0,0 +1,76 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerLoadTests: XCTestCase {
+  func testLoad() throws {
+    struct DidLoadCMix: Equatable {
+      var storageDir: String
+      var password: Data
+      var cMixParamsJSON: Data
+    }
+    var didLoadCMix: [DidLoadCMix] = []
+    var didSetCMix: [CMix?] = []
+
+    let storageDir = "test-storage-dir"
+    let password = "password".data(using: .utf8)!
+    let cMixParams = "cmix-params".data(using: .utf8)!
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.set = { didSetCMix.append($0) }
+    env.storageDir = storageDir
+    env.passwordStorage.load = { password }
+    env.getCMixParams.run = { cMixParams }
+    env.loadCMix.run = { storageDir, password, cMixParamsJSON in
+      didLoadCMix.append(.init(
+        storageDir: storageDir,
+        password: password,
+        cMixParamsJSON: cMixParamsJSON
+      ))
+      return .unimplemented
+    }
+    let load: MessengerLoad = .live(env)
+
+    try load()
+
+    XCTAssertNoDifference(didLoadCMix, [
+      DidLoadCMix(
+        storageDir: storageDir,
+        password: password,
+        cMixParamsJSON: cMixParams
+      )
+    ])
+    XCTAssertEqual(didSetCMix.compactMap{ $0 }.count, 1)
+  }
+
+  func testMissingPassword() {
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = "storage-dir"
+    env.passwordStorage.load = { throw PasswordStorage.MissingPasswordError() }
+    let load: MessengerLoad = .live(env)
+
+    XCTAssertThrowsError(try load()) { err in
+      XCTAssertEqual(
+        err as? PasswordStorage.MissingPasswordError,
+        PasswordStorage.MissingPasswordError()
+      )
+    }
+  }
+
+  func testLoadCMixFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.storageDir = "storage-dir"
+    env.passwordStorage.load = { "password".data(using: .utf8)! }
+    env.getCMixParams.run = { "cmix-params".data(using: .utf8)! }
+    env.loadCMix.run = { _, _, _ in throw error }
+    let load: MessengerLoad = .live(env)
+
+    XCTAssertThrowsError(try load()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..83dd2cd02fc79fcb5d460d244c025d05590a2233
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerLogInTests.swift
@@ -0,0 +1,180 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerLogInTests: XCTestCase {
+  func testLogin() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didNewOrLoadUDWithFollower: [UdNetworkStatus] = []
+    var didSetUD: [UserDiscovery?] = []
+
+    let e2eId = 1234
+    let networkFollowerStatus: NetworkFollowerStatus = .stopped
+    let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)!
+    let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)!
+    let udAddressFromNDF = "ndf-ud-address"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { networkFollowerStatus }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      e2e.getUdCertFromNdf.run = { udCertFromNDF }
+      e2e.getUdContactFromNdf.run = { udContactFromNDF }
+      e2e.getUdAddressFromNdf.run = { udAddressFromNDF }
+      return e2e
+    }
+    env.ud.set = { didSetUD.append($0) }
+    env.udCert = nil
+    env.udContact = nil
+    env.udAddress = nil
+    env.newOrLoadUd.run = { params, follower in
+      didNewOrLoadUDWithParams.append(params)
+      didNewOrLoadUDWithFollower.append(follower)
+      return .unimplemented
+    }
+    let logIn: MessengerLogIn = .live(env)
+    try logIn()
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: nil,
+      registrationValidationSignature: nil,
+      cert: udCertFromNDF,
+      contactFile: udContactFromNDF,
+      address: udAddressFromNDF
+    )])
+    XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1)
+    XCTAssertEqual(
+      didNewOrLoadUDWithFollower.first?.handle(),
+      networkFollowerStatus.rawValue
+    )
+    XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1)
+  }
+
+  func testLoginWithAlternativeUD() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didSetUD: [UserDiscovery?] = []
+
+    let e2eId = 1234
+    let altUdCert = "alt-ud-cert".data(using: .utf8)!
+    let altUdContact = "alt-ud-contact".data(using: .utf8)!
+    let altUdAddress = "alt-ud-address"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      return e2e
+    }
+    env.ud.set = { didSetUD.append($0) }
+    env.udCert = altUdCert
+    env.udContact = altUdContact
+    env.udAddress = altUdAddress
+    env.newOrLoadUd.run = { params, _ in
+      didNewOrLoadUDWithParams.append(params)
+      return .unimplemented
+    }
+    let logIn: MessengerLogIn = .live(env)
+    try logIn()
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: nil,
+      registrationValidationSignature: nil,
+      cert: altUdCert,
+      contactFile: altUdContact,
+      address: altUdAddress
+    )])
+    XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1)
+  }
+
+  func testLoginWithoutCMix() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { error in
+      XCTAssertEqual(
+        error as? MessengerLogIn.Error,
+        MessengerLogIn.Error.notLoaded
+      )
+    }
+  }
+
+  func testLoginWithoutE2E() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { .unimplemented }
+    env.e2e.get = { nil }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { error in
+      XCTAssertEqual(
+        error as? MessengerLogIn.Error,
+        MessengerLogIn.Error.notConnected
+      )
+    }
+  }
+
+  func testGetUdContactFromNdfFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      e2e.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! }
+      e2e.getUdContactFromNdf.run = { throw error }
+      return e2e
+    }
+    env.udCert = nil
+    env.udContact = nil
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewOrLoadUdFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      return e2e
+    }
+    env.udCert = "ud-cert".data(using: .utf8)!
+    env.udContact = "ud-contact".data(using: .utf8)!
+    env.udAddress = "ud-address"
+    env.newOrLoadUd.run = { _, _ in throw error }
+    let logIn: MessengerLogIn = .live(env)
+
+    XCTAssertThrowsError(try logIn()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ef96b25197a388e7b258a341638e053cb63ff3ad
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerRegisterTests.swift
@@ -0,0 +1,196 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerRegisterTests: XCTestCase {
+  func testRegister() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didNewOrLoadUDWithFollower: [UdNetworkStatus] = []
+    var didSetUD: [UserDiscovery?] = []
+
+    let e2eId = 1234
+    let networkFollowerStatus: NetworkFollowerStatus = .stopped
+    let registrationSignature = "registration-signature".data(using: .utf8)!
+    let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)!
+    let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)!
+    let udAddressFromNDF = "ndf-ud-address"
+    let username = "new-user-name"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { networkFollowerStatus }
+      cMix.getReceptionRegistrationValidationSignature.run = {
+        registrationSignature
+      }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      e2e.getUdCertFromNdf.run = { udCertFromNDF }
+      e2e.getUdContactFromNdf.run = { udContactFromNDF }
+      e2e.getUdAddressFromNdf.run = { udAddressFromNDF }
+      return e2e
+    }
+    env.ud.set = { didSetUD.append($0) }
+    env.udCert = nil
+    env.udContact = nil
+    env.udAddress = nil
+    env.newOrLoadUd.run = { params, follower in
+      didNewOrLoadUDWithParams.append(params)
+      didNewOrLoadUDWithFollower.append(follower)
+      return .unimplemented
+    }
+    let register: MessengerRegister = .live(env)
+    try register(username: username)
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: username,
+      registrationValidationSignature: registrationSignature,
+      cert: udCertFromNDF,
+      contactFile: udContactFromNDF,
+      address: udAddressFromNDF
+    )])
+    XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1)
+    XCTAssertEqual(
+      didNewOrLoadUDWithFollower.first?.handle(),
+      networkFollowerStatus.rawValue
+    )
+    XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1)
+  }
+
+  func testRegisterWithAlternativeUD() throws {
+    var didNewOrLoadUDWithParams: [NewOrLoadUd.Params] = []
+    var didSetUD: [UserDiscovery?] = []
+
+    let e2eId = 1234
+    let registrationSignature = "registration-signature".data(using: .utf8)!
+    let altUdCert = "alt-ud-cert".data(using: .utf8)!
+    let altUdContact = "alt-ud-contact".data(using: .utf8)!
+    let altUdAddress = "alt-ud-address"
+    let username = "new-user-name"
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      cMix.getReceptionRegistrationValidationSignature.run = {
+        registrationSignature
+      }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { e2eId }
+      return e2e
+    }
+    env.ud.set = { didSetUD.append($0) }
+    env.udCert = altUdCert
+    env.udContact = altUdContact
+    env.udAddress = altUdAddress
+    env.newOrLoadUd.run = { params, _ in
+      didNewOrLoadUDWithParams.append(params)
+      return .unimplemented
+    }
+    let register: MessengerRegister = .live(env)
+    try register(username: username)
+
+    XCTAssertNoDifference(didNewOrLoadUDWithParams, [.init(
+      e2eId: e2eId,
+      username: username,
+      registrationValidationSignature: registrationSignature,
+      cert: altUdCert,
+      contactFile: altUdContact,
+      address: altUdAddress
+    )])
+    XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1)
+  }
+
+  func testRegisterWithoutCMix() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { error in
+      XCTAssertEqual(
+        error as? MessengerRegister.Error,
+        MessengerRegister.Error.notLoaded
+      )
+    }
+  }
+
+  func testRegisterWithoutE2E() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { .unimplemented }
+    env.e2e.get = { nil }
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { error in
+      XCTAssertEqual(
+        error as? MessengerRegister.Error,
+        MessengerRegister.Error.notConnected
+      )
+    }
+  }
+
+  func testGetUdContactFromNdfFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      cMix.getReceptionRegistrationValidationSignature.run = {
+        "registration-signature".data(using: .utf8)!
+      }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      e2e.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! }
+      e2e.getUdContactFromNdf.run = { throw error }
+      return e2e
+    }
+    env.udCert = nil
+    env.udContact = nil
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+
+  func testNewOrLoadUdFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      cMix.getReceptionRegistrationValidationSignature.run = {
+        "registration-signature".data(using: .utf8)!
+      }
+      return cMix
+    }
+    env.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getId.run = { 1234 }
+      return e2e
+    }
+    env.udCert = "ud-cert".data(using: .utf8)!
+    env.udContact = "ud-contact".data(using: .utf8)!
+    env.udAddress = "ud-address"
+    env.newOrLoadUd.run = { _, _ in throw error }
+    let register: MessengerRegister = .live(env)
+
+    XCTAssertThrowsError(try register(username: "new-user-name")) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerStartTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerStartTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7b3e33af570bd8a522d26091ef551be5fec7b674
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerStartTests.swift
@@ -0,0 +1,68 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerStartTests: XCTestCase {
+  func testStart() throws {
+    var didStartNetworkFollower: [Int] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .stopped }
+      cMix.startNetworkFollower.run = { timeoutMS in
+        didStartNetworkFollower.append(timeoutMS)
+      }
+      return cMix
+    }
+    let start: MessengerStart = .live(env)
+
+    try start(timeoutMS: 123)
+
+    XCTAssertNoDifference(didStartNetworkFollower, [123])
+  }
+
+  func testStartWhenNotLoaded() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let start: MessengerStart = .live(env)
+
+    XCTAssertThrowsError(try start()) { error in
+      XCTAssertEqual(
+        error as? MessengerStart.Error,
+        MessengerStart.Error.notLoaded
+      )
+    }
+  }
+
+  func testStartWhenRunning() throws {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .running }
+      return cMix
+    }
+    let start: MessengerStart = .live(env)
+
+    try start()
+  }
+
+  func testStartNetworkFollowerFailure() {
+    struct Error: Swift.Error, Equatable {}
+    let error = Error()
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.networkFollowerStatus.run = { .stopped }
+      cMix.startNetworkFollower.run = { _ in throw error }
+      return cMix
+    }
+    let start: MessengerStart = .live(env)
+
+    XCTAssertThrowsError(try start()) { err in
+      XCTAssertEqual(err as? Error, error)
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNetworkTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNetworkTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1dbae73528c82a00c4270bf99c759356c7488171
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNetworkTests.swift
@@ -0,0 +1,55 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerWaitForNetworkTests: XCTestCase {
+  func testWaitSuccess() throws {
+    var didWaitForNetwork: [Int] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForNetwork.run = { timeoutMS in
+        didWaitForNetwork.append(timeoutMS)
+        return true
+      }
+      return cMix
+    }
+    let waitForNetwork: MessengerWaitForNetwork = .live(env)
+
+    try waitForNetwork(timeoutMS: 123)
+
+    XCTAssertNoDifference(didWaitForNetwork, [123])
+  }
+
+  func testWaitWhenNotLoaded() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let waitForNetwork: MessengerWaitForNetwork = .live(env)
+
+    XCTAssertThrowsError(try waitForNetwork()) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNetwork.Error,
+        MessengerWaitForNetwork.Error.notLoaded
+      )
+    }
+  }
+
+  func testWaitTimeout() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForNetwork.run = { _ in false }
+      return cMix
+    }
+    let waitForNetwork: MessengerWaitForNetwork = .live(env)
+
+    XCTAssertThrowsError(try waitForNetwork()) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNetwork.Error,
+        MessengerWaitForNetwork.Error.timeout
+      )
+    }
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bc23afb53c604fd0b737bba5db544edfebdffe68
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
@@ -0,0 +1,119 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerWaitForNodesTests: XCTestCase {
+  func testWaitWhenNotLoaded() {
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = { nil }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    XCTAssertThrowsError(try waitForNodes()) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNodes.Error,
+        MessengerWaitForNodes.Error.notLoaded
+      )
+    }
+  }
+
+  func testWaitWhenHasTargetRatio() throws {
+    var didProgress: [NodeRegistrationReport] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = {
+        NodeRegistrationReport(registered: 8, total: 10)
+      }
+      return cMix
+    }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )
+
+    XCTAssertNoDifference(didProgress, [
+      NodeRegistrationReport(registered: 8, total: 10)
+    ])
+  }
+
+  func testWaitForTargetRatio() throws {
+    var didSleep: [Int] = []
+    var didProgress: [NodeRegistrationReport] = []
+
+    var reports: [NodeRegistrationReport] = [
+      .init(registered: 0, total: 10),
+      .init(registered: 3, total: 10),
+      .init(registered: 8, total: 10),
+    ]
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = { reports.removeFirst() }
+      return cMix
+    }
+    env.sleep = { didSleep.append($0) }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )
+
+    XCTAssertNoDifference(didSleep, [123, 123])
+    XCTAssertNoDifference(didProgress, [
+      NodeRegistrationReport(registered: 0, total: 10),
+      NodeRegistrationReport(registered: 3, total: 10),
+      NodeRegistrationReport(registered: 8, total: 10),
+    ])
+  }
+
+  func testWaitTimeout() {
+    var didSleep: [Int] = []
+    var didProgress: [NodeRegistrationReport] = []
+
+    var reports: [NodeRegistrationReport] = [
+      .init(registered: 0, total: 10),
+      .init(registered: 3, total: 10),
+      .init(registered: 5, total: 10),
+      .init(registered: 6, total: 10),
+    ]
+
+    var env: MessengerEnvironment = .unimplemented
+    env.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.getNodeRegistrationStatus.run = { reports.removeFirst() }
+      return cMix
+    }
+    env.sleep = { didSleep.append($0) }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    XCTAssertThrowsError(try waitForNodes(
+      targetRatio: 0.7,
+      sleepMS: 123,
+      retries: 3,
+      onProgress: { didProgress.append($0) }
+    )) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNodes.Error,
+        MessengerWaitForNodes.Error.timeout
+      )
+    }
+
+    XCTAssertNoDifference(didSleep, [123, 123, 123])
+    XCTAssertNoDifference(didProgress, [
+      NodeRegistrationReport(registered: 0, total: 10),
+      NodeRegistrationReport(registered: 3, total: 10),
+      NodeRegistrationReport(registered: 5, total: 10),
+      NodeRegistrationReport(registered: 6, total: 10),
+    ])
+  }
+}
diff --git a/Tests/XXMessengerClientTests/Messenger/Utils/StoredTests.swift b/Tests/XXMessengerClientTests/Messenger/Utils/StoredTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..154f61967627500def1526f2944aeb074792c748
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Utils/StoredTests.swift
@@ -0,0 +1,18 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class StoredTests: XCTestCase {
+  func testInMemory() throws {
+    let stored: Stored<String?> = .inMemory()
+
+    XCTAssertNil(stored())
+
+    stored.set("test")
+
+    XCTAssertEqual(stored(), "test")
+
+    stored.set(nil)
+
+    XCTAssertNil(stored())
+  }
+}