diff --git a/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
new file mode 100644
index 0000000000000000000000000000000000000000..39048a37f158036f2f7b346d2c18cd201807afaf
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functors/MessengerWaitForNodes.swift
@@ -0,0 +1,57 @@
+import XXClient
+import XCTestDynamicOverlay
+
+public struct MessengerWaitForNodes {
+  public typealias Progress = (Double) -> 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.ctx.getCMix() else {
+        throw Error.notLoaded
+      }
+
+      func getProgress(_ report: NodeRegistrationReport) -> Double {
+        min(1, ((report.ratio / targetRatio) * 100).rounded() / 100)
+      }
+
+      var report = try cMix.getNodeRegistrationStatus()
+      var retries = retries
+      onProgress(getProgress(report))
+
+      while report.ratio < targetRatio && retries > 0 {
+        env.sleep(sleepMS)
+        retries -= 1
+        report = try cMix.getNodeRegistrationStatus()
+        onProgress(getProgress(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
index 959aff7ffec86419698f34b9512c5939a31be1ad..5a442ac9aa17220dcaa1f2b166c74a51be641c2b 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -16,6 +16,7 @@ public struct Messenger {
   public var isLoggedIn: MessengerIsLoggedIn
   public var logIn: MessengerLogIn
   public var waitForNetwork: MessengerWaitForNetwork
+  public var waitForNodes: MessengerWaitForNodes
 }
 
 extension Messenger {
@@ -35,7 +36,8 @@ extension Messenger {
       register: .live(env),
       isLoggedIn: .live(env),
       logIn: .live(env),
-      waitForNetwork: .live(env)
+      waitForNetwork: .live(env),
+      waitForNodes: .live(env)
     )
   }
 }
@@ -56,6 +58,7 @@ extension Messenger {
     register: .unimplemented,
     isLoggedIn: .unimplemented,
     logIn: .unimplemented,
-    waitForNetwork: .unimplemented
+    waitForNetwork: .unimplemented,
+    waitForNodes: .unimplemented
   )
 }
diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
index b3009ae051ab0ea89c0e8e3c07ae81a4ece8e283..eb7646f3ecc8f9c0996ddc19fda93ef87fc8efe8 100644
--- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
+++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift
@@ -16,6 +16,7 @@ public struct MessengerEnvironment {
   public var newCMix: NewCMix
   public var newOrLoadUd: NewOrLoadUd
   public var passwordStorage: PasswordStorage
+  public var sleep: (Int) -> Void
   public var storageDir: String
   public var udAddress: String?
   public var udCert: Data?
@@ -44,6 +45,7 @@ extension MessengerEnvironment {
       newCMix: .live,
       newOrLoadUd: .live,
       passwordStorage: .keychain,
+      sleep: { Foundation.sleep(UInt32($0)) },
       storageDir: MessengerEnvironment.defaultStorageDir,
       udAddress: nil,
       udCert: nil,
@@ -67,6 +69,7 @@ extension MessengerEnvironment {
     newCMix: .unimplemented,
     newOrLoadUd: .unimplemented,
     passwordStorage: .unimplemented,
+    sleep: XCTUnimplemented("\(Self.self).sleep"),
     storageDir: "unimplemented",
     udAddress: nil,
     udCert: nil,
diff --git a/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3154758f2ee12e766a12ccf5156b63e728262497
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functors/MessengerWaitForNodesTests.swift
@@ -0,0 +1,108 @@
+import CustomDump
+import XCTest
+import XXClient
+@testable import XXMessengerClient
+
+final class MessengerWaitForNodesTests: XCTestCase {
+  func testWaitWhenNotLoaded() {
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = { nil }
+    let waitForNodes: MessengerWaitForNodes = .live(env)
+
+    XCTAssertThrowsError(try waitForNodes()) { error in
+      XCTAssertEqual(
+        error as? MessengerWaitForNodes.Error,
+        MessengerWaitForNodes.Error.notLoaded
+      )
+    }
+  }
+
+  func testWaitWhenHasTargetRatio() throws {
+    var didProgress: [Double] = []
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = {
+      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, [1])
+  }
+
+  func testWaitForTargetRatio() throws {
+    var didSleep: [Int] = []
+    var didProgress: [Double] = []
+
+    var reports: [NodeRegistrationReport] = [
+      .init(registered: 0, total: 10),
+      .init(registered: 3, total: 10),
+      .init(registered: 8, total: 10),
+    ]
+
+    var env: MessengerEnvironment = .unimplemented
+    env.ctx.getCMix = {
+      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, [0, 0.43, 1])
+  }
+
+  func testWaitTimeout() {
+    var didSleep: [Int] = []
+    var didProgress: [Double] = []
+
+    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.ctx.getCMix = {
+      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, [0, 0.43, 0.71, 0.86])
+  }
+}