diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..a2c612b798714cdc30572dad61ce6b5615e7f374
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1410"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "GroupFeature"
+               BuildableName = "GroupFeature"
+               BlueprintName = "GroupFeature"
+               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 = "GroupFeatureTests"
+               BuildableName = "GroupFeatureTests"
+               BlueprintName = "GroupFeatureTests"
+               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 = "GroupFeature"
+            BuildableName = "GroupFeature"
+            BlueprintName = "GroupFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupsFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupsFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..65b9cc334358a6bcfd2a06176d7d9da743bfea75
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/GroupsFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1410"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "GroupsFeature"
+               BuildableName = "GroupsFeature"
+               BlueprintName = "GroupsFeature"
+               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 = "GroupsFeatureTests"
+               BuildableName = "GroupsFeatureTests"
+               BlueprintName = "GroupsFeatureTests"
+               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 = "GroupsFeature"
+            BuildableName = "GroupsFeature"
+            BlueprintName = "GroupsFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/NewGroupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/NewGroupFeature.xcscheme
new file mode 100644
index 0000000000000000000000000000000000000000..8a4e4405667c2a1dd517c6d8a3468e2c21a5dc3b
--- /dev/null
+++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/NewGroupFeature.xcscheme
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1410"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "NewGroupFeature"
+               BuildableName = "NewGroupFeature"
+               BlueprintName = "NewGroupFeature"
+               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 = "NewGroupFeatureTests"
+               BuildableName = "NewGroupFeatureTests"
+               BlueprintName = "NewGroupFeatureTests"
+               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 = "NewGroupFeature"
+            BuildableName = "NewGroupFeature"
+            BlueprintName = "NewGroupFeature"
+            ReferencedContainer = "container:">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index e49750ca16702861be964a5991472e4ff186b9fb..be183c099b55f8840c04f0470ab00cf3fd37ffd4 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -22,8 +22,11 @@ let package = Package(
     .library(name: "ContactFeature", targets: ["ContactFeature"]),
     .library(name: "ContactLookupFeature", targets: ["ContactLookupFeature"]),
     .library(name: "ContactsFeature", targets: ["ContactsFeature"]),
+    .library(name: "GroupFeature", targets: ["GroupFeature"]),
+    .library(name: "GroupsFeature", targets: ["GroupsFeature"]),
     .library(name: "HomeFeature", targets: ["HomeFeature"]),
     .library(name: "MyContactFeature", targets: ["MyContactFeature"]),
+    .library(name: "NewGroupFeature", targets: ["NewGroupFeature"]),
     .library(name: "RegisterFeature", targets: ["RegisterFeature"]),
     .library(name: "ResetAuthFeature", targets: ["ResetAuthFeature"]),
     .library(name: "RestoreFeature", targets: ["RestoreFeature"]),
@@ -38,7 +41,7 @@ let package = Package(
     ),
     .package(
       url: "https://github.com/pointfreeco/swift-composable-architecture.git",
-      .upToNextMajor(from: "0.43.0")
+      .upToNextMajor(from: "0.47.2")
     ),
     .package(
       url: "https://git.xx.network/elixxir/client-ios-db.git",
@@ -46,15 +49,15 @@ let package = Package(
     ),
     .package(
       url: "https://github.com/darrarski/swift-composable-presentation.git",
-      .upToNextMajor(from: "0.6.0")
+      .upToNextMajor(from: "0.6.1")
     ),
     .package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
-      .upToNextMajor(from: "0.5.0")
+      .upToNextMajor(from: "0.6.0")
     ),
     .package(
       url: "https://github.com/pointfreeco/swift-custom-dump.git",
-      .upToNextMajor(from: "0.6.0")
+      .upToNextMajor(from: "0.6.1")
     ),
     .package(
       url: "https://github.com/apple/swift-log.git",
@@ -260,12 +263,56 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "GroupFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .target(name: "ChatFeature"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "GroupFeatureTests",
+      dependencies: [
+        .target(name: "GroupFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .target(
+      name: "GroupsFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .target(name: "GroupFeature"),
+        .target(name: "NewGroupFeature"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "GroupsFeatureTests",
+      dependencies: [
+        .target(name: "GroupsFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "HomeFeature",
       dependencies: [
         .target(name: "AppCore"),
         .target(name: "BackupFeature"),
         .target(name: "ContactsFeature"),
+        .target(name: "GroupsFeature"),
         .target(name: "RegisterFeature"),
         .target(name: "UserSearchFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
@@ -302,6 +349,25 @@ let package = Package(
       ],
       swiftSettings: swiftSettings
     ),
+    .target(
+      name: "NewGroupFeature",
+      dependencies: [
+        .target(name: "AppCore"),
+        .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
+        .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"),
+        .product(name: "XXModels", package: "client-ios-db"),
+      ],
+      swiftSettings: swiftSettings
+    ),
+    .testTarget(
+      name: "NewGroupFeatureTests",
+      dependencies: [
+        .target(name: "NewGroupFeature"),
+        .product(name: "CustomDump", package: "swift-custom-dump"),
+      ],
+      swiftSettings: swiftSettings
+    ),
     .target(
       name: "RegisterFeature",
       dependencies: [
diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj
index 3aa853ea0de719e4ec0c0602968dd637f7214a95..79aef5cb6f3b4768082dd1f6d64ad3131d79e024 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj
@@ -165,7 +165,7 @@
 				CLANG_WARN_UNREACHABLE_CODE = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 7;
 				DEBUG_INFORMATION_FORMAT = dwarf;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
@@ -228,7 +228,7 @@
 				CLANG_WARN_UNREACHABLE_CODE = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 7;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
index aef30c316ef7aa40b1b1c36e37057d4284222bf4..5cd339fe12651b68629b2b533f8add948a6211a6 100644
--- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
+++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme
@@ -119,6 +119,26 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "GroupFeatureTests"
+               BuildableName = "GroupFeatureTests"
+               BlueprintName = "GroupFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "GroupsFeatureTests"
+               BuildableName = "GroupsFeatureTests"
+               BlueprintName = "GroupsFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
@@ -139,6 +159,16 @@
                ReferencedContainer = "container:..">
             </BuildableReference>
          </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "NewGroupFeatureTests"
+               BuildableName = "NewGroupFeatureTests"
+               BlueprintName = "NewGroupFeatureTests"
+               ReferencedContainer = "container:..">
+            </BuildableReference>
+         </TestableReference>
          <TestableReference
             skipped = "NO">
             <BuildableReference
diff --git a/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
index 90077f25563bbc1e91634ee579403863d8f0b4bd..9e55ac6da53b79b6c43352a78f58c78e4020551d 100644
--- a/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
+++ b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
@@ -12,11 +12,14 @@ public struct AppDependencies {
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var now: () -> Date
   public var sendMessage: SendMessage
+  public var sendGroupMessage: SendGroupMessage
   public var sendImage: SendImage
   public var messageListener: MessageListenerHandler
   public var receiveFileHandler: ReceiveFileHandler
   public var log: Logger
   public var loadData: URLDataLoader
+  public var groupRequestHandler: GroupRequestHandler
+  public var groupMessageHandler: GroupMessageHandler
 }
 
 extension AppDependencies {
@@ -44,6 +47,11 @@ extension AppDependencies {
         db: dbManager.getDB,
         now: now
       ),
+      sendGroupMessage: .live(
+        messenger: messenger,
+        db: dbManager.getDB,
+        now: now
+      ),
       sendImage: .live(
         messenger: messenger,
         db: dbManager.getDB,
@@ -59,7 +67,15 @@ extension AppDependencies {
         now: now
       ),
       log: .live(),
-      loadData: .live
+      loadData: .live,
+      groupRequestHandler: .live(
+        messenger: messenger,
+        db: dbManager.getDB
+      ),
+      groupMessageHandler: .live(
+        messenger: messenger,
+        db: dbManager.getDB
+      )
     )
   }
 
@@ -75,11 +91,14 @@ extension AppDependencies {
       placeholder: Date(timeIntervalSince1970: 0)
     ),
     sendMessage: .unimplemented,
+    sendGroupMessage: .unimplemented,
     sendImage: .unimplemented,
     messageListener: .unimplemented,
     receiveFileHandler: .unimplemented,
     log: .unimplemented,
-    loadData: .unimplemented
+    loadData: .unimplemented,
+    groupRequestHandler: .unimplemented,
+    groupMessageHandler: .unimplemented
   )
 }
 
diff --git a/Examples/xx-messenger/Sources/AppCore/Groups/GroupMessageHandler.swift b/Examples/xx-messenger/Sources/AppCore/Groups/GroupMessageHandler.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c0316957f317d3409c990baaf585f6ba516e8ab2
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/Groups/GroupMessageHandler.swift
@@ -0,0 +1,55 @@
+import XXModels
+import XXClient
+import Foundation
+import XXMessengerClient
+import XCTestDynamicOverlay
+
+public struct GroupMessageHandler {
+  public typealias OnError = (Error) -> Void
+
+  public var run: (@escaping OnError) -> Cancellable
+
+  public func callAsFunction(onError: @escaping OnError) -> Cancellable {
+    run(onError)
+  }
+}
+
+extension GroupMessageHandler {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB
+  ) -> GroupMessageHandler {
+    GroupMessageHandler { onError in
+      messenger.registerGroupChatProcessor(.init { result in
+        switch result {
+        case .success(let callback):
+          do {
+            let payload = try MessagePayload.decode(callback.decryptedMessage.payload)
+            try db().saveMessage(.init(
+              networkId: callback.decryptedMessage.messageId,
+              senderId: callback.decryptedMessage.senderId,
+              recipientId: nil,
+              groupId: callback.decryptedMessage.groupId,
+              date: Date(timeIntervalSince1970: TimeInterval(callback.decryptedMessage.timestamp) / 1_000_000_000),
+              status: .received,
+              isUnread: true,
+              text: payload.text,
+              replyMessageId: payload.replyingTo,
+              roundURL: callback.roundUrl
+            ))
+          } catch {
+            onError(error)
+          }
+        case .failure(let error):
+          onError(error)
+        }
+      })
+    }
+  }
+}
+
+extension GroupMessageHandler {
+  public static let unimplemented = GroupMessageHandler(
+    run: XCTestDynamicOverlay.unimplemented("\(Self.self)", placeholder: Cancellable {})
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/Groups/GroupRequestHandler.swift b/Examples/xx-messenger/Sources/AppCore/Groups/GroupRequestHandler.swift
new file mode 100644
index 0000000000000000000000000000000000000000..7a5be6c5ad9ae943df916c7730b57866709b079b
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/Groups/GroupRequestHandler.swift
@@ -0,0 +1,99 @@
+import XXModels
+import XXClient
+import Foundation
+import XXMessengerClient
+import XCTestDynamicOverlay
+
+public struct GroupRequestHandler {
+  public typealias OnError = (Error) -> Void
+
+  public var run: (@escaping OnError) -> Cancellable
+
+  public func callAsFunction(onError: @escaping OnError) -> Cancellable {
+    run(onError)
+  }
+}
+
+extension GroupRequestHandler {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB
+  ) -> GroupRequestHandler {
+    GroupRequestHandler { onError in
+      messenger.registerGroupRequestHandler(.init { group in
+        do {
+          if let _ = try db().fetchGroups(.init(id: [group.getId()])).first {
+            return
+          }
+          guard let leader = try group.getMembership().first else {
+            return // Failed to get group membership/leader
+          }
+          try db().saveGroup(.init(
+            id: group.getId(),
+            name: String(data: group.getName(), encoding: .utf8)!,
+            leaderId: leader.id,
+            createdAt: Date(timeIntervalSince1970: TimeInterval(group.getCreatedMS()) / 1_000),
+            authStatus: .pending,
+            serialized: group.serialize()
+          ))
+          if let initialMessageData = group.getInitMessage(),
+             let initialMessage = String(data: initialMessageData, encoding: .utf8) {
+            try db().saveMessage(.init(
+              senderId: leader.id,
+              recipientId: nil,
+              groupId: group.getId(),
+              date: Date(timeIntervalSince1970: TimeInterval(group.getCreatedMS()) / 1_000),
+              status: .received,
+              isUnread: true,
+              text: initialMessage
+            ))
+          }
+          let members = try group.getMembership()
+          let friends = try db().fetchContacts(.init(id: Set(members.map(\.id)), authStatus: [
+            .friend, .hidden, .confirming,
+            .verified, .requested, .requesting,
+            .verificationInProgress, .requestFailed,
+            .verificationFailed, .confirmationFailed
+          ]))
+          let strangers = Set(members.map(\.id)).subtracting(Set(friends.map(\.id)))
+          try strangers.forEach {
+            if let stranger = try? db().fetchContacts(.init(id: [$0])).first {
+              print(stranger)
+            } else {
+              try db().saveContact(.init(
+                id: $0,
+                username: "Fetching...",
+                authStatus: .stranger,
+                isRecent: false,
+                isBlocked: false,
+                isBanned: false,
+                createdAt: Date(timeIntervalSince1970: TimeInterval(group.getCreatedMS()) / 1_000)
+              ))
+            }
+          }
+          try members.map {
+            XXModels.GroupMember(groupId: group.getId(), contactId: $0.id)
+          }.forEach {
+            try db().saveGroupMember($0)
+          }
+          let lookupResult = try messenger.lookupContacts(ids: strangers.map { $0 })
+          for user in lookupResult.contacts {
+            if var foo = try? db().fetchContacts(.init(id: [user.getId()])).first,
+               let username = try? user.getFact(.username)?.value {
+              foo.username = username
+              _ = try? db().saveContact(foo)
+            }
+          }
+        } catch {
+          onError(error)
+        }
+      })
+    }
+  }
+}
+
+extension GroupRequestHandler {
+  public static let unimplemented = GroupRequestHandler(
+    run: XCTestDynamicOverlay.unimplemented("\(Self.self)", placeholder: Cancellable {})
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/Models/MessagePayload.swift b/Examples/xx-messenger/Sources/AppCore/Models/MessagePayload.swift
index 67fb94d905b06ad25816ca8c4d11081c72053f19..7ae774337944fdb62b7c5b862cfdd28d7a3a3295 100644
--- a/Examples/xx-messenger/Sources/AppCore/Models/MessagePayload.swift
+++ b/Examples/xx-messenger/Sources/AppCore/Models/MessagePayload.swift
@@ -1,16 +1,22 @@
 import Foundation
 
 public struct MessagePayload: Equatable {
-  public init(text: String) {
+  public init(
+    text: String,
+    replyingTo: Data? = nil
+  ) {
     self.text = text
+    self.replyingTo = replyingTo
   }
 
   public var text: String
+  public var replyingTo: Data?
 }
 
 extension MessagePayload: Codable {
   enum CodingKeys: String, CodingKey {
     case text
+    case replyingTo
   }
 
   public static func decode(_ data: Data) throws -> Self {
diff --git a/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9427053788da3f869e9dd6b0845d581c7e8905c2
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
@@ -0,0 +1,84 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct SendGroupMessage {
+  public typealias OnError = (Error) -> Void
+  public typealias Completion = () -> Void
+
+  public var run: (String, Data, @escaping OnError, @escaping Completion) -> Void
+
+  public func callAsFunction(
+    text: String,
+    to groupId: Data,
+    onError: @escaping OnError,
+    completion: @escaping Completion
+  ) {
+    run(text, groupId, onError, completion)
+  }
+}
+
+extension SendGroupMessage {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    now: @escaping () -> Date
+  ) -> SendGroupMessage {
+    SendGroupMessage { text, groupId, onError, completion in
+      do {
+        let chat = try messenger.groupChat.tryGet()
+        let myContactId = try messenger.e2e.tryGet().getContact().getId()
+        var message = try db().saveMessage(.init(
+          senderId: myContactId,
+          recipientId: nil,
+          groupId: groupId,
+          date: now(),
+          status: .sending,
+          isUnread: false,
+          text: text
+        ))
+        let payload = MessagePayload(text: message.text)
+        let report = try chat.send(
+          groupId: groupId,
+          message: try payload.encode()
+        )
+        message.networkId = report.messageId
+        message.roundURL = report.roundURL
+        message = try db().saveMessage(message)
+        try messenger.cMix.tryGet().waitForRoundResult(
+          roundList: try report.encode(),
+          timeoutMS: 30_000,
+          callback: .init { result in
+            let status: XXModels.Message.Status
+            switch result {
+            case .delivered(_):
+              status = .sent
+            case .notDelivered(let timedOut):
+              status = timedOut ? .sendingTimedOut : .sendingFailed
+            }
+            do {
+              try db().bulkUpdateMessages(
+                .init(id: [message.id]),
+                .init(status: status)
+              )
+            } catch {
+              onError(error)
+            }
+            completion()
+          }
+        )
+      } catch {
+        onError(error)
+        completion()
+      }
+    }
+  }
+}
+
+extension SendGroupMessage {
+  public static let unimplemented = SendGroupMessage(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/GroupAuthStatusView.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/GroupAuthStatusView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..bddb9314099396b612fae595309affc0d8840297
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/GroupAuthStatusView.swift
@@ -0,0 +1,57 @@
+import SwiftUI
+import XXModels
+
+public struct GroupAuthStatusView: View {
+  public init(_ authStatus: XXModels.Group.AuthStatus) {
+    self.authStatus = authStatus
+  }
+
+  public var authStatus: XXModels.Group.AuthStatus
+
+  public var body: some View {
+    switch authStatus {
+    case .pending:
+      HStack {
+        Text("Pending")
+        Spacer()
+        Image(systemName: "envelope.badge")
+      }
+
+    case .deleting:
+      HStack {
+        Text("Deleting")
+        Spacer()
+        ProgressView()
+      }
+
+    case .participating:
+      HStack {
+        Text("Participating")
+        Spacer()
+        Image(systemName: "checkmark")
+      }
+
+    case .hidden:
+      HStack {
+        Text("Hidden")
+        Spacer()
+        Image(systemName: "eye.slash")
+      }
+    }
+  }
+}
+
+#if DEBUG
+struct GroupAuthStatusView_Previews: PreviewProvider {
+  static var previews: some View {
+    NavigationView {
+      Form {
+        Section { GroupAuthStatusView(.pending) }
+        Section { GroupAuthStatusView(.deleting) }
+        Section { GroupAuthStatusView(.participating) }
+        Section { GroupAuthStatusView(.hidden) }
+      }
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift b/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift
index d092a69e57ac781372297df8de4cdcce02a43d36..d0d37d658c208c4694c68caa0fe8be98871c4858 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppComponent.swift
@@ -41,6 +41,8 @@ struct AppComponent: ReducerProtocol {
   @Dependency(\.app.log) var log: Logger
   @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
   @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
+  @Dependency(\.app.groupRequestHandler) var groupRequestHandler: GroupRequestHandler
+  @Dependency(\.app.groupMessageHandler) var groupMessageHandler: GroupMessageHandler
 
   var body: some ReducerProtocol<State, Action> {
     BindingReducer()
@@ -80,7 +82,12 @@ struct AppComponent: ReducerProtocol {
             cancellables.append(receiveFileHandler(onError: { error in
               log(.error(error as NSError))
             }))
-
+            cancellables.append(groupRequestHandler(onError: { error in
+              log(.error(error as NSError))
+            }))
+            cancellables.append(groupMessageHandler(onError: { error in
+              log(.error(error as NSError))
+            }))
             cancellables.append(messenger.registerBackupCallback(.init { data in
               try? backupStorage.store(data)
             }))
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
index 6c71789dbbb201122cfda709999ac36984363b55..def3f14cdbae265ed27077c7e88a96416b924a49 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
@@ -10,7 +10,8 @@ import XXModels
 public struct ChatComponent: ReducerProtocol {
   public struct State: Equatable, Identifiable {
     public enum ID: Equatable, Hashable {
-      case contact(Data)
+      case contact(XXModels.Contact.ID)
+      case group(XXModels.Group.ID)
     }
 
     public struct Message: Equatable, Identifiable {
@@ -18,6 +19,7 @@ public struct ChatComponent: ReducerProtocol {
         id: Int64,
         date: Date,
         senderId: Data,
+        senderName: String?,
         text: String,
         status: XXModels.Message.Status,
         fileTransfer: XXModels.FileTransfer? = nil
@@ -25,6 +27,7 @@ public struct ChatComponent: ReducerProtocol {
         self.id = id
         self.date = date
         self.senderId = senderId
+        self.senderName = senderName
         self.text = text
         self.status = status
         self.fileTransfer = fileTransfer
@@ -33,6 +36,7 @@ public struct ChatComponent: ReducerProtocol {
       public var id: Int64
       public var date: Date
       public var senderId: Data
+      public var senderName: String?
       public var text: String
       public var status: XXModels.Message.Status
       public var fileTransfer: XXModels.FileTransfer?
@@ -77,6 +81,7 @@ public struct ChatComponent: ReducerProtocol {
   @Dependency(\.app.messenger) var messenger: Messenger
   @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
   @Dependency(\.app.sendMessage) var sendMessage: SendMessage
+  @Dependency(\.app.sendGroupMessage) var sendGroupMessage: SendGroupMessage
   @Dependency(\.app.sendImage) var sendImage: SendImage
   @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
   @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
@@ -93,37 +98,46 @@ public struct ChatComponent: ReducerProtocol {
           let myContactId = try messenger.e2e.tryGet().getContact().getId()
           state.myContactId = myContactId
           let queryChat: XXModels.Message.Query.Chat
-          let receivedFileTransfersQuery: XXModels.FileTransfer.Query
-          let sentFileTransfersQuery: XXModels.FileTransfer.Query
+          let receivedFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
+          let sentFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
           switch state.id {
           case .contact(let contactId):
             queryChat = .direct(myContactId, contactId)
-            receivedFileTransfersQuery = .init(
+            receivedFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
               contactId: contactId,
               isIncoming: true
-            )
-            sentFileTransfersQuery = .init(
+            ))
+            sentFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
               contactId: myContactId,
               isIncoming: false
-            )
+            ))
+          case .group(let groupId):
+            queryChat = .group(groupId)
+            receivedFileTransfersPublisher = Just([])
+              .setFailureType(to: Error.self)
+              .eraseToAnyPublisher()
+            sentFileTransfersPublisher = Just([])
+              .setFailureType(to: Error.self)
+              .eraseToAnyPublisher()
           }
           let messagesQuery = XXModels.Message.Query(chat: queryChat)
           return Publishers.CombineLatest3(
             try db().fetchMessagesPublisher(messagesQuery),
-            try db().fetchFileTransfersPublisher(receivedFileTransfersQuery),
-            try db().fetchFileTransfersPublisher(sentFileTransfersQuery)
+            try db().fetchContactsPublisher(.init()),
+            Publishers.CombineLatest(
+              receivedFileTransfersPublisher,
+              sentFileTransfersPublisher
+            ).map(+)
           )
-          .map { messages, receivedFileTransfers, sentFileTransfers in
-            (messages, receivedFileTransfers + sentFileTransfers)
-          }
           .assertNoFailure()
-          .map { messages, fileTransfers in
-            messages.compactMap { message in
+          .map { messages, contacts, fileTransfers -> [State.Message] in
+            messages.compactMap { message -> State.Message? in
               guard let id = message.id else { return nil }
               return State.Message(
                 id: id,
                 date: message.date,
                 senderId: message.senderId,
+                senderName: contacts.first { $0.id == message.senderId }?.username,
                 text: message.text,
                 status: message.status,
                 fileTransfer: fileTransfers.first { $0.id == message.fileTransferId }
@@ -163,6 +177,17 @@ public struct ChatComponent: ReducerProtocol {
                 subscriber.send(completion: .finished)
               }
             )
+          case .group(let groupId):
+            sendGroupMessage(
+              text: text,
+              to: groupId,
+              onError: { error in
+                subscriber.send(.sendFailed(error.localizedDescription))
+              },
+              completion: {
+                subscriber.send(completion: .finished)
+              }
+            )
           }
           return AnyCancellable {}
         }
@@ -175,21 +200,18 @@ public struct ChatComponent: ReducerProtocol {
         return .none
 
       case .imagePicked(let data):
-        let chatId = state.id
+        guard case .contact(let recipientId) = state.id else { return .none }
         return Effect.run { subscriber in
-          switch chatId {
-          case .contact(let recipientId):
-            sendImage(
-              data,
-              to: recipientId,
-              onError: { error in
-                subscriber.send(.sendFailed(error.localizedDescription))
-              },
-              completion: {
-                subscriber.send(completion: .finished)
-              }
-            )
-          }
+          sendImage(
+            data,
+            to: recipientId,
+            onError: { error in
+              subscriber.send(.sendFailed(error.localizedDescription))
+            },
+            completion: {
+              subscriber.send(completion: .finished)
+            }
+          )
           return AnyCancellable {}
         }
         .subscribe(on: bgQueue)
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
index 1611815c087786954fd730fe1768760801a7ea73..a7272385a3241f5d46890a88039edefeb7b41d80 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
@@ -16,6 +16,7 @@ public struct ChatView: View {
     var failure: String?
     var sendFailure: String?
     var text: String
+    var disableImagePicker: Bool
 
     init(state: ChatComponent.State) {
       myContactId = state.myContactId
@@ -23,6 +24,12 @@ public struct ChatView: View {
       failure = state.failure
       sendFailure = state.sendFailure
       text = state.text
+      switch state.id {
+      case .contact(_):
+        disableImagePicker = false
+      case .group(_):
+        disableImagePicker = true
+      }
     }
   }
 
@@ -109,6 +116,7 @@ public struct ChatView: View {
                   }
                 }
               }
+              .disabled(viewStore.disableImagePicker)
             }
           }
           .padding()
@@ -139,6 +147,13 @@ public struct ChatView: View {
 
     var body: some View {
       VStack {
+        if let sender = message.senderName {
+          Text(sender)
+            .foregroundColor(.secondary)
+            .font(.footnote)
+            .frame(maxWidth: .infinity, alignment: alignment)
+        }
+
         Text("\(message.date.formatted()), \(statusText)")
           .foregroundColor(.secondary)
           .font(.footnote)
@@ -208,6 +223,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 1,
               date: Date(),
               senderId: "contact-id".data(using: .utf8)!,
+              senderName: "Contact",
               text: "Hello!",
               status: .received
             ),
@@ -215,6 +231,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 2,
               date: Date(),
               senderId: "my-contact-id".data(using: .utf8)!,
+              senderName: "Me",
               text: "Hi!",
               status: .sent
             ),
@@ -222,6 +239,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 3,
               date: Date(),
               senderId: "contact-id".data(using: .utf8)!,
+              senderName: "Contact",
               text: "",
               status: .received,
               fileTransfer: .init(
@@ -237,6 +255,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 4,
               date: Date(),
               senderId: "my-contact-id".data(using: .utf8)!,
+              senderName: "Me",
               text: "",
               status: .sent,
               fileTransfer: .init(
diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift
new file mode 100644
index 0000000000000000000000000000000000000000..359ee8991596e56cac72c39626955366588033b0
--- /dev/null
+++ b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift
@@ -0,0 +1,119 @@
+import AppCore
+import ChatFeature
+import ComposableArchitecture
+import ComposablePresentation
+import Foundation
+import XXMessengerClient
+import XXModels
+
+public struct GroupComponent: ReducerProtocol {
+  public struct State: Equatable {
+    public init(
+      groupId: XXModels.Group.ID,
+      groupInfo: XXModels.GroupInfo? = nil,
+      isJoining: Bool = false,
+      joinFailure: String? = nil,
+      chat: ChatComponent.State? = nil
+    ) {
+      self.groupId = groupId
+      self.groupInfo = groupInfo
+      self.isJoining = isJoining
+      self.joinFailure = joinFailure
+      self.chat = chat
+    }
+
+    public var groupId: XXModels.Group.ID
+    public var groupInfo: XXModels.GroupInfo?
+    public var isJoining: Bool
+    public var joinFailure: String?
+    public var chat: ChatComponent.State?
+  }
+
+  public enum Action: Equatable {
+    case start
+    case didFetchGroupInfo(XXModels.GroupInfo?)
+    case joinButtonTapped
+    case didJoin
+    case didFailToJoin(String)
+    case chatButtonTapped
+    case didDismissChat
+    case chat(ChatComponent.Action)
+  }
+
+  public init() {}
+
+  @Dependency(\.app.messenger) var messenger: Messenger
+  @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
+  @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
+  @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
+
+  public var body: some ReducerProtocol<State, Action> {
+    Reduce { state, action in
+      switch action {
+      case .start:
+        return Effect
+          .catching { try db() }
+          .flatMap { [state] in
+            let query = GroupInfo.Query(groupId: state.groupId)
+            return $0.fetchGroupInfosPublisher(query).map(\.first)
+          }
+          .assertNoFailure()
+          .map(Action.didFetchGroupInfo)
+          .subscribe(on: bgQueue)
+          .receive(on: mainQueue)
+          .eraseToEffect()
+
+      case .didFetchGroupInfo(let groupInfo):
+        state.groupInfo = groupInfo
+        return .none
+
+      case .joinButtonTapped:
+        guard let info = state.groupInfo else { return .none }
+        state.isJoining = true
+        state.joinFailure = nil
+        return Effect.result {
+          do {
+            let groupChat = try messenger.groupChat.tryGet()
+            try groupChat.joinGroup(serializedGroupData: info.group.serialized)
+            var group = info.group
+            group.authStatus = .participating
+            try db().saveGroup(group)
+            return .success(.didJoin)
+          } catch {
+            return .success(.didFailToJoin(error.localizedDescription))
+          }
+        }
+        .subscribe(on: bgQueue)
+        .receive(on: mainQueue)
+        .eraseToEffect()
+
+      case .didJoin:
+        state.isJoining = false
+        state.joinFailure = nil
+        return .none
+
+      case .didFailToJoin(let failure):
+        state.isJoining = false
+        state.joinFailure = failure
+        return .none
+
+      case .chatButtonTapped:
+        state.chat = ChatComponent.State(id: .group(state.groupId))
+        return .none
+
+      case .didDismissChat:
+        state.chat = nil
+        return .none
+
+      case .chat(_):
+        return .none
+      }
+    }
+    .presenting(
+      state: .keyPath(\.chat),
+      id: .notNil(),
+      action: /Action.chat,
+      presented: { ChatComponent() }
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a7520d53802b8fc8a31a306a4305804b94c1cb61
--- /dev/null
+++ b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift
@@ -0,0 +1,137 @@
+import AppCore
+import ChatFeature
+import ComposableArchitecture
+import ComposablePresentation
+import SwiftUI
+import XXModels
+
+public struct GroupView: View {
+  public typealias Component = GroupComponent
+  typealias ViewStore = ComposableArchitecture.ViewStore<ViewState, Component.Action>
+
+  public init(store: StoreOf<Component>) {
+    self.store = store
+  }
+
+  let store: StoreOf<Component>
+
+  struct ViewState: Equatable {
+    init(state: Component.State) {
+      info = state.groupInfo
+      isJoining = state.isJoining
+      joinFailure = state.joinFailure
+    }
+
+    var info: XXModels.GroupInfo?
+    var isJoining: Bool
+    var joinFailure: String?
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+        if let info = viewStore.info {
+          Section("Name") {
+            Text(info.group.name)
+          }
+
+          Section("Leader") {
+            Label(info.leader.username ?? "", systemImage: "person.badge.shield.checkmark")
+          }
+
+          Section("Members") {
+            ForEach(info.members.filter { $0 != info.leader }) { contact in
+              Label(contact.username ?? "", systemImage: "person")
+            }
+          }
+
+          Section("Status") {
+            GroupAuthStatusView(info.group.authStatus)
+
+            if case .pending = info.group.authStatus {
+              Button {
+                viewStore.send(.joinButtonTapped)
+              } label: {
+                HStack {
+                  Text("Join")
+                  Spacer()
+                  if viewStore.isJoining {
+                    ProgressView()
+                  } else {
+                    Image(systemName: "play.fill")
+                  }
+                }
+              }
+              .disabled(viewStore.isJoining)
+            }
+
+            if let failure = viewStore.joinFailure {
+              Text(failure)
+            }
+          }
+        }
+
+        Section {
+          Button {
+            viewStore.send(.chatButtonTapped)
+          } label: {
+            HStack {
+              Text("Chat")
+              Spacer()
+              Image(systemName: "chevron.forward")
+            }
+          }
+        }
+      }
+      .navigationTitle("Group")
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.chat,
+          action: Component.Action.chat
+        ),
+        onDeactivate: { viewStore.send(.didDismissChat) },
+        destination: ChatView.init(store:)
+      ))
+      .task { viewStore.send(.start) }
+    }
+  }
+}
+
+#if DEBUG
+public struct GroupView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      GroupView(store: Store(
+        initialState: GroupComponent.State(
+          groupId: "group-id".data(using: .utf8)!,
+          groupInfo: .init(
+            group: .init(
+              id: "group-id".data(using: .utf8)!,
+              name: "Preview group",
+              leaderId: "group-leader-id".data(using: .utf8)!,
+              createdAt: Date(timeIntervalSince1970: TimeInterval(86_400)),
+              authStatus: .participating,
+              serialized: "group-serialized".data(using: .utf8)!
+            ),
+            leader: .init(
+              id: "group-leader-id".data(using: .utf8)!,
+              username: "Group leader"
+            ),
+            members: [
+              .init(
+                id: "member-1-id".data(using: .utf8)!,
+                username: "Member 1"
+              ),
+              .init(
+                id: "member-2-id".data(using: .utf8)!,
+                username: "Member 2"
+              ),
+            ]
+          )
+        ),
+        reducer: EmptyReducer()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift b/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift
new file mode 100644
index 0000000000000000000000000000000000000000..91e6dc55a8aeacfe8f811fffeabe092c391b6fd0
--- /dev/null
+++ b/Examples/xx-messenger/Sources/GroupsFeature/GroupsComponent.swift
@@ -0,0 +1,97 @@
+import AppCore
+import ComposableArchitecture
+import ComposablePresentation
+import Foundation
+import GroupFeature
+import NewGroupFeature
+import XXModels
+
+public struct GroupsComponent: ReducerProtocol {
+  public struct State: Equatable {
+    public init(
+      groups: IdentifiedArrayOf<Group> = [],
+      newGroup: NewGroupComponent.State? = nil,
+      group: GroupComponent.State? = nil
+    ) {
+      self.groups = groups
+      self.newGroup = newGroup
+      self.group = group
+    }
+
+    public var groups: IdentifiedArrayOf<XXModels.Group> = []
+    public var newGroup: NewGroupComponent.State?
+    public var group: GroupComponent.State?
+  }
+
+  public enum Action: Equatable {
+    case start
+    case didFetchGroups([XXModels.Group])
+    case didSelectGroup(XXModels.Group)
+    case didDismissGroup
+    case newGroupButtonTapped
+    case newGroupDismissed
+    case newGroup(NewGroupComponent.Action)
+    case group(GroupComponent.Action)
+  }
+
+  public init() {}
+
+  @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
+  @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
+  @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
+
+  public var body: some ReducerProtocol<State, Action> {
+    Reduce { state, action in
+      switch action {
+      case .start:
+        return Effect
+          .catching { try db() }
+          .flatMap { $0.fetchGroupsPublisher(.init()) }
+          .assertNoFailure()
+          .map(Action.didFetchGroups)
+          .subscribe(on: bgQueue)
+          .receive(on: mainQueue)
+          .eraseToEffect()
+
+      case .didFetchGroups(let groups):
+        state.groups = IdentifiedArray(uniqueElements: groups)
+        return .none
+
+      case .didSelectGroup(let group):
+        state.group = GroupComponent.State(groupId: group.id)
+        return .none
+
+      case .didDismissGroup:
+        state.group = nil
+        return .none
+
+      case .newGroupButtonTapped:
+        state.newGroup = NewGroupComponent.State()
+        return .none
+
+      case .newGroupDismissed:
+        state.newGroup = nil
+        return .none
+
+      case .newGroup(.didFinish):
+        state.newGroup = nil
+        return .none
+
+      case .newGroup(_), .group(_):
+        return .none
+      }
+    }
+    .presenting(
+      state: .keyPath(\.newGroup),
+      id: .notNil(),
+      action: /Action.newGroup,
+      presented: { NewGroupComponent() }
+    )
+    .presenting(
+      state: .keyPath(\.group),
+      id: .notNil(),
+      action: /Action.group,
+      presented: { GroupComponent() }
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift b/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1391f6d91d2b23b62c0da579c410cd9213e6af54
--- /dev/null
+++ b/Examples/xx-messenger/Sources/GroupsFeature/GroupsView.swift
@@ -0,0 +1,100 @@
+import AppCore
+import ComposableArchitecture
+import ComposablePresentation
+import GroupFeature
+import NewGroupFeature
+import SwiftUI
+import XXModels
+
+public struct GroupsView: View {
+  public typealias Component = GroupsComponent
+  typealias ViewStore = ComposableArchitecture.ViewStore<ViewState, Component.Action>
+
+  public init(store: StoreOf<Component>) {
+    self.store = store
+  }
+
+  let store: StoreOf<Component>
+
+  struct ViewState: Equatable {
+    init(state: Component.State) {
+      groups = state.groups
+    }
+
+    var groups: IdentifiedArrayOf<XXModels.Group>
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+        newGroupButton(viewStore)
+
+        ForEach(viewStore.groups) { group in
+          groupView(group, viewStore)
+        }
+      }
+      .navigationTitle("Groups")
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.newGroup,
+          action: Component.Action.newGroup
+        ),
+        onDeactivate: { viewStore.send(.newGroupDismissed) },
+        destination: NewGroupView.init
+      ))
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.group,
+          action: Component.Action.group
+        ),
+        onDeactivate: { viewStore.send(.didDismissGroup) },
+        destination: GroupView.init
+      ))
+      .task { viewStore.send(.start) }
+    }
+  }
+
+  func newGroupButton(_ viewStore: ViewStore) -> some View {
+    Section {
+      Button {
+        viewStore.send(.newGroupButtonTapped)
+      } label: {
+        HStack {
+          Text("New Group")
+          Spacer()
+          Image(systemName: "chevron.forward")
+        }
+      }
+    }
+  }
+
+  func groupView(_ group: XXModels.Group, _ viewStore: ViewStore) -> some View {
+    Section {
+      Button {
+        viewStore.send(.didSelectGroup(group))
+      } label: {
+        HStack {
+          Label(group.name, systemImage: "person.3")
+            .font(.callout)
+            .tint(Color.primary)
+          Spacer()
+          Image(systemName: "chevron.forward")
+        }
+      }
+      GroupAuthStatusView(group.authStatus)
+    }
+  }
+}
+
+#if DEBUG
+public struct GroupsView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      GroupsView(store: Store(
+        initialState: GroupsComponent.State(),
+        reducer: EmptyReducer()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift
index 18212bf0ca916cffff86f5a4ee6cb09f4d220359..91b0acffa31647524cc91e1d9f2eec028768e064 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeComponent.swift
@@ -5,6 +5,7 @@ import ComposableArchitecture
 import ComposablePresentation
 import ContactsFeature
 import Foundation
+import GroupsFeature
 import RegisterFeature
 import UserSearchFeature
 import XCTestDynamicOverlay
@@ -23,7 +24,8 @@ public struct HomeComponent: ReducerProtocol {
       register: RegisterComponent.State? = nil,
       contacts: ContactsComponent.State? = nil,
       userSearch: UserSearchComponent.State? = nil,
-      backup: BackupComponent.State? = nil
+      backup: BackupComponent.State? = nil,
+      groups: GroupsComponent.State? = nil
     ) {
       self.failure = failure
       self.isNetworkHealthy = isNetworkHealthy
@@ -33,6 +35,7 @@ public struct HomeComponent: ReducerProtocol {
       self.contacts = contacts
       self.userSearch = userSearch
       self.backup = backup
+      self.groups = groups
     }
 
     public var failure: String?
@@ -44,6 +47,7 @@ public struct HomeComponent: ReducerProtocol {
     public var contacts: ContactsComponent.State?
     public var userSearch: UserSearchComponent.State?
     public var backup: BackupComponent.State?
+    public var groups: GroupsComponent.State?
   }
 
   public enum Action: Equatable {
@@ -79,10 +83,13 @@ public struct HomeComponent: ReducerProtocol {
     case didDismissContacts
     case backupButtonTapped
     case didDismissBackup
+    case groupsButtonTapped
+    case didDismissGroups
     case register(RegisterComponent.Action)
     case contacts(ContactsComponent.Action)
     case userSearch(UserSearchComponent.Action)
     case backup(BackupComponent.Action)
+    case groups(GroupsComponent.Action)
   }
 
   public init() {}
@@ -119,6 +126,10 @@ public struct HomeComponent: ReducerProtocol {
                 try messenger.startFileTransfer()
               }
 
+              if messenger.isGroupChatRunning() == false {
+                try messenger.startGroupChat()
+              }
+
               if messenger.isLoggedIn() == false {
                 if try messenger.isRegistered() == false {
                   return .success(.messenger(.didStartUnregistered))
@@ -260,7 +271,15 @@ public struct HomeComponent: ReducerProtocol {
         state.backup = nil
         return .none
 
-      case .register(_), .contacts(_), .userSearch(_), .backup(_):
+      case .groupsButtonTapped:
+        state.groups = GroupsComponent.State()
+        return .none
+
+      case .didDismissGroups:
+        state.groups = nil
+        return .none
+
+      case .register(_), .contacts(_), .userSearch(_), .backup(_), .groups(_):
         return .none
       }
     }
@@ -288,5 +307,11 @@ public struct HomeComponent: ReducerProtocol {
       action: /Action.backup,
       presented: { BackupComponent() }
     )
+    .presenting(
+      state: .keyPath(\.groups),
+      id: .notNil(),
+      action: /Action.groups,
+      presented: { GroupsComponent() }
+    )
   }
 }
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
index f2fb823ce3772601b24e65bf66063bad2304846f..24465f6e6f3b39344d070660b21fdbaef0310a76 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift
@@ -3,6 +3,7 @@ import BackupFeature
 import ComposableArchitecture
 import ComposablePresentation
 import ContactsFeature
+import GroupsFeature
 import RegisterFeature
 import SwiftUI
 import UserSearchFeature
@@ -109,6 +110,16 @@ public struct HomeView: View {
                 Image(systemName: "chevron.forward")
               }
             }
+
+            Button {
+              viewStore.send(.groupsButtonTapped)
+            } label: {
+              HStack {
+                Text("Groups")
+                Spacer()
+                Image(systemName: "chevron.forward")
+              }
+            }
           } header: {
             Text("Contacts")
           }
@@ -181,6 +192,16 @@ public struct HomeView: View {
           },
           destination: BackupView.init(store:)
         ))
+        .background(NavigationLinkWithStore(
+          store.scope(
+            state: \.groups,
+            action: HomeComponent.Action.groups
+          ),
+          onDeactivate: {
+            viewStore.send(.didDismissGroups)
+          },
+          destination: GroupsView.init(store:)
+        ))
       }
       .navigationViewStyle(.stack)
       .task { viewStore.send(.messenger(.start)) }
diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift
new file mode 100644
index 0000000000000000000000000000000000000000..fd5e36c6b25aa5760ecec4214b3630454c6de1c4
--- /dev/null
+++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupComponent.swift
@@ -0,0 +1,149 @@
+import AppCore
+import ComposableArchitecture
+import Foundation
+import XXMessengerClient
+import XXModels
+
+public struct NewGroupComponent: ReducerProtocol {
+  public struct State: Equatable {
+    public enum Field: String, Hashable {
+      case name
+      case message
+    }
+
+    public init(
+      contacts: IdentifiedArrayOf<XXModels.Contact> = [],
+      members: IdentifiedArrayOf<XXModels.Contact> = [],
+      name: String = "",
+      message: String = "",
+      focusedField: Field? = nil,
+      isCreating: Bool = false,
+      failure: String? = nil
+    ) {
+      self.contacts = contacts
+      self.members = members
+      self.name = name
+      self.message = message
+      self.focusedField = focusedField
+      self.isCreating = isCreating
+      self.failure = failure
+    }
+
+    public var contacts: IdentifiedArrayOf<XXModels.Contact>
+    public var members: IdentifiedArrayOf<XXModels.Contact>
+    @BindableState public var name: String
+    @BindableState public var message: String
+    @BindableState public var focusedField: Field?
+    public var isCreating: Bool
+    public var failure: String?
+  }
+
+  public enum Action: Equatable, BindableAction {
+    case start
+    case didFetchContacts([XXModels.Contact])
+    case didSelectContact(XXModels.Contact)
+    case createButtonTapped
+    case didFinish
+    case didFail(String)
+    case binding(BindingAction<State>)
+  }
+
+  public init() {}
+
+  @Dependency(\.app.messenger) var messenger: Messenger
+  @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
+  @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
+  @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
+  @Dependency(\.date) var date: DateGenerator
+
+  public var body: some ReducerProtocol<State, Action> {
+    BindingReducer()
+    Reduce { state, action in
+      switch action {
+      case .start:
+        let myId = try? messenger.e2e.tryGet().getContact().getId()
+        return Effect
+          .catching { try db() }
+          .flatMap { $0.fetchContactsPublisher(.init()) }
+          .assertNoFailure()
+          .map { $0.filter { $0.id != myId } }
+          .map(Action.didFetchContacts)
+          .subscribe(on: bgQueue)
+          .receive(on: mainQueue)
+          .eraseToEffect()
+
+      case .didFetchContacts(let contacts):
+        state.contacts = IdentifiedArray(uniqueElements: contacts)
+        return .none
+
+      case .didSelectContact(let contact):
+        if state.members.contains(contact) {
+          state.members.remove(contact)
+        } else {
+          state.members.append(contact)
+        }
+        return .none
+
+      case .createButtonTapped:
+        state.focusedField = nil
+        state.isCreating = true
+        state.failure = nil
+        return Effect.result { [state] in
+          do {
+            let groupChat = try messenger.groupChat.tryGet()
+            let report = try groupChat.makeGroup(
+              membership: state.members.map(\.id),
+              message: state.message.data(using: .utf8)!,
+              name: state.name.data(using: .utf8)!
+            )
+            let myContactId = try messenger.e2e.tryGet().getContact().getId()
+            let group = XXModels.Group(
+              id: report.id,
+              name: state.name,
+              leaderId: myContactId,
+              createdAt: date(),
+              authStatus: .participating,
+              serialized: try report.encode()
+            )
+            try db().saveGroup(group)
+            if state.message.isEmpty == false {
+              try db().saveMessage(.init(
+                senderId: myContactId,
+                recipientId: nil,
+                groupId: group.id,
+                date: group.createdAt,
+                status: .sent,
+                isUnread: false,
+                text: state.message
+              ))
+            }
+            try state.members.map {
+              GroupMember(groupId: group.id, contactId: $0.id)
+            }.forEach {
+              try db().saveGroupMember($0)
+            }
+            return .success(.didFinish)
+          } catch {
+            return .success(.didFail(error.localizedDescription))
+          }
+        }
+        .subscribe(on: bgQueue)
+        .receive(on: mainQueue)
+        .eraseToEffect()
+
+      case .didFinish:
+        state.isCreating = false
+        state.failure = nil
+        return .none
+
+      case .didFail(let failure):
+        state.isCreating = false
+        state.failure = failure
+        return .none
+
+      case .binding(_):
+        return .none
+      }
+    }
+  }
+}
diff --git a/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift
new file mode 100644
index 0000000000000000000000000000000000000000..2536dc120bfd4ed462fead26661e06c40f5c790f
--- /dev/null
+++ b/Examples/xx-messenger/Sources/NewGroupFeature/NewGroupView.swift
@@ -0,0 +1,127 @@
+import AppCore
+import ComposableArchitecture
+import SwiftUI
+import XXModels
+
+public struct NewGroupView: View {
+  public typealias Component = NewGroupComponent
+  typealias ViewStore = ComposableArchitecture.ViewStore<ViewState, Component.Action>
+
+  public init(store: StoreOf<Component>) {
+    self.store = store
+  }
+
+  let store: StoreOf<Component>
+  @FocusState var focusedField: Component.State.Field?
+
+  struct ViewState: Equatable {
+    init(state: Component.State) {
+      contacts = state.contacts
+      members = state.members
+      name = state.name
+      message = state.message
+      focusedField = state.focusedField
+      isCreating = state.isCreating
+      failure = state.failure
+    }
+
+    var contacts: IdentifiedArrayOf<XXModels.Contact>
+    var members: IdentifiedArrayOf<XXModels.Contact>
+    var name: String
+    var message: String
+    var focusedField: Component.State.Field?
+    var isCreating: Bool
+    var failure: String?
+  }
+
+  public var body: some View {
+    WithViewStore(store, observe: ViewState.init) { viewStore in
+      Form {
+        Section {
+          membersView(viewStore)
+          nameView(viewStore)
+          messageView(viewStore)
+        }
+        Section {
+          createButton(viewStore)
+          if let failure = viewStore.failure {
+            Text(failure)
+          }
+        }
+      }
+      .navigationTitle("New Group")
+      .task { viewStore.send(.start) }
+      .onChange(of: viewStore.focusedField) { focusedField = $0 }
+      .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
+    }
+  }
+
+  func membersView(_ viewStore: ViewStore) -> some View {
+    NavigationLink("Members (\(viewStore.members.count))") {
+      Form {
+        ForEach(viewStore.contacts) { contact in
+          Button {
+            viewStore.send(.didSelectContact(contact))
+          } label: {
+            HStack {
+              Text(contact.username ?? "")
+              Spacer()
+              if viewStore.members.contains(contact) {
+                Image(systemName: "checkmark")
+              }
+            }
+          }
+        }
+      }
+    }
+    .disabled(viewStore.isCreating)
+  }
+
+  func nameView(_ viewStore: ViewStore) -> some View {
+    TextField("Group name", text: viewStore.binding(
+      get: \.name,
+      send: { .set(\.$name, $0) }
+    ))
+    .focused($focusedField, equals: .name)
+    .disabled(viewStore.isCreating)
+  }
+
+  func messageView(_ viewStore: ViewStore) -> some View {
+    TextField("Initial message", text: viewStore.binding(
+      get: \.message,
+      send: { .set(\.$message, $0) }
+    ))
+    .focused($focusedField, equals: .message)
+    .disabled(viewStore.isCreating)
+  }
+
+  func createButton(_ viewStore: ViewStore) -> some View {
+    Button {
+      viewStore.send(.createButtonTapped)
+    } label: {
+      HStack {
+        Text("Create group")
+        Spacer()
+        if viewStore.isCreating {
+          ProgressView()
+        } else {
+          Image(systemName: "play.fill")
+        }
+      }
+    }
+    .disabled(viewStore.isCreating)
+  }
+}
+
+#if DEBUG
+public struct NewGroupView_Previews: PreviewProvider {
+  public static var previews: some View {
+    NavigationView {
+      NewGroupView(store: Store(
+        initialState: NewGroupComponent.State(),
+        reducer: EmptyReducer()
+      ))
+    }
+  }
+}
+#endif
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1df2eaa3717344509c590a462036218c0ff416b0
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
@@ -0,0 +1,279 @@
+import CustomDump
+import XCTest
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import AppCore
+
+final class SendGroupMessageTests: XCTestCase {
+  enum Action: Equatable {
+    case didReceiveError(String)
+    case didComplete
+    case didSaveMessage(XXModels.Message)
+    case didSend(groupId: Data, message: Data, tag: String?)
+    case didWaitForRoundResults(roundList: Data, timeoutMS: Int)
+    case didUpdateMessage(
+      query: XXModels.Message.Query,
+      assignments: XXModels.Message.Assignments
+    )
+  }
+
+  var actions: [Action]!
+
+  override func setUp() {
+    actions = []
+  }
+
+  override func tearDown() {
+    actions = nil
+  }
+
+  func testSend() {
+    let text = "Hello!"
+    let groupId = "group-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let messageId: Int64 = 321
+    let sendReport = GroupSendReport(
+      rounds: [],
+      roundURL: "round-url",
+      timestamp: 1234,
+      messageId: "message-id".data(using: .utf8)!
+    )
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { groupId, message, tag in
+        self.actions.append(.didSend(groupId: groupId, message: message, tag: tag))
+        return sendReport
+      }
+      return groupChat
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    messenger.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { roundList, timeoutMS, callback in
+        self.actions.append(.didWaitForRoundResults(roundList: roundList, timeoutMS: timeoutMS))
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in
+        self.actions.append(.didSaveMessage(message))
+        var message = message
+        message.id = messageId
+        return message
+      }
+      db.bulkUpdateMessages.run = { query, assignments in
+        self.actions.append(.didUpdateMessage(query: query, assignments: assignments))
+        return 1
+      }
+      return db
+    }
+    let now = Date()
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: { now }
+    )
+
+    send(
+      text: text,
+      to: groupId,
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didSaveMessage(.init(
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text
+      )),
+      .didSend(
+        groupId: groupId,
+        message: try! MessagePayload(text: text).encode(),
+        tag: nil
+      ),
+      .didSaveMessage(.init(
+        id: messageId,
+        networkId: sendReport.messageId,
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text,
+        roundURL: sendReport.roundURL
+      )),
+      .didWaitForRoundResults(
+        roundList: try! sendReport.encode(),
+        timeoutMS: 30_000
+      ),
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sent)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: true))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingTimedOut)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: false))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingFailed)
+      ),
+      .didComplete,
+    ])
+  }
+
+  func testSendDatabaseFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messenger: Messenger = .unimplemented
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.groupChat.get = { .unimplemented }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = { throw failure }
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: XCTestDynamicOverlay.unimplemented("now", placeholder: Date())
+    )
+
+    send(
+      text: "Hello",
+      to: "group-id".data(using: .utf8)!,
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
+      .didComplete
+    ])
+  }
+
+  func testBulkUpdateOnDeliveryFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { _, _, _ in
+        GroupSendReport(
+          rounds: [],
+          roundURL: "",
+          timestamp: 0,
+          messageId: Data()
+        )
+      }
+      return groupChat
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { _, _, callback in
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in message }
+      db.bulkUpdateMessages.run = { _, _ in throw failure }
+      return db
+    }
+    let now = Date()
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: { now }
+    )
+
+    send(
+      text: "Hello",
+      to: Data(),
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
+      .didComplete,
+    ])
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift
index 7533666ecc857876698f0478e688257cb2c501e6..b9c76682c487cabde0d702e71a8f1abf81f9c393 100644
--- a/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift
+++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppComponentTests.swift
@@ -64,6 +64,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.start)
@@ -76,6 +84,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -117,6 +127,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.start)
@@ -129,6 +147,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -170,6 +190,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.welcome(.finished)) {
@@ -183,6 +211,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -224,6 +254,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.restore(.finished)) {
@@ -237,6 +275,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -275,6 +315,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.home(.deleteAccount(.success))) {
@@ -288,6 +336,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -378,6 +428,14 @@ final class AppComponentTests: XCTestCase {
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {}
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {}
+    }
 
     actions = []
     store.send(.start)
@@ -390,6 +448,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -447,6 +507,18 @@ final class AppComponentTests: XCTestCase {
     store.dependencies.app.backupStorage.store = { data in
       actions.append(.didStoreBackup(data))
     }
+    store.dependencies.app.groupRequestHandler.run = { _ in
+      actions.append(.didStartGroupRequestHandler)
+      return Cancellable {
+        actions.append(.didCancelGroupRequestHandler)
+      }
+    }
+    store.dependencies.app.groupMessageHandler.run = { _ in
+      actions.append(.didStartGroupMessageHandler)
+      return Cancellable {
+        actions.append(.didCancelGroupMessageHandler)
+      }
+    }
 
     actions = []
     store.send(.start)
@@ -458,6 +530,8 @@ final class AppComponentTests: XCTestCase {
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -473,10 +547,14 @@ final class AppComponentTests: XCTestCase {
       .didCancelAuthHandler,
       .didCancelMessageListener,
       .didCancelReceiveFileHandler,
+      .didCancelGroupRequestHandler,
+      .didCancelGroupMessageHandler,
       .didCancelBackupCallback,
       .didStartAuthHandler,
       .didStartMessageListener,
       .didStartReceiveFileHandler,
+      .didStartGroupRequestHandler,
+      .didStartGroupMessageHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -519,6 +597,8 @@ final class AppComponentTests: XCTestCase {
       .didCancelAuthHandler,
       .didCancelMessageListener,
       .didCancelReceiveFileHandler,
+      .didCancelGroupRequestHandler,
+      .didCancelGroupMessageHandler,
       .didCancelBackupCallback,
     ])
   }
@@ -539,4 +619,8 @@ private enum Action: Equatable {
   case didStoreBackup(Data)
   case didSetLogLevel(LogLevel)
   case didStartLogging
+  case didStartGroupRequestHandler
+  case didCancelGroupRequestHandler
+  case didStartGroupMessageHandler
+  case didCancelGroupMessageHandler
 }
diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
index c8f5952864c6f77f04ec42b53b5875a317ee37a4..ba7ed23792f71c169b42daffc10fec988caef3a5 100644
--- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
+++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
@@ -9,7 +9,7 @@ import XXModels
 @testable import ChatFeature
 
 final class ChatComponentTests: XCTestCase {
-  func testStart() {
+  func testStartDirectChat() {
     let contactId = "contact-id".data(using: .utf8)!
     let myContactId = "my-contact-id".data(using: .utf8)!
 
@@ -22,6 +22,8 @@ final class ChatComponentTests: XCTestCase {
     let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
     var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = []
     let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>()
+    var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
+    let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
 
     store.dependencies.app.mainQueue = .immediate
     store.dependencies.app.bgQueue = .immediate
@@ -40,6 +42,10 @@ final class ChatComponentTests: XCTestCase {
         didFetchMessagesWithQuery.append(query)
         return messagesPublisher.eraseToAnyPublisher()
       }
+      db.fetchContactsPublisher.run = { query in
+        didFetchContactsWithQuery.append(query)
+        return contactsPublisher.eraseToAnyPublisher()
+      }
       db.fetchFileTransfersPublisher.run = { query in
         didFetchFileTransfersWithQuery.append(query)
         return fileTransfersPublisher.eraseToAnyPublisher()
@@ -58,6 +64,9 @@ final class ChatComponentTests: XCTestCase {
       .init(contactId: contactId, isIncoming: true),
       .init(contactId: myContactId, isIncoming: false),
     ])
+    XCTAssertNoDifference(didFetchContactsWithQuery, [
+      .init(),
+    ])
 
     let receivedFileTransfer = FileTransfer(
       id: "file-transfer-1-id".data(using: .utf8)!,
@@ -111,12 +120,17 @@ final class ChatComponentTests: XCTestCase {
       receivedFileTransfer,
       sentFileTransfer,
     ])
+    contactsPublisher.send([
+      .init(id: myContactId, username: "My username"),
+      .init(id: contactId, username: "Contact username"),
+    ])
 
     let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
       .init(
         id: 1,
         date: Date(timeIntervalSince1970: 1),
         senderId: contactId,
+        senderName: "Contact username",
         text: "Message 1",
         status: .received,
         fileTransfer: receivedFileTransfer
@@ -125,6 +139,7 @@ final class ChatComponentTests: XCTestCase {
         id: 2,
         date: Date(timeIntervalSince1970: 2),
         senderId: myContactId,
+        senderName: "My username",
         text: "Message 2",
         status: .sent,
         fileTransfer: sentFileTransfer
@@ -137,6 +152,131 @@ final class ChatComponentTests: XCTestCase {
 
     messagesPublisher.send(completion: .finished)
     fileTransfersPublisher.send(completion: .finished)
+    contactsPublisher.send(completion: .finished)
+  }
+
+  func testStartGroupChat() {
+    let groupId = "group-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let firstMemberId = "member-1-id".data(using: .utf8)!
+    let secondMemberId = "member-2-id".data(using: .utf8)!
+
+    let store = TestStore(
+      initialState: ChatComponent.State(id: .group(groupId)),
+      reducer: ChatComponent()
+    )
+
+    var didFetchMessagesWithQuery: [XXModels.Message.Query] = []
+    let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
+    var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
+    let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.fetchMessagesPublisher.run = { query in
+        didFetchMessagesWithQuery.append(query)
+        return messagesPublisher.eraseToAnyPublisher()
+      }
+      db.fetchContactsPublisher.run = { query in
+        didFetchContactsWithQuery.append(query)
+        return contactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
+    store.send(.start) {
+      $0.myContactId = myContactId
+    }
+
+    XCTAssertNoDifference(didFetchMessagesWithQuery, [
+      .init(chat: .group(groupId))
+    ])
+    XCTAssertNoDifference(didFetchContactsWithQuery, [
+      .init(),
+    ])
+
+    messagesPublisher.send([
+      .init(
+        id: 0,
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 0),
+        status: .sent,
+        isUnread: false,
+        text: "Message 0"
+      ),
+      .init(
+        id: 1,
+        senderId: firstMemberId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 1),
+        status: .received,
+        isUnread: false,
+        text: "Message 1"
+      ),
+      .init(
+        id: 2,
+        senderId: secondMemberId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 2),
+        status: .received,
+        isUnread: false,
+        text: "Message 2"
+      ),
+    ])
+    contactsPublisher.send([
+      .init(id: myContactId, username: "My username"),
+      .init(id: firstMemberId, username: "First username"),
+      .init(id: secondMemberId, username: "Second username"),
+    ])
+
+    let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
+      .init(
+        id: 0,
+        date: Date(timeIntervalSince1970: 0),
+        senderId: myContactId,
+        senderName: "My username",
+        text: "Message 0",
+        status: .sent
+      ),
+      .init(
+        id: 1,
+        date: Date(timeIntervalSince1970: 1),
+        senderId: firstMemberId,
+        senderName: "First username",
+        text: "Message 1",
+        status: .received
+      ),
+      .init(
+        id: 2,
+        date: Date(timeIntervalSince1970: 2),
+        senderId: secondMemberId,
+        senderName: "Second username",
+        text: "Message 2",
+        status: .received
+      ),
+    ])
+
+    store.receive(.didFetchMessages(expectedMessages)) {
+      $0.messages = expectedMessages
+    }
+
+    messagesPublisher.send(completion: .finished)
+    contactsPublisher.send(completion: .finished)
   }
 
   func testStartFailure() {
@@ -165,7 +305,7 @@ final class ChatComponentTests: XCTestCase {
     }
   }
 
-  func testSend() {
+  func testSendDirectMessage() {
     struct SendMessageParams: Equatable {
       var text: String
       var recipientId: Data
@@ -200,7 +340,7 @@ final class ChatComponentTests: XCTestCase {
     sendMessageCompletion?()
   }
 
-  func testSendFailure() {
+  func testSendDirectMessageFailure() {
     var sendMessageOnError: SendMessage.OnError?
     var sendMessageCompletion: SendMessage.Completion?
 
@@ -237,6 +377,80 @@ final class ChatComponentTests: XCTestCase {
     }
   }
 
+  func testSendGroupMessage() {
+    let groupId = "group-id".data(using: .utf8)!
+    let text = "Hello"
+    struct SendGroupMessageParams: Equatable {
+      var text: String
+      var groupId: Data
+    }
+    var didSendGroupMessageWithParams: [SendGroupMessageParams] = []
+    var sendGroupMessageCompletion: SendGroupMessage.Completion?
+
+    let store = TestStore(
+      initialState: ChatComponent.State(id: .group(groupId)),
+      reducer: ChatComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.sendGroupMessage.run = { text, groupId, _, completion in
+      didSendGroupMessageWithParams.append(.init(text: text, groupId: groupId))
+      sendGroupMessageCompletion = completion
+    }
+
+    store.send(.set(\.$text, text)) {
+      $0.text = text
+    }
+
+    store.send(.sendTapped) {
+      $0.text = ""
+    }
+
+    XCTAssertNoDifference(didSendGroupMessageWithParams, [
+      .init(text: text, groupId: groupId)
+    ])
+
+    sendGroupMessageCompletion?()
+  }
+
+  func testSendGroupMessageFailure() {
+    var sendGroupMessageOnError: SendGroupMessage.OnError?
+    var sendGroupMessageCompletion: SendGroupMessage.Completion?
+
+    let store = TestStore(
+      initialState: ChatComponent.State(
+        id: .group("group-id".data(using: .utf8)!),
+        text: "Hello"
+      ),
+      reducer: ChatComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.sendGroupMessage.run = { _, _, onError, completion in
+      sendGroupMessageOnError = onError
+      sendGroupMessageCompletion = completion
+    }
+
+    store.send(.sendTapped) {
+      $0.text = ""
+    }
+
+    let error = NSError(domain: "test", code: 123)
+    sendGroupMessageOnError?(error)
+
+    store.receive(.sendFailed(error.localizedDescription)) {
+      $0.sendFailure = error.localizedDescription
+    }
+
+    sendGroupMessageCompletion?()
+
+    store.send(.dismissSendFailureTapped) {
+      $0.sendFailure = nil
+    }
+  }
+
   func testSendImage() {
     struct SendImageParams: Equatable {
       var image: Data
diff --git a/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..b3791b1f107106d9935e6576e664f9ad40cf80f7
--- /dev/null
+++ b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift
@@ -0,0 +1,193 @@
+import ChatFeature
+import Combine
+import ComposableArchitecture
+import CustomDump
+import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import GroupFeature
+
+final class GroupComponentTests: XCTestCase {
+  enum Action: Equatable {
+    case didFetchGroupInfos(GroupInfo.Query)
+    case didJoinGroup(Data)
+    case didSaveGroup(XXModels.Group)
+  }
+
+  var actions: [Action]!
+
+  override func setUp() {
+    actions = []
+  }
+
+  override func tearDown() {
+    actions = nil
+  }
+
+  func testStart() {
+    let groupId = "group-id".data(using: .utf8)!
+    let groupInfosSubject = PassthroughSubject<[GroupInfo], Error>()
+
+    let store = TestStore(
+      initialState: GroupComponent.State(
+        groupId: groupId
+      ),
+      reducer: GroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.fetchGroupInfosPublisher.run = { query in
+        self.actions.append(.didFetchGroupInfos(query))
+        return groupInfosSubject.eraseToAnyPublisher()
+      }
+      return db
+    }
+
+    store.send(.start)
+
+    XCTAssertNoDifference(actions, [
+      .didFetchGroupInfos(.init(groupId: groupId)),
+    ])
+
+    let groupInfo = GroupInfo.stub()
+    groupInfosSubject.send([groupInfo])
+
+    store.receive(.didFetchGroupInfo(groupInfo)) {
+      $0.groupInfo = groupInfo
+    }
+
+    groupInfosSubject.send(completion: .finished)
+  }
+
+  func testJoinGroup() {
+    var groupInfo = GroupInfo.stub()
+    groupInfo.group.authStatus = .pending
+
+    let store = TestStore(
+      initialState: GroupComponent.State(
+        groupId: groupInfo.group.id,
+        groupInfo: groupInfo
+      ),
+      reducer: GroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.joinGroup.run = { serializedGroupData in
+        self.actions.append(.didJoinGroup(serializedGroupData))
+      }
+      return groupChat
+    }
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.saveGroup.run = { group in
+        self.actions.append(.didSaveGroup(group))
+        return group
+      }
+      return db
+    }
+
+    store.send(.joinButtonTapped) {
+      $0.isJoining = true
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didJoinGroup(groupInfo.group.serialized),
+      .didSaveGroup({
+        var group = groupInfo.group
+        group.authStatus = .participating
+        return group
+      }())
+    ])
+
+    store.receive(.didJoin) {
+      $0.isJoining = false
+    }
+  }
+
+  func testJoinGroupFailure() {
+    let groupInfo = GroupInfo.stub()
+    struct Failure: Error {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: GroupComponent.State(
+        groupId: groupInfo.group.id,
+        groupInfo: groupInfo
+      ),
+      reducer: GroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.joinGroup.run = { _ in throw failure }
+      return groupChat
+    }
+
+    store.send(.joinButtonTapped) {
+      $0.isJoining = true
+    }
+
+    store.receive(.didFailToJoin(failure.localizedDescription)) {
+      $0.isJoining = false
+      $0.joinFailure = failure.localizedDescription
+    }
+  }
+
+  func testPresentChat() {
+    let groupInfo = GroupInfo.stub()
+
+    let store = TestStore(
+      initialState: GroupComponent.State(
+        groupId: groupInfo.group.id,
+        groupInfo: groupInfo
+      ),
+      reducer: GroupComponent()
+    )
+
+    store.send(.chatButtonTapped) {
+      $0.chat = ChatComponent.State(id: .group(groupInfo.id))
+    }
+
+    store.send(.didDismissChat) {
+      $0.chat = nil
+    }
+  }
+}
+
+private extension XXModels.GroupInfo {
+  static func stub() -> XXModels.GroupInfo {
+    XXModels.GroupInfo(
+      group: .init(
+        id: "group-id".data(using: .utf8)!,
+        name: "Group Name",
+        leaderId: "group-leader-id".data(using: .utf8)!,
+        createdAt: Date(timeIntervalSince1970: TimeInterval(86_400)),
+        authStatus: .participating,
+        serialized: "group-serialized".data(using: .utf8)!
+      ),
+      leader: .init(
+        id: "group-leader-id".data(using: .utf8)!,
+        username: "Group leader"
+      ),
+      members: [
+        .init(
+          id: "member-1-id".data(using: .utf8)!,
+          username: "Member 1"
+        ),
+        .init(
+          id: "member-2-id".data(using: .utf8)!,
+          username: "Member 2"
+        ),
+      ]
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift b/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..5742aaf73119e9b3687191f587de4b2203f4f944
--- /dev/null
+++ b/Examples/xx-messenger/Tests/GroupsFeatureTests/GroupsComponentTests.swift
@@ -0,0 +1,158 @@
+import Combine
+import ComposableArchitecture
+import CustomDump
+import GroupFeature
+import NewGroupFeature
+import XCTest
+import XXModels
+@testable import GroupsFeature
+
+final class GroupsComponentTests: XCTestCase {
+  enum Action: Equatable {
+    case didFetchGroups(XXModels.Group.Query)
+  }
+
+  var actions: [Action]!
+
+  override func setUp() {
+    actions = []
+  }
+
+  override func tearDown() {
+    actions = nil
+  }
+
+  func testStart() {
+    let groupsSubject = PassthroughSubject<[XXModels.Group], Error>()
+
+    let store = TestStore(
+      initialState: GroupsComponent.State(),
+      reducer: GroupsComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.fetchGroupsPublisher.run = { query in
+        self.actions.append(.didFetchGroups(query))
+        return groupsSubject.eraseToAnyPublisher()
+      }
+      return db
+    }
+
+    store.send(.start)
+
+    XCTAssertNoDifference(actions, [
+      .didFetchGroups(.init())
+    ])
+
+    let groups: [XXModels.Group] = [
+      .stub(1),
+      .stub(2),
+      .stub(3),
+    ]
+    groupsSubject.send(groups)
+
+    store.receive(.didFetchGroups(groups)) {
+      $0.groups = IdentifiedArray(uniqueElements: groups)
+    }
+
+    groupsSubject.send(completion: .finished)
+  }
+
+  func testSelectGroup() {
+    let groups: [XXModels.Group] = [
+      .stub(1),
+      .stub(2),
+      .stub(3),
+    ]
+
+    let store = TestStore(
+      initialState: GroupsComponent.State(
+        groups: IdentifiedArray(uniqueElements: groups)
+      ),
+      reducer: GroupsComponent()
+    )
+
+    store.send(.didSelectGroup(groups[1])) {
+      $0.group = GroupComponent.State(groupId: groups[1].id)
+    }
+  }
+
+  func testDismissGroup() {
+    let groups: [XXModels.Group] = [
+      .stub(1),
+      .stub(2),
+      .stub(3),
+    ]
+
+    let store = TestStore(
+      initialState: GroupsComponent.State(
+        groups: IdentifiedArray(uniqueElements: groups),
+        group: GroupComponent.State(
+          groupId: groups[1].id
+        )
+      ),
+      reducer: GroupsComponent()
+    )
+
+    store.send(.didDismissGroup) {
+      $0.group = nil
+    }
+  }
+
+  func testPresentNewGroup() {
+    let store = TestStore(
+      initialState: GroupsComponent.State(),
+      reducer: GroupsComponent()
+    )
+
+    store.send(.newGroupButtonTapped) {
+      $0.newGroup = NewGroupComponent.State()
+    }
+
+    store.send(.newGroupDismissed) {
+      $0.newGroup = nil
+    }
+  }
+
+  func testDismissNewGroup() {
+    let store = TestStore(
+      initialState: GroupsComponent.State(
+        newGroup: NewGroupComponent.State()
+      ),
+      reducer: GroupsComponent()
+    )
+
+    store.send(.newGroupDismissed) {
+      $0.newGroup = nil
+    }
+  }
+
+  func testNewGroupDidFinish() {
+    let store = TestStore(
+      initialState: GroupsComponent.State(
+        newGroup: NewGroupComponent.State()
+      ),
+      reducer: GroupsComponent()
+    )
+
+    store.send(.newGroup(.didFinish)) {
+      $0.newGroup = nil
+    }
+  }
+}
+
+private extension XXModels.Group {
+  static func stub(_ id: Int) -> XXModels.Group {
+    XXModels.Group(
+      id: "group-\(id)-id".data(using: .utf8)!,
+      name: "Group \(id)",
+      leaderId: "group-\(id)-leader-id".data(using: .utf8)!,
+      createdAt: Date(timeIntervalSince1970: TimeInterval(id * 86_400)),
+      authStatus: .participating,
+      serialized: "group-\(id)-serialized".data(using: .utf8)!
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift
index 3bd49a4283b9b4e535c40fb8c9279a5482aa3d16..813a4534da468e32fc31fdde76c21ab4f8f00f33 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeComponentTests.swift
@@ -3,6 +3,7 @@ import BackupFeature
 import ComposableArchitecture
 import ContactsFeature
 import CustomDump
+import GroupsFeature
 import RegisterFeature
 import UserSearchFeature
 import XCTest
@@ -22,6 +23,7 @@ final class HomeComponentTests: XCTestCase {
     var messengerDidConnect = 0
     var messengerDidListenForMessages = 0
     var messengerDidStartFileTransfer = 0
+    var messengerDidStartGroupChat = 0
 
     store.dependencies.app.bgQueue = .immediate
     store.dependencies.app.mainQueue = .immediate
@@ -34,6 +36,8 @@ final class HomeComponentTests: XCTestCase {
     store.dependencies.app.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 }
     store.dependencies.app.messenger.isLoggedIn.run = { false }
     store.dependencies.app.messenger.isRegistered.run = { false }
+    store.dependencies.app.messenger.isGroupChatRunning.run = { false }
+    store.dependencies.app.messenger.startGroupChat.run = { messengerDidStartGroupChat += 1 }
 
     store.send(.messenger(.start))
 
@@ -41,6 +45,7 @@ final class HomeComponentTests: XCTestCase {
     XCTAssertNoDifference(messengerDidConnect, 1)
     XCTAssertNoDifference(messengerDidListenForMessages, 1)
     XCTAssertNoDifference(messengerDidStartFileTransfer, 1)
+    XCTAssertNoDifference(messengerDidStartGroupChat, 1)
 
     store.receive(.networkMonitor(.stop))
     store.receive(.messenger(.didStartUnregistered)) {
@@ -60,6 +65,7 @@ final class HomeComponentTests: XCTestCase {
     var messengerDidStartFileTransfer = 0
     var messengerDidLogIn = 0
     var messengerDidResumeBackup = 0
+    var messengerDidStartGroupChat = 0
 
     store.dependencies.app.bgQueue = .immediate
     store.dependencies.app.mainQueue = .immediate
@@ -84,6 +90,8 @@ final class HomeComponentTests: XCTestCase {
       }
       return cMix
     }
+    store.dependencies.app.messenger.isGroupChatRunning.run = { false }
+    store.dependencies.app.messenger.startGroupChat.run = { messengerDidStartGroupChat += 1 }
 
     store.send(.messenger(.start))
 
@@ -93,6 +101,7 @@ final class HomeComponentTests: XCTestCase {
     XCTAssertNoDifference(messengerDidStartFileTransfer, 1)
     XCTAssertNoDifference(messengerDidLogIn, 1)
     XCTAssertNoDifference(messengerDidResumeBackup, 1)
+    XCTAssertNoDifference(messengerDidStartGroupChat, 1)
 
     store.receive(.networkMonitor(.stop))
     store.receive(.messenger(.didStartRegistered))
@@ -131,6 +140,7 @@ final class HomeComponentTests: XCTestCase {
       }
       return cMix
     }
+    store.dependencies.app.messenger.isGroupChatRunning.run = { true }
 
     store.send(.register(.finished)) {
       $0.register = nil
@@ -209,6 +219,7 @@ final class HomeComponentTests: XCTestCase {
     store.dependencies.app.messenger.isFileTransferRunning.run = { true }
     store.dependencies.app.messenger.isLoggedIn.run = { false }
     store.dependencies.app.messenger.isRegistered.run = { throw error }
+    store.dependencies.app.messenger.isGroupChatRunning.run = { true }
 
     store.send(.messenger(.start))
 
@@ -236,6 +247,7 @@ final class HomeComponentTests: XCTestCase {
     store.dependencies.app.messenger.isLoggedIn.run = { false }
     store.dependencies.app.messenger.isRegistered.run = { true }
     store.dependencies.app.messenger.logIn.run = { throw error }
+    store.dependencies.app.messenger.isGroupChatRunning.run = { true }
 
     store.send(.messenger(.start))
 
@@ -529,4 +541,28 @@ final class HomeComponentTests: XCTestCase {
       $0.backup = nil
     }
   }
+
+  func testGroupsButtonTapped() {
+    let store = TestStore(
+      initialState: HomeComponent.State(),
+      reducer: HomeComponent()
+    )
+
+    store.send(.groupsButtonTapped) {
+      $0.groups = GroupsComponent.State()
+    }
+  }
+
+  func testDidDismissGroups() {
+    let store = TestStore(
+      initialState: HomeComponent.State(
+        groups: GroupsComponent.State()
+      ),
+      reducer: HomeComponent()
+    )
+
+    store.send(.didDismissGroups) {
+      $0.groups = nil
+    }
+  }
 }
diff --git a/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3207ed8cf24c110fafb019dcb9280a6b0b5e496e
--- /dev/null
+++ b/Examples/xx-messenger/Tests/NewGroupFeatureTests/NewGroupComponentTests.swift
@@ -0,0 +1,290 @@
+import Combine
+import ComposableArchitecture
+import CustomDump
+import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import NewGroupFeature
+
+final class NewGroupComponentTests: XCTestCase {
+  enum Action: Equatable {
+    case didFetchContacts(XXModels.Contact.Query)
+    case didMakeGroup(membership: [Data], message: Data?, name: Data?)
+    case didSaveGroup(XXModels.Group)
+    case didSaveMessage(XXModels.Message)
+    case didSaveGroupMember(XXModels.GroupMember)
+  }
+
+  var actions: [Action]!
+
+  override func setUp() {
+    actions = []
+  }
+
+  override func tearDown() {
+    actions = nil
+  }
+
+  func testStart() {
+    let contactsSubject = PassthroughSubject<[XXModels.Contact], Error>()
+
+    let store = TestStore(
+      initialState: NewGroupComponent.State(),
+      reducer: NewGroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!)
+        contact.getIdFromContact.run = { _ in "my-contact-id".data(using: .utf8)! }
+        return contact
+      }
+      return e2e
+    }
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.fetchContactsPublisher.run = { query in
+        self.actions.append(.didFetchContacts(query))
+        return contactsSubject.eraseToAnyPublisher()
+      }
+      return db
+    }
+
+    store.send(.start)
+
+    XCTAssertNoDifference(actions, [
+      .didFetchContacts(.init())
+    ])
+
+    let contacts: [XXModels.Contact] = [
+      .init(id: "contact-1-id".data(using: .utf8)!),
+      .init(id: "contact-2-id".data(using: .utf8)!),
+      .init(id: "contact-3-id".data(using: .utf8)!),
+    ]
+    contactsSubject.send(contacts)
+
+    store.receive(.didFetchContacts(contacts)) {
+      $0.contacts = IdentifiedArray(uniqueElements: contacts)
+    }
+
+    contactsSubject.send(completion: .finished)
+  }
+
+  func testSelectMembers() {
+    let contacts: [XXModels.Contact] = [
+      .init(id: "contact-1-id".data(using: .utf8)!),
+      .init(id: "contact-2-id".data(using: .utf8)!),
+      .init(id: "contact-3-id".data(using: .utf8)!),
+    ]
+
+    let store = TestStore(
+      initialState: NewGroupComponent.State(
+        contacts: IdentifiedArray(uniqueElements: contacts)
+      ),
+      reducer: NewGroupComponent()
+    )
+
+    store.send(.didSelectContact(contacts[0])) {
+      $0.members = IdentifiedArray(uniqueElements: [contacts[0]])
+    }
+
+    store.send(.didSelectContact(contacts[1])) {
+      $0.members = IdentifiedArray(uniqueElements: [contacts[0], contacts[1]])
+    }
+
+    store.send(.didSelectContact(contacts[0])) {
+      $0.members = IdentifiedArray(uniqueElements: [contacts[1]])
+    }
+  }
+
+  func testEnterGroupName() {
+    let store = TestStore(
+      initialState: NewGroupComponent.State(),
+      reducer: NewGroupComponent()
+    )
+
+    store.send(.binding(.set(\.$focusedField, .name))) {
+      $0.focusedField = .name
+    }
+
+    store.send(.binding(.set(\.$name, "My New Group"))) {
+      $0.name = "My New Group"
+    }
+
+    store.send(.binding(.set(\.$focusedField, nil))) {
+      $0.focusedField = nil
+    }
+  }
+
+  func testEnterInitialMessage() {
+    let store = TestStore(
+      initialState: NewGroupComponent.State(),
+      reducer: NewGroupComponent()
+    )
+
+    store.send(.binding(.set(\.$focusedField, .message))) {
+      $0.focusedField = .message
+    }
+
+    store.send(.binding(.set(\.$message, "Welcome message"))) {
+      $0.message = "Welcome message"
+    }
+
+    store.send(.binding(.set(\.$focusedField, nil))) {
+      $0.focusedField = nil
+    }
+  }
+
+  func testCreateGroup() {
+    let members: [XXModels.Contact] = [
+      .init(id: "member-contact-1".data(using: .utf8)!),
+      .init(id: "member-contact-2".data(using: .utf8)!),
+      .init(id: "member-contact-3".data(using: .utf8)!),
+    ]
+    let name = "New group"
+    let message = "Welcome message"
+    let groupReport = GroupReport(
+      id: "new-group-id".data(using: .utf8)!,
+      rounds: [],
+      roundURL: "",
+      status: 0
+    )
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let currentDate = Date(timeIntervalSince1970: 123)
+
+    let store = TestStore(
+      initialState: NewGroupComponent.State(
+        members: IdentifiedArray(uniqueElements: members),
+        name: name,
+        message: message
+      ),
+      reducer: NewGroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.makeGroup.run = { membership, message, name in
+        self.actions.append(.didMakeGroup(
+          membership: membership,
+          message: message,
+          name: name
+        ))
+        return groupReport
+      }
+      return groupChat
+    }
+    store.dependencies.app.messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!)
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    store.dependencies.date = .constant(currentDate)
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.saveGroup.run = { group in
+        self.actions.append(.didSaveGroup(group))
+        return group
+      }
+      db.saveMessage.run = { message in
+        self.actions.append(.didSaveMessage(message))
+        return message
+      }
+      db.saveGroupMember.run = { groupMember in
+        self.actions.append(.didSaveGroupMember(groupMember))
+        return groupMember
+      }
+      return db
+    }
+
+    store.send(.createButtonTapped) {
+      $0.isCreating = true
+    }
+
+    XCTAssertNoDifference(actions, [
+      .didMakeGroup(
+        membership: members.map(\.id),
+        message: message.data(using: .utf8)!,
+        name: name.data(using: .utf8)!
+      ),
+      .didSaveGroup(.init(
+        id: groupReport.id,
+        name: name,
+        leaderId: myContactId,
+        createdAt: currentDate,
+        authStatus: .participating,
+        serialized: try! groupReport.encode()
+      )),
+      .didSaveMessage(.init(
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupReport.id,
+        date: currentDate,
+        status: .sent,
+        isUnread: false,
+        text: message
+      )),
+      .didSaveGroupMember(.init(
+        groupId: groupReport.id,
+        contactId: members[0].id
+      )),
+      .didSaveGroupMember(.init(
+        groupId: groupReport.id,
+        contactId: members[1].id
+      )),
+      .didSaveGroupMember(.init(
+        groupId: groupReport.id,
+        contactId: members[2].id
+      )),
+    ])
+
+    store.receive(.didFinish) {
+      $0.isCreating = false
+    }
+  }
+
+  func testCreateGroupFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    let store = TestStore(
+      initialState: NewGroupComponent.State(),
+      reducer: NewGroupComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.makeGroup.run = { _, _, _ in throw failure }
+      return groupChat
+    }
+
+    store.send(.createButtonTapped) {
+      $0.isCreating = true
+    }
+
+    store.receive(.didFail(failure.localizedDescription)) {
+      $0.isCreating = false
+      $0.failure = failure.localizedDescription
+    }
+  }
+
+  func testFinish() {
+    let store = TestStore(
+      initialState: NewGroupComponent.State(),
+      reducer: NewGroupComponent()
+    )
+
+    store.send(.didFinish)
+  }
+}
diff --git a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 4626e31d8eb62969ebd67d9f70afd2f184787bd0..6a90e7fb3973c153ce2fa363a6ed46ea8f2fe03f 100644
--- a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -14,8 +14,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/combine-schedulers",
       "state" : {
-        "revision" : "aa3e575929f2bcc5bad012bd2575eae716cbcdf7",
-        "version" : "0.8.0"
+        "revision" : "882ac01eb7ef9e36d4467eb4b1151e74fcef85ab",
+        "version" : "0.9.1"
       }
     },
     {
@@ -23,8 +23,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/groue/GRDB.swift",
       "state" : {
-        "revision" : "13e1f4d7c2896a6a9293102f664e5311e017ffb2",
-        "version" : "6.1.0"
+        "revision" : "8330469ac3cbbf0ee52b7e8a4e2b1f4c44bb13ea",
+        "version" : "6.4.0"
       }
     },
     {
@@ -50,8 +50,17 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-case-paths",
       "state" : {
-        "revision" : "15bba50ebf3a2065388c8d12210debe4f6ada202",
-        "version" : "0.10.0"
+        "revision" : "bb436421f57269fbcfe7360735985321585a86e5",
+        "version" : "0.10.1"
+      }
+    },
+    {
+      "identity" : "swift-clocks",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/pointfreeco/swift-clocks",
+      "state" : {
+        "revision" : "20b25ca0dd88ebfb9111ec937814ddc5a8880172",
+        "version" : "0.2.0"
       }
     },
     {
@@ -68,8 +77,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-composable-architecture.git",
       "state" : {
-        "revision" : "5bd450a8ac6a802f82d485bac219cbfacffa69fb",
-        "version" : "0.43.0"
+        "revision" : "c9259b5f74892690cb04a9a8088b4a1789b05a7d",
+        "version" : "0.47.2"
       }
     },
     {
@@ -77,8 +86,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/darrarski/swift-composable-presentation.git",
       "state" : {
-        "revision" : "f69eb0c9a82832f67dfd5dace98e6d0e8d748b0f",
-        "version" : "0.6.0"
+        "revision" : "dec8f4fa46d5e07848aa6fc763a6c9fd0007b37a",
+        "version" : "0.6.1"
       }
     },
     {
@@ -86,8 +95,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
       "state" : {
-        "revision" : "819d9d370cd721c9d87671e29d947279292e4541",
-        "version" : "0.6.0"
+        "revision" : "ead7d30cc224c3642c150b546f4f1080d1c411a8",
+        "version" : "0.6.1"
       }
     },
     {
@@ -95,8 +104,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-identified-collections",
       "state" : {
-        "revision" : "bfb0d43e75a15b6dfac770bf33479e8393884a36",
-        "version" : "0.4.1"
+        "revision" : "a08887de589e3829d488e0b4b707b2ca804b1060",
+        "version" : "0.5.0"
       }
     },
     {
@@ -108,13 +117,22 @@
         "version" : "1.4.4"
       }
     },
+    {
+      "identity" : "swiftui-navigation",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/pointfreeco/swiftui-navigation",
+      "state" : {
+        "revision" : "4b666bcc59ba1711a7543ecb37e1d181963b180c",
+        "version" : "0.4.2"
+      }
+    },
     {
       "identity" : "xctest-dynamic-overlay",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       "state" : {
-        "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784",
-        "version" : "0.5.0"
+        "revision" : "5a5457a744239896e9b0b03a8e1a5069c3e7b91f",
+        "version" : "0.6.0"
       }
     }
   ],
diff --git a/Frameworks/Bindings.txt b/Frameworks/Bindings.txt
index d38c6191e6b7efaeaefb18d4107cfdb300a18274..3e5d533ac45dc9ce1e158953db4c5e6a45bebfb0 100644
--- a/Frameworks/Bindings.txt
+++ b/Frameworks/Bindings.txt
@@ -1,4 +1,4 @@
-https://git.xx.network/elixxir/client/-/commit/7a48e9fb32831c90532d059148da03453a9093b7
+https://git.xx.network/elixxir/client/-/commit/118de443434478c16f16220858c54fb1237fc342
 go version go1.19.3 darwin/arm64
 Xcode 14.1 Build version 14B47b
 gomobile bind target: ios,iossimulator,macos
diff --git a/Frameworks/Bindings.xcframework/Info.plist b/Frameworks/Bindings.xcframework/Info.plist
index e66824e5243985b4ac053ef62aeaaf571fe03970..43fe32dad94767ae305b61db56f5604f8612617e 100644
--- a/Frameworks/Bindings.xcframework/Info.plist
+++ b/Frameworks/Bindings.xcframework/Info.plist
@@ -6,7 +6,7 @@
 	<array>
 		<dict>
 			<key>LibraryIdentifier</key>
-			<string>ios-arm64_x86_64-simulator</string>
+			<string>macos-arm64_x86_64</string>
 			<key>LibraryPath</key>
 			<string>Bindings.framework</string>
 			<key>SupportedArchitectures</key>
@@ -15,34 +15,34 @@
 				<string>x86_64</string>
 			</array>
 			<key>SupportedPlatform</key>
-			<string>ios</string>
-			<key>SupportedPlatformVariant</key>
-			<string>simulator</string>
+			<string>macos</string>
 		</dict>
 		<dict>
 			<key>LibraryIdentifier</key>
-			<string>ios-arm64</string>
+			<string>ios-arm64_x86_64-simulator</string>
 			<key>LibraryPath</key>
 			<string>Bindings.framework</string>
 			<key>SupportedArchitectures</key>
 			<array>
 				<string>arm64</string>
+				<string>x86_64</string>
 			</array>
 			<key>SupportedPlatform</key>
 			<string>ios</string>
+			<key>SupportedPlatformVariant</key>
+			<string>simulator</string>
 		</dict>
 		<dict>
 			<key>LibraryIdentifier</key>
-			<string>macos-arm64_x86_64</string>
+			<string>ios-arm64</string>
 			<key>LibraryPath</key>
 			<string>Bindings.framework</string>
 			<key>SupportedArchitectures</key>
 			<array>
 				<string>arm64</string>
-				<string>x86_64</string>
 			</array>
 			<key>SupportedPlatform</key>
-			<string>macos</string>
+			<string>ios</string>
 		</dict>
 	</array>
 	<key>CFBundlePackageType</key>
diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings
index 9523b4aac9e1c29073e98d8728546d947098e37a..4b0ee2902564f493694ba1b4e2b7441f4ff8b61a 100644
Binary files a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index dcf46cd4439785f4f904ea1f41277a66ed3e7db9..29b692cb622e84ca73a5d29b1f3954765ffb29db 100644
--- a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -470,7 +470,7 @@ be returned by this function. Any padding will be discarded within
 this function.
 
 Parameters:
- - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
+  - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
  */
 - (NSData* _Nullable)decrypt:(NSData* _Nullable)ciphertext error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -478,15 +478,28 @@ Parameters:
 done on the plaintext so all encrypted data looks uniform at rest.
 
 Parameters:
- - plaintext - The data to be encrypted. This must be smaller than the block
-   size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
-   return an error.
+  - plaintext - The data to be encrypted. This must be smaller than the block
+    size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
+    return an error.
  */
 - (NSData* _Nullable)encrypt:(NSData* _Nullable)plaintext error:(NSError* _Nullable* _Nullable)error;
 /**
  * GetID returns the ID for this ChannelDbCipher in the channelDbCipherTracker.
  */
 - (long)getID;
+/**
+ * MarshalJSON marshals the cipher into valid JSON. This function adheres to the
+json.Marshaler interface.
+ */
+- (NSData* _Nullable)marshalJSON:(NSError* _Nullable* _Nullable)error;
+/**
+ * UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the
+json.Unmarshaler interface.
+
+Note that this function does not transfer the internal RNG. Use
+NewCipherFromJSON to properly reconstruct a cipher from JSON.
+ */
+- (BOOL)unmarshalJSON:(NSData* _Nullable)data error:(NSError* _Nullable* _Nullable)error;
 @end
 
 /**
@@ -495,10 +508,11 @@ contains the public channel info formatted in pretty print and the private
 key for the channel in PEM format.
 
 Example JSON:
- {
-   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
-	  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
- }
+
+	 {
+	   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
+		  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
+	 }
  */
 @interface BindingsChannelGeneration : NSObject <goSeqRefInterface> {
 }
@@ -514,11 +528,12 @@ Example JSON:
  * ChannelInfo contains information about a channel.
 
 Example of ChannelInfo JSON:
- {
-   "Name": "Test Channel",
-   "Description": "This is a test channel",
-   "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
- }
+
+	{
+	  "Name": "Test Channel",
+	  "Description": "This is a test channel",
+	  "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
+	}
  */
 @interface BindingsChannelInfo : NSObject <goSeqRefInterface> {
 }
@@ -536,11 +551,12 @@ Example of ChannelInfo JSON:
 ChannelsManager's Send operations.
 
 JSON Example:
- {
-   "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
-   "Rounds":[1,5,9],
-   "EphId": 0
- }
+
+	{
+	  "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
+	  "Rounds":[1,5,9],
+	  "EphId": 0
+	}
  */
 @interface BindingsChannelSendReport : NSObject <goSeqRefInterface> {
 }
@@ -574,12 +590,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 - (nullable instancetype)init:(long)cmixID privateIdentity:(NSData* _Nullable)privateIdentity eventBuilder:(id<BindingsEventModelBuilder> _Nullable)eventBuilder;
 // skipped constructor ChannelsManager.NewChannelsManagerGoEventModel with unsupported parameter or return types
@@ -597,13 +613,14 @@ string.
  * GetChannels returns the IDs of all channels that have been joined.
 
 Returns:
- - []byte - A JSON marshalled list of IDs.
+  - []byte - A JSON marshalled list of IDs.
 
 JSON Example:
- {
-   "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
-   "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
- }
+
+	{
+	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
+	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
+	}
  */
 - (NSData* _Nullable)getChannels:(NSError* _Nullable* _Nullable)error;
 /**
@@ -639,14 +656,14 @@ calling [ChannelsManager.JoinChannelFromURL]. There is no enforcement for
 public URLs.
 
 Parameters:
- - cmixID - The tracked Cmix object ID.
- - host - The URL to append the channel info to.
- - maxUses - The maximum number of uses the link can be used (0 for
-   unlimited).
- - marshalledChanId - A marshalled channel ID ([id.ID]).
+  - cmixID - The tracked Cmix object ID.
+  - host - The URL to append the channel info to.
+  - maxUses - The maximum number of uses the link can be used (0 for
+    unlimited).
+  - marshalledChanId - A marshalled channel ID ([id.ID]).
 
 Returns:
- - JSON of ShareURL.
+  - JSON of ShareURL.
  */
 - (NSData* _Nullable)getShareURL:(long)cmixID host:(NSString* _Nullable)host maxUses:(long)maxUses marshalledChanId:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -658,14 +675,15 @@ Returns:
 been joined.
 
 Parameters:
- - channelPretty - A portable channel string. Should be received from
-   another user or generated via GenerateChannel.
+  - channelPretty - A portable channel string. Should be received from
+    another user or generated via GenerateChannel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 - (NSData* _Nullable)joinChannel:(NSString* _Nullable)channelPretty error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -673,7 +691,7 @@ Returns:
 channel was not previously joined.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)leaveChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -685,10 +703,10 @@ There can only be one handler per [channels.MessageType], and this will
 return an error on any re-registration.
 
 Parameters:
- - messageType - represents the [channels.MessageType] which will have a
-   registered listener.
- - listenerCb - the callback which will be executed when a channel message
-   of messageType is received.
+  - messageType - represents the [channels.MessageType] which will have a
+    registered listener.
+  - listenerCb - the callback which will be executed when a channel message
+    of messageType is received.
  */
 - (BOOL)registerReceiveHandler:(long)messageType listenerCb:(id<BindingsChannelMessageReceptionCallback> _Nullable)listenerCb error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -696,7 +714,7 @@ Parameters:
 memory (~3 weeks) over the event model.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)replayChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -707,23 +725,23 @@ before being sent over the wire, is too long, this will return an error. The
 message must be at most 510 bytes long.
 
 Parameters:
- - adminPrivateKey - The PEM-encoded admin RSA private key.
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. The message should be at most 510
-   bytes. This need not be of data type string, as the message could be a
-   specified format that the channel may recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - adminPrivateKey - The PEM-encoded admin RSA private key.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. The message should be at most 510
+    bytes. This need not be of data type string, as the message could be a
+    specified format that the channel may recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendAdminGeneric:(NSData* _Nullable)adminPrivateKey marshalledChanId:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -736,22 +754,22 @@ to send a payload of 802 bytes at minimum. The meaning of validUntil depends
 on the use case.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. This need not be of data type
-   string, as the message could be a specified format that the channel may
-   recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. This need not be of data type
+    string, as the message could be a specified format that the channel may
+    recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendGeneric:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -764,20 +782,20 @@ The message will auto delete validUntil after the round it is sent in,
 lasting forever if [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
-   empty, and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
+    empty, and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendMessage:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -787,19 +805,19 @@ be rejected otherwise.
 Users will drop the reaction if they do not recognize the reactTo message.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - reaction - The user's reaction. This should be a single emoji with no
-   other characters. As such, a Unicode string is expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
- and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - reaction - The user's reaction. This should be a single emoji with no
+    other characters. As such, a Unicode string is expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendReaction:(NSData* _Nullable)marshalledChanId reaction:(NSString* _Nullable)reaction messageToReactTo:(NSData* _Nullable)messageToReactTo cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -814,25 +832,25 @@ delete validUntil after the round it is sent in, lasting forever if
 [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendReply:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message messageToReactTo:(NSData* _Nullable)messageToReactTo leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -2130,17 +2148,18 @@ channel the message was sent to and the message itself. This is returned via
 the callback as JSON marshalled bytes.
 
 JSON Example:
- {
-   "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
-   "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
-   "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
-   "MessageType": 42,
-   "SenderUsername": "hunter2",
-   "Content": "YmFuX2JhZFVTZXI=",
-   "Timestamp": 1662502150335283000,
-   "Lease": 25,
-   "Rounds": [ 1, 4, 9],
- }
+
+	{
+	  "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+	  "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
+	  "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
+	  "MessageType": 42,
+	  "SenderUsername": "hunter2",
+	  "Content": "YmFuX2JhZFVTZXI=",
+	  "Timestamp": 1662502150335283000,
+	  "Lease": 25,
+	  "Rounds": [ 1, 4, 9],
+	}
  */
 @interface BindingsReceivedChannelMessageReport : NSObject <goSeqRefInterface> {
 }
@@ -2271,22 +2290,25 @@ JSON Example:
 channel's share URL and password, if it needs one.
 
 JSON example for a public channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
-   "password": ""
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
+	  "password": ""
+	}
 
 JSON example for a private channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
-   "password": "tribune gangrene labrador italics nutmeg process exhume legal"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
+	  "password": "tribune gangrene labrador italics nutmeg process exhume legal"
+	}
 
 JSON example for a secret channel:
- {
-   "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
-   "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
+	  "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
+	}
  */
 @interface BindingsShareURL : NSObject <goSeqRefInterface> {
 }
@@ -2481,11 +2503,11 @@ FOUNDATION_EXPORT BOOL BindingsAsyncRequestRestLike(long e2eID, NSData* _Nullabl
 and codeset version.
 
 Parameters:
- - pubKey - The Ed25519 public key.
- - codesetVersion - The version of the codeset used to generate the identity.
+  - pubKey - The Ed25519 public key.
+  - codesetVersion - The version of the codeset used to generate the identity.
 
 Returns:
- - JSON of [channel.Identity].
+  - JSON of [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsConstructIdentity(NSData* _Nullable pubKey, long codesetVersion, NSError* _Nullable* _Nullable error);
 
@@ -2512,12 +2534,12 @@ pretty print. This function can only be used for private or secret channel
 URLs. To get the privacy level of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
- - password - The password needed to decrypt the secret data in the URL.
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
+  - password - The password needed to decrypt the secret data in the URL.
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePrivateURL(NSString* _Nullable url, NSString* _Nullable password, NSError* _Nullable* _Nullable error);
 
@@ -2527,11 +2549,11 @@ function can only be used for public channel URLs. To get the privacy level
 of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePublicURL(NSString* _Nullable url, NSError* _Nullable* _Nullable error);
 
@@ -2561,33 +2583,33 @@ the admin. It is only for making new channels, not joining existing ones.
 It returns a pretty print of the channel and the private key.
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - name - The name of the new channel. The name must be between 3 and 24
-   characters inclusive. It can only include upper and lowercase unicode
-   letters, digits 0 through 9, and underscores (_). It cannot be changed
-   once a channel is created.
- - description - The description of a channel. The description is optional
-   but cannot be longer than 144 characters and can include all unicode
-   characters. It cannot be changed once a channel is created.
- - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
-   1 = private, and 2 = secret. Refer to the comment below for more
-   information.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - name - The name of the new channel. The name must be between 3 and 24
+    characters inclusive. It can only include upper and lowercase unicode
+    letters, digits 0 through 9, and underscores (_). It cannot be changed
+    once a channel is created.
+  - description - The description of a channel. The description is optional
+    but cannot be longer than 144 characters and can include all unicode
+    characters. It cannot be changed once a channel is created.
+  - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
+    1 = private, and 2 = secret. Refer to the comment below for more
+    information.
 
 Returns:
- - []byte - [ChannelGeneration] describes a generated channel. It contains
-   both the public channel info and the private key for the channel in PEM
-   format.
+  - []byte - [ChannelGeneration] describes a generated channel. It contains
+    both the public channel info and the private key for the channel in PEM
+    format.
 
 The [broadcast.PrivacyLevel] of a channel indicates the level of channel
 information revealed when sharing it via URL. For any channel besides public
 channels, the secret information is encrypted and a password is required to
 share and join a channel.
- - A privacy level of [broadcast.Public] reveals all the information
-   including the name, description, privacy level, public key and salt.
- - A privacy level of [broadcast.Private] reveals only the name and
-   description.
- - A privacy level of [broadcast.Secret] reveals nothing.
+  - A privacy level of [broadcast.Public] reveals all the information
+    including the name, description, privacy level, public key and salt.
+  - A privacy level of [broadcast.Private] reveals only the name and
+    description.
+  - A privacy level of [broadcast.Secret] reveals nothing.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSString* _Nullable name, NSString* _Nullable description, long privacyLevel, NSError* _Nullable* _Nullable error);
 
@@ -2597,11 +2619,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSStrin
 via [GetPublicChannelIdentityFromPrivate].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
 
 Returns:
- - Marshalled bytes of [channel.PrivateIdentity].
+  - Marshalled bytes of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannelIdentity(long cmixID, NSError* _Nullable* _Nullable error);
 
@@ -2625,13 +2647,14 @@ FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsGetChannelDbCipherT
  * GetChannelInfo returns the info about a channel from its public description.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2639,23 +2662,24 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable p
  * GetChannelJSON returns the JSON of the channel for the given pretty print.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 Returns:
- - JSON of the [broadcast.Channel] object.
+  - JSON of the [broadcast.Channel] object.
 
 Example JSON of [broadcast.Channel]:
- {
-   "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
-   "Name": "My_Channel",
-   "Description": "Here is information about my channel.",
-   "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
-   "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
-   "RsaPubKeyLength": 5,
-   "RSASubPayloads": 1,
-   "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
-   "Level": 0
- }
+
+	{
+	  "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
+	  "Name": "My_Channel",
+	  "Description": "Here is information about my channel.",
+	  "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
+	  "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
+	  "RsaPubKeyLength": 5,
+	  "RSASubPayloads": 1,
+	  "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
+	  "Level": 0
+	}
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelJSON(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2762,10 +2786,10 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPubkeyFromContact(NSData* _Nullab
 from a bytes version and returns it JSON marshaled.
 
 Parameters:
- - marshaledPublic - Bytes of the public identity ([channel.Identity]).
+  - marshaledPublic - Bytes of the public identity ([channel.Identity]).
 
 Returns:
- - JSON of the constructed [channel.Identity].
+  - JSON of the constructed [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nullable marshaledPublic, NSError* _Nullable* _Nullable error);
 
@@ -2775,11 +2799,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nu
 ([channel.PrivateIdentity]).
 
 Parameters:
- - marshaledPrivate - Bytes of the private identity
-   (channel.PrivateIdentity]).
+  - marshaledPrivate - Bytes of the private identity
+    (channel.PrivateIdentity]).
 
 Returns:
- - JSON of the public [channel.Identity].
+  - JSON of the public [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentityFromPrivate(NSData* _Nullable marshaledPrivate, NSError* _Nullable* _Nullable error);
 
@@ -2790,11 +2814,11 @@ given channel ID.
 NOTE: This function is unsafe and only for debugging purposes only.
 
 Parameters:
- - cmixID - ID of [Cmix] object in tracker.
- - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
+  - cmixID - ID of [Cmix] object in tracker.
+  - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
 
 Returns:
- - The PEM file of the private key.
+  - The PEM file of the private key.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(long cmixID, NSString* _Nullable channelIdBase64, NSError* _Nullable* _Nullable error);
 
@@ -2803,15 +2827,16 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(lon
 If the URL is an invalid channel URL, an error is returned.
 
 Parameters:
- - url - The channel share URL.
+  - url - The channel share URL.
 
 Returns:
- - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
+  - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
 
 Possible returns:
- 0 = public channel
- 1 = private channel
- 2 = secret channel
+
+	0 = public channel
+	1 = private channel
+	2 = secret channel
  */
 FOUNDATION_EXPORT BOOL BindingsGetShareUrlType(NSString* _Nullable url, long* _Nullable ret0_, NSError* _Nullable* _Nullable error);
 
@@ -2825,11 +2850,11 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void);
 data.
 
 Parameters:
- - password - The password used to encrypt the identity.
- - data - The encrypted data.
+  - password - The password used to encrypt the identity.
+  - data - The encrypted data.
 
 Returns:
- - JSON of [channel.PrivateIdentity].
+  - JSON of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsImportPrivateIdentity(NSString* _Nullable password, NSData* _Nullable data, NSError* _Nullable* _Nullable error);
 
@@ -2909,12 +2934,12 @@ The channel manager should have previously been created with
 [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - storageTag - The storage tag associated with the previously created
-   channel manager and retrieved with [ChannelsManager.GetStorageTag].
- - event - An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - storageTag - The storage tag associated with the previously created
+    channel manager and retrieved with [ChannelsManager.GetStorageTag].
+  - event - An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsLoadChannelsManager(long cmixID, NSString* _Nullable storageTag, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
@@ -3014,12 +3039,12 @@ FOUNDATION_EXPORT BOOL BindingsMultiLookupUD(long e2eID, NSData* _Nullable udCon
  * NewChannelsDatabaseCipher constructs a ChannelDbCipher object.
 
 Parameters:
- - cmixID - The tracked [Cmix] object ID.
- - password - The password for storage. This should be the same password
-   passed into [NewCmix].
- - plaintTextBlockSize - The maximum size of a payload to be encrypted.
-   A payload passed into [ChannelDbCipher.Encrypt] that is larger than
-   plaintTextBlockSize will result in an error.
+  - cmixID - The tracked [Cmix] object ID.
+  - password - The password for storage. This should be the same password
+    passed into [NewCmix].
+  - plaintTextBlockSize - The maximum size of a payload to be encrypted.
+    A payload passed into [ChannelDbCipher.Encrypt] that is larger than
+    plaintTextBlockSize will result in an error.
  */
 FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsNewChannelsDatabaseCipher(long cmixID, NSData* _Nullable password, long plaintTextBlockSize, NSError* _Nullable* _Nullable error);
 
@@ -3033,12 +3058,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsNewChannelsManager(long cmixID, NSData* _Nullable privateIdentity, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings
index 91e00787b70d81643696ce73d3baf18e406901ed..a616579df5bfd7ae5d3cd41ec961af17d8ed9e79 100644
Binary files a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index dcf46cd4439785f4f904ea1f41277a66ed3e7db9..29b692cb622e84ca73a5d29b1f3954765ffb29db 100644
--- a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -470,7 +470,7 @@ be returned by this function. Any padding will be discarded within
 this function.
 
 Parameters:
- - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
+  - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
  */
 - (NSData* _Nullable)decrypt:(NSData* _Nullable)ciphertext error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -478,15 +478,28 @@ Parameters:
 done on the plaintext so all encrypted data looks uniform at rest.
 
 Parameters:
- - plaintext - The data to be encrypted. This must be smaller than the block
-   size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
-   return an error.
+  - plaintext - The data to be encrypted. This must be smaller than the block
+    size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
+    return an error.
  */
 - (NSData* _Nullable)encrypt:(NSData* _Nullable)plaintext error:(NSError* _Nullable* _Nullable)error;
 /**
  * GetID returns the ID for this ChannelDbCipher in the channelDbCipherTracker.
  */
 - (long)getID;
+/**
+ * MarshalJSON marshals the cipher into valid JSON. This function adheres to the
+json.Marshaler interface.
+ */
+- (NSData* _Nullable)marshalJSON:(NSError* _Nullable* _Nullable)error;
+/**
+ * UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the
+json.Unmarshaler interface.
+
+Note that this function does not transfer the internal RNG. Use
+NewCipherFromJSON to properly reconstruct a cipher from JSON.
+ */
+- (BOOL)unmarshalJSON:(NSData* _Nullable)data error:(NSError* _Nullable* _Nullable)error;
 @end
 
 /**
@@ -495,10 +508,11 @@ contains the public channel info formatted in pretty print and the private
 key for the channel in PEM format.
 
 Example JSON:
- {
-   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
-	  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
- }
+
+	 {
+	   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
+		  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
+	 }
  */
 @interface BindingsChannelGeneration : NSObject <goSeqRefInterface> {
 }
@@ -514,11 +528,12 @@ Example JSON:
  * ChannelInfo contains information about a channel.
 
 Example of ChannelInfo JSON:
- {
-   "Name": "Test Channel",
-   "Description": "This is a test channel",
-   "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
- }
+
+	{
+	  "Name": "Test Channel",
+	  "Description": "This is a test channel",
+	  "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
+	}
  */
 @interface BindingsChannelInfo : NSObject <goSeqRefInterface> {
 }
@@ -536,11 +551,12 @@ Example of ChannelInfo JSON:
 ChannelsManager's Send operations.
 
 JSON Example:
- {
-   "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
-   "Rounds":[1,5,9],
-   "EphId": 0
- }
+
+	{
+	  "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
+	  "Rounds":[1,5,9],
+	  "EphId": 0
+	}
  */
 @interface BindingsChannelSendReport : NSObject <goSeqRefInterface> {
 }
@@ -574,12 +590,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 - (nullable instancetype)init:(long)cmixID privateIdentity:(NSData* _Nullable)privateIdentity eventBuilder:(id<BindingsEventModelBuilder> _Nullable)eventBuilder;
 // skipped constructor ChannelsManager.NewChannelsManagerGoEventModel with unsupported parameter or return types
@@ -597,13 +613,14 @@ string.
  * GetChannels returns the IDs of all channels that have been joined.
 
 Returns:
- - []byte - A JSON marshalled list of IDs.
+  - []byte - A JSON marshalled list of IDs.
 
 JSON Example:
- {
-   "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
-   "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
- }
+
+	{
+	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
+	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
+	}
  */
 - (NSData* _Nullable)getChannels:(NSError* _Nullable* _Nullable)error;
 /**
@@ -639,14 +656,14 @@ calling [ChannelsManager.JoinChannelFromURL]. There is no enforcement for
 public URLs.
 
 Parameters:
- - cmixID - The tracked Cmix object ID.
- - host - The URL to append the channel info to.
- - maxUses - The maximum number of uses the link can be used (0 for
-   unlimited).
- - marshalledChanId - A marshalled channel ID ([id.ID]).
+  - cmixID - The tracked Cmix object ID.
+  - host - The URL to append the channel info to.
+  - maxUses - The maximum number of uses the link can be used (0 for
+    unlimited).
+  - marshalledChanId - A marshalled channel ID ([id.ID]).
 
 Returns:
- - JSON of ShareURL.
+  - JSON of ShareURL.
  */
 - (NSData* _Nullable)getShareURL:(long)cmixID host:(NSString* _Nullable)host maxUses:(long)maxUses marshalledChanId:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -658,14 +675,15 @@ Returns:
 been joined.
 
 Parameters:
- - channelPretty - A portable channel string. Should be received from
-   another user or generated via GenerateChannel.
+  - channelPretty - A portable channel string. Should be received from
+    another user or generated via GenerateChannel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 - (NSData* _Nullable)joinChannel:(NSString* _Nullable)channelPretty error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -673,7 +691,7 @@ Returns:
 channel was not previously joined.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)leaveChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -685,10 +703,10 @@ There can only be one handler per [channels.MessageType], and this will
 return an error on any re-registration.
 
 Parameters:
- - messageType - represents the [channels.MessageType] which will have a
-   registered listener.
- - listenerCb - the callback which will be executed when a channel message
-   of messageType is received.
+  - messageType - represents the [channels.MessageType] which will have a
+    registered listener.
+  - listenerCb - the callback which will be executed when a channel message
+    of messageType is received.
  */
 - (BOOL)registerReceiveHandler:(long)messageType listenerCb:(id<BindingsChannelMessageReceptionCallback> _Nullable)listenerCb error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -696,7 +714,7 @@ Parameters:
 memory (~3 weeks) over the event model.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)replayChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -707,23 +725,23 @@ before being sent over the wire, is too long, this will return an error. The
 message must be at most 510 bytes long.
 
 Parameters:
- - adminPrivateKey - The PEM-encoded admin RSA private key.
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. The message should be at most 510
-   bytes. This need not be of data type string, as the message could be a
-   specified format that the channel may recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - adminPrivateKey - The PEM-encoded admin RSA private key.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. The message should be at most 510
+    bytes. This need not be of data type string, as the message could be a
+    specified format that the channel may recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendAdminGeneric:(NSData* _Nullable)adminPrivateKey marshalledChanId:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -736,22 +754,22 @@ to send a payload of 802 bytes at minimum. The meaning of validUntil depends
 on the use case.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. This need not be of data type
-   string, as the message could be a specified format that the channel may
-   recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. This need not be of data type
+    string, as the message could be a specified format that the channel may
+    recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendGeneric:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -764,20 +782,20 @@ The message will auto delete validUntil after the round it is sent in,
 lasting forever if [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
-   empty, and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
+    empty, and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendMessage:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -787,19 +805,19 @@ be rejected otherwise.
 Users will drop the reaction if they do not recognize the reactTo message.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - reaction - The user's reaction. This should be a single emoji with no
-   other characters. As such, a Unicode string is expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
- and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - reaction - The user's reaction. This should be a single emoji with no
+    other characters. As such, a Unicode string is expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendReaction:(NSData* _Nullable)marshalledChanId reaction:(NSString* _Nullable)reaction messageToReactTo:(NSData* _Nullable)messageToReactTo cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -814,25 +832,25 @@ delete validUntil after the round it is sent in, lasting forever if
 [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendReply:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message messageToReactTo:(NSData* _Nullable)messageToReactTo leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -2130,17 +2148,18 @@ channel the message was sent to and the message itself. This is returned via
 the callback as JSON marshalled bytes.
 
 JSON Example:
- {
-   "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
-   "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
-   "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
-   "MessageType": 42,
-   "SenderUsername": "hunter2",
-   "Content": "YmFuX2JhZFVTZXI=",
-   "Timestamp": 1662502150335283000,
-   "Lease": 25,
-   "Rounds": [ 1, 4, 9],
- }
+
+	{
+	  "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+	  "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
+	  "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
+	  "MessageType": 42,
+	  "SenderUsername": "hunter2",
+	  "Content": "YmFuX2JhZFVTZXI=",
+	  "Timestamp": 1662502150335283000,
+	  "Lease": 25,
+	  "Rounds": [ 1, 4, 9],
+	}
  */
 @interface BindingsReceivedChannelMessageReport : NSObject <goSeqRefInterface> {
 }
@@ -2271,22 +2290,25 @@ JSON Example:
 channel's share URL and password, if it needs one.
 
 JSON example for a public channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
-   "password": ""
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
+	  "password": ""
+	}
 
 JSON example for a private channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
-   "password": "tribune gangrene labrador italics nutmeg process exhume legal"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
+	  "password": "tribune gangrene labrador italics nutmeg process exhume legal"
+	}
 
 JSON example for a secret channel:
- {
-   "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
-   "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
+	  "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
+	}
  */
 @interface BindingsShareURL : NSObject <goSeqRefInterface> {
 }
@@ -2481,11 +2503,11 @@ FOUNDATION_EXPORT BOOL BindingsAsyncRequestRestLike(long e2eID, NSData* _Nullabl
 and codeset version.
 
 Parameters:
- - pubKey - The Ed25519 public key.
- - codesetVersion - The version of the codeset used to generate the identity.
+  - pubKey - The Ed25519 public key.
+  - codesetVersion - The version of the codeset used to generate the identity.
 
 Returns:
- - JSON of [channel.Identity].
+  - JSON of [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsConstructIdentity(NSData* _Nullable pubKey, long codesetVersion, NSError* _Nullable* _Nullable error);
 
@@ -2512,12 +2534,12 @@ pretty print. This function can only be used for private or secret channel
 URLs. To get the privacy level of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
- - password - The password needed to decrypt the secret data in the URL.
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
+  - password - The password needed to decrypt the secret data in the URL.
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePrivateURL(NSString* _Nullable url, NSString* _Nullable password, NSError* _Nullable* _Nullable error);
 
@@ -2527,11 +2549,11 @@ function can only be used for public channel URLs. To get the privacy level
 of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePublicURL(NSString* _Nullable url, NSError* _Nullable* _Nullable error);
 
@@ -2561,33 +2583,33 @@ the admin. It is only for making new channels, not joining existing ones.
 It returns a pretty print of the channel and the private key.
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - name - The name of the new channel. The name must be between 3 and 24
-   characters inclusive. It can only include upper and lowercase unicode
-   letters, digits 0 through 9, and underscores (_). It cannot be changed
-   once a channel is created.
- - description - The description of a channel. The description is optional
-   but cannot be longer than 144 characters and can include all unicode
-   characters. It cannot be changed once a channel is created.
- - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
-   1 = private, and 2 = secret. Refer to the comment below for more
-   information.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - name - The name of the new channel. The name must be between 3 and 24
+    characters inclusive. It can only include upper and lowercase unicode
+    letters, digits 0 through 9, and underscores (_). It cannot be changed
+    once a channel is created.
+  - description - The description of a channel. The description is optional
+    but cannot be longer than 144 characters and can include all unicode
+    characters. It cannot be changed once a channel is created.
+  - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
+    1 = private, and 2 = secret. Refer to the comment below for more
+    information.
 
 Returns:
- - []byte - [ChannelGeneration] describes a generated channel. It contains
-   both the public channel info and the private key for the channel in PEM
-   format.
+  - []byte - [ChannelGeneration] describes a generated channel. It contains
+    both the public channel info and the private key for the channel in PEM
+    format.
 
 The [broadcast.PrivacyLevel] of a channel indicates the level of channel
 information revealed when sharing it via URL. For any channel besides public
 channels, the secret information is encrypted and a password is required to
 share and join a channel.
- - A privacy level of [broadcast.Public] reveals all the information
-   including the name, description, privacy level, public key and salt.
- - A privacy level of [broadcast.Private] reveals only the name and
-   description.
- - A privacy level of [broadcast.Secret] reveals nothing.
+  - A privacy level of [broadcast.Public] reveals all the information
+    including the name, description, privacy level, public key and salt.
+  - A privacy level of [broadcast.Private] reveals only the name and
+    description.
+  - A privacy level of [broadcast.Secret] reveals nothing.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSString* _Nullable name, NSString* _Nullable description, long privacyLevel, NSError* _Nullable* _Nullable error);
 
@@ -2597,11 +2619,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSStrin
 via [GetPublicChannelIdentityFromPrivate].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
 
 Returns:
- - Marshalled bytes of [channel.PrivateIdentity].
+  - Marshalled bytes of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannelIdentity(long cmixID, NSError* _Nullable* _Nullable error);
 
@@ -2625,13 +2647,14 @@ FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsGetChannelDbCipherT
  * GetChannelInfo returns the info about a channel from its public description.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2639,23 +2662,24 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable p
  * GetChannelJSON returns the JSON of the channel for the given pretty print.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 Returns:
- - JSON of the [broadcast.Channel] object.
+  - JSON of the [broadcast.Channel] object.
 
 Example JSON of [broadcast.Channel]:
- {
-   "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
-   "Name": "My_Channel",
-   "Description": "Here is information about my channel.",
-   "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
-   "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
-   "RsaPubKeyLength": 5,
-   "RSASubPayloads": 1,
-   "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
-   "Level": 0
- }
+
+	{
+	  "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
+	  "Name": "My_Channel",
+	  "Description": "Here is information about my channel.",
+	  "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
+	  "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
+	  "RsaPubKeyLength": 5,
+	  "RSASubPayloads": 1,
+	  "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
+	  "Level": 0
+	}
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelJSON(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2762,10 +2786,10 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPubkeyFromContact(NSData* _Nullab
 from a bytes version and returns it JSON marshaled.
 
 Parameters:
- - marshaledPublic - Bytes of the public identity ([channel.Identity]).
+  - marshaledPublic - Bytes of the public identity ([channel.Identity]).
 
 Returns:
- - JSON of the constructed [channel.Identity].
+  - JSON of the constructed [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nullable marshaledPublic, NSError* _Nullable* _Nullable error);
 
@@ -2775,11 +2799,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nu
 ([channel.PrivateIdentity]).
 
 Parameters:
- - marshaledPrivate - Bytes of the private identity
-   (channel.PrivateIdentity]).
+  - marshaledPrivate - Bytes of the private identity
+    (channel.PrivateIdentity]).
 
 Returns:
- - JSON of the public [channel.Identity].
+  - JSON of the public [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentityFromPrivate(NSData* _Nullable marshaledPrivate, NSError* _Nullable* _Nullable error);
 
@@ -2790,11 +2814,11 @@ given channel ID.
 NOTE: This function is unsafe and only for debugging purposes only.
 
 Parameters:
- - cmixID - ID of [Cmix] object in tracker.
- - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
+  - cmixID - ID of [Cmix] object in tracker.
+  - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
 
 Returns:
- - The PEM file of the private key.
+  - The PEM file of the private key.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(long cmixID, NSString* _Nullable channelIdBase64, NSError* _Nullable* _Nullable error);
 
@@ -2803,15 +2827,16 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(lon
 If the URL is an invalid channel URL, an error is returned.
 
 Parameters:
- - url - The channel share URL.
+  - url - The channel share URL.
 
 Returns:
- - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
+  - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
 
 Possible returns:
- 0 = public channel
- 1 = private channel
- 2 = secret channel
+
+	0 = public channel
+	1 = private channel
+	2 = secret channel
  */
 FOUNDATION_EXPORT BOOL BindingsGetShareUrlType(NSString* _Nullable url, long* _Nullable ret0_, NSError* _Nullable* _Nullable error);
 
@@ -2825,11 +2850,11 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void);
 data.
 
 Parameters:
- - password - The password used to encrypt the identity.
- - data - The encrypted data.
+  - password - The password used to encrypt the identity.
+  - data - The encrypted data.
 
 Returns:
- - JSON of [channel.PrivateIdentity].
+  - JSON of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsImportPrivateIdentity(NSString* _Nullable password, NSData* _Nullable data, NSError* _Nullable* _Nullable error);
 
@@ -2909,12 +2934,12 @@ The channel manager should have previously been created with
 [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - storageTag - The storage tag associated with the previously created
-   channel manager and retrieved with [ChannelsManager.GetStorageTag].
- - event - An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - storageTag - The storage tag associated with the previously created
+    channel manager and retrieved with [ChannelsManager.GetStorageTag].
+  - event - An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsLoadChannelsManager(long cmixID, NSString* _Nullable storageTag, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
@@ -3014,12 +3039,12 @@ FOUNDATION_EXPORT BOOL BindingsMultiLookupUD(long e2eID, NSData* _Nullable udCon
  * NewChannelsDatabaseCipher constructs a ChannelDbCipher object.
 
 Parameters:
- - cmixID - The tracked [Cmix] object ID.
- - password - The password for storage. This should be the same password
-   passed into [NewCmix].
- - plaintTextBlockSize - The maximum size of a payload to be encrypted.
-   A payload passed into [ChannelDbCipher.Encrypt] that is larger than
-   plaintTextBlockSize will result in an error.
+  - cmixID - The tracked [Cmix] object ID.
+  - password - The password for storage. This should be the same password
+    passed into [NewCmix].
+  - plaintTextBlockSize - The maximum size of a payload to be encrypted.
+    A payload passed into [ChannelDbCipher.Encrypt] that is larger than
+    plaintTextBlockSize will result in an error.
  */
 FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsNewChannelsDatabaseCipher(long cmixID, NSData* _Nullable password, long plaintTextBlockSize, NSError* _Nullable* _Nullable error);
 
@@ -3033,12 +3058,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsNewChannelsManager(long cmixID, NSData* _Nullable privateIdentity, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings
index ecd3142a1f4d3259c940950b488d71d38794a272..d2b36722d13be6fb98007d8fa0d902553ba0a5fe 100644
Binary files a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings differ
diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
index dcf46cd4439785f4f904ea1f41277a66ed3e7db9..29b692cb622e84ca73a5d29b1f3954765ffb29db 100644
--- a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
+++ b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h
@@ -470,7 +470,7 @@ be returned by this function. Any padding will be discarded within
 this function.
 
 Parameters:
- - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
+  - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt].
  */
 - (NSData* _Nullable)decrypt:(NSData* _Nullable)ciphertext error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -478,15 +478,28 @@ Parameters:
 done on the plaintext so all encrypted data looks uniform at rest.
 
 Parameters:
- - plaintext - The data to be encrypted. This must be smaller than the block
-   size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
-   return an error.
+  - plaintext - The data to be encrypted. This must be smaller than the block
+    size passed into [NewChannelsDatabaseCipher]. If it is larger, this will
+    return an error.
  */
 - (NSData* _Nullable)encrypt:(NSData* _Nullable)plaintext error:(NSError* _Nullable* _Nullable)error;
 /**
  * GetID returns the ID for this ChannelDbCipher in the channelDbCipherTracker.
  */
 - (long)getID;
+/**
+ * MarshalJSON marshals the cipher into valid JSON. This function adheres to the
+json.Marshaler interface.
+ */
+- (NSData* _Nullable)marshalJSON:(NSError* _Nullable* _Nullable)error;
+/**
+ * UnmarshalJSON unmarshalls JSON into the cipher. This function adheres to the
+json.Unmarshaler interface.
+
+Note that this function does not transfer the internal RNG. Use
+NewCipherFromJSON to properly reconstruct a cipher from JSON.
+ */
+- (BOOL)unmarshalJSON:(NSData* _Nullable)data error:(NSError* _Nullable* _Nullable)error;
 @end
 
 /**
@@ -495,10 +508,11 @@ contains the public channel info formatted in pretty print and the private
 key for the channel in PEM format.
 
 Example JSON:
- {
-   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
-	  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
- }
+
+	 {
+	   "Channel": "\u003cSpeakeasy-v3:name|description:desc|level:Public|created:1665489600000000000|secrets:zjHmrPPMDQ0tNSANjAmQfKhRpJIdJMU+Hz5hsZ+fVpk=|qozRNkADprqb38lsnU7WxCtGCq9OChlySCEgl4NHjI4=|2|328|7aZQAtuVjE84q4Z09iGytTSXfZj9NyTa6qBp0ueKjCI=\u003e",
+		  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMCYCAQACAwDVywIDAQABAgMAlVECAgDvAgIA5QICAJECAgCVAgIA1w==\n-----END RSA PRIVATE KEY-----"
+	 }
  */
 @interface BindingsChannelGeneration : NSObject <goSeqRefInterface> {
 }
@@ -514,11 +528,12 @@ Example JSON:
  * ChannelInfo contains information about a channel.
 
 Example of ChannelInfo JSON:
- {
-   "Name": "Test Channel",
-   "Description": "This is a test channel",
-   "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
- }
+
+	{
+	  "Name": "Test Channel",
+	  "Description": "This is a test channel",
+	  "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D",
+	}
  */
 @interface BindingsChannelInfo : NSObject <goSeqRefInterface> {
 }
@@ -536,11 +551,12 @@ Example of ChannelInfo JSON:
 ChannelsManager's Send operations.
 
 JSON Example:
- {
-   "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
-   "Rounds":[1,5,9],
-   "EphId": 0
- }
+
+	{
+	  "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=",
+	  "Rounds":[1,5,9],
+	  "EphId": 0
+	}
  */
 @interface BindingsChannelSendReport : NSObject <goSeqRefInterface> {
 }
@@ -574,12 +590,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 - (nullable instancetype)init:(long)cmixID privateIdentity:(NSData* _Nullable)privateIdentity eventBuilder:(id<BindingsEventModelBuilder> _Nullable)eventBuilder;
 // skipped constructor ChannelsManager.NewChannelsManagerGoEventModel with unsupported parameter or return types
@@ -597,13 +613,14 @@ string.
  * GetChannels returns the IDs of all channels that have been joined.
 
 Returns:
- - []byte - A JSON marshalled list of IDs.
+  - []byte - A JSON marshalled list of IDs.
 
 JSON Example:
- {
-   "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
-   "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
- }
+
+	{
+	  "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID",
+	  "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD"
+	}
  */
 - (NSData* _Nullable)getChannels:(NSError* _Nullable* _Nullable)error;
 /**
@@ -639,14 +656,14 @@ calling [ChannelsManager.JoinChannelFromURL]. There is no enforcement for
 public URLs.
 
 Parameters:
- - cmixID - The tracked Cmix object ID.
- - host - The URL to append the channel info to.
- - maxUses - The maximum number of uses the link can be used (0 for
-   unlimited).
- - marshalledChanId - A marshalled channel ID ([id.ID]).
+  - cmixID - The tracked Cmix object ID.
+  - host - The URL to append the channel info to.
+  - maxUses - The maximum number of uses the link can be used (0 for
+    unlimited).
+  - marshalledChanId - A marshalled channel ID ([id.ID]).
 
 Returns:
- - JSON of ShareURL.
+  - JSON of ShareURL.
  */
 - (NSData* _Nullable)getShareURL:(long)cmixID host:(NSString* _Nullable)host maxUses:(long)maxUses marshalledChanId:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -658,14 +675,15 @@ Returns:
 been joined.
 
 Parameters:
- - channelPretty - A portable channel string. Should be received from
-   another user or generated via GenerateChannel.
+  - channelPretty - A portable channel string. Should be received from
+    another user or generated via GenerateChannel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 - (NSData* _Nullable)joinChannel:(NSString* _Nullable)channelPretty error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -673,7 +691,7 @@ Returns:
 channel was not previously joined.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)leaveChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -685,10 +703,10 @@ There can only be one handler per [channels.MessageType], and this will
 return an error on any re-registration.
 
 Parameters:
- - messageType - represents the [channels.MessageType] which will have a
-   registered listener.
- - listenerCb - the callback which will be executed when a channel message
-   of messageType is received.
+  - messageType - represents the [channels.MessageType] which will have a
+    registered listener.
+  - listenerCb - the callback which will be executed when a channel message
+    of messageType is received.
  */
 - (BOOL)registerReceiveHandler:(long)messageType listenerCb:(id<BindingsChannelMessageReceptionCallback> _Nullable)listenerCb error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -696,7 +714,7 @@ Parameters:
 memory (~3 weeks) over the event model.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
  */
 - (BOOL)replayChannel:(NSData* _Nullable)marshalledChanId error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -707,23 +725,23 @@ before being sent over the wire, is too long, this will return an error. The
 message must be at most 510 bytes long.
 
 Parameters:
- - adminPrivateKey - The PEM-encoded admin RSA private key.
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. The message should be at most 510
-   bytes. This need not be of data type string, as the message could be a
-   specified format that the channel may recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - adminPrivateKey - The PEM-encoded admin RSA private key.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. The message should be at most 510
+    bytes. This need not be of data type string, as the message could be a
+    specified format that the channel may recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendAdminGeneric:(NSData* _Nullable)adminPrivateKey marshalledChanId:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -736,22 +754,22 @@ to send a payload of 802 bytes at minimum. The meaning of validUntil depends
 on the use case.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - messageType - The message type of the message. This will be a valid
-   [channels.MessageType].
- - message - The contents of the message. This need not be of data type
-   string, as the message could be a specified format that the channel may
-   recognize.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - messageType - The message type of the message. This will be a valid
+    [channels.MessageType].
+  - message - The contents of the message. This need not be of data type
+    string, as the message could be a specified format that the channel may
+    recognize.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendGeneric:(NSData* _Nullable)marshalledChanId messageType:(long)messageType message:(NSData* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -764,20 +782,20 @@ The message will auto delete validUntil after the round it is sent in,
 lasting forever if [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
-   empty, and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be
+    empty, and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendMessage:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -787,19 +805,19 @@ be rejected otherwise.
 Users will drop the reaction if they do not recognize the reactTo message.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - reaction - The user's reaction. This should be a single emoji with no
-   other characters. As such, a Unicode string is expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
- and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - reaction - The user's reaction. This should be a single emoji with no
+    other characters. As such, a Unicode string is expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport.
+  - []byte - A JSON marshalled ChannelSendReport.
  */
 - (NSData* _Nullable)sendReaction:(NSData* _Nullable)marshalledChanId reaction:(NSString* _Nullable)reaction messageToReactTo:(NSData* _Nullable)messageToReactTo cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -814,25 +832,25 @@ delete validUntil after the round it is sent in, lasting forever if
 [channels.ValidForever] is used.
 
 Parameters:
- - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
- - message - The contents of the message. The message should be at most 510
-   bytes. This is expected to be Unicode, and thus a string data type is
-   expected.
- - messageToReactTo - The marshalled [channel.MessageID] of the message you
-   wish to reply to. This may be found in the ChannelSendReport if replying
-   to your own. Alternatively, if reacting to another user's message, you may
-   retrieve it via the ChannelMessageReceptionCallback registered using
-   RegisterReceiveHandler.
- - leaseTimeMS - The lease of the message. This will be how long the message
-   is valid until, in milliseconds. As per the channels.Manager
-   documentation, this has different meanings depending on the use case.
-   These use cases may be generic enough that they will not be enumerated
-   here.
- - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
-   and GetDefaultCMixParams will be used internally.
+  - marshalledChanId - A JSON marshalled channel ID ([id.ID]).
+  - message - The contents of the message. The message should be at most 510
+    bytes. This is expected to be Unicode, and thus a string data type is
+    expected.
+  - messageToReactTo - The marshalled [channel.MessageID] of the message you
+    wish to reply to. This may be found in the ChannelSendReport if replying
+    to your own. Alternatively, if reacting to another user's message, you may
+    retrieve it via the ChannelMessageReceptionCallback registered using
+    RegisterReceiveHandler.
+  - leaseTimeMS - The lease of the message. This will be how long the message
+    is valid until, in milliseconds. As per the channels.Manager
+    documentation, this has different meanings depending on the use case.
+    These use cases may be generic enough that they will not be enumerated
+    here.
+  - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty,
+    and GetDefaultCMixParams will be used internally.
 
 Returns:
- - []byte - A JSON marshalled ChannelSendReport
+  - []byte - A JSON marshalled ChannelSendReport
  */
 - (NSData* _Nullable)sendReply:(NSData* _Nullable)marshalledChanId message:(NSString* _Nullable)message messageToReactTo:(NSData* _Nullable)messageToReactTo leaseTimeMS:(int64_t)leaseTimeMS cmixParamsJSON:(NSData* _Nullable)cmixParamsJSON error:(NSError* _Nullable* _Nullable)error;
 /**
@@ -2130,17 +2148,18 @@ channel the message was sent to and the message itself. This is returned via
 the callback as JSON marshalled bytes.
 
 JSON Example:
- {
-   "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
-   "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
-   "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
-   "MessageType": 42,
-   "SenderUsername": "hunter2",
-   "Content": "YmFuX2JhZFVTZXI=",
-   "Timestamp": 1662502150335283000,
-   "Lease": 25,
-   "Rounds": [ 1, 4, 9],
- }
+
+	{
+	  "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+	  "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=",
+	  "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=",
+	  "MessageType": 42,
+	  "SenderUsername": "hunter2",
+	  "Content": "YmFuX2JhZFVTZXI=",
+	  "Timestamp": 1662502150335283000,
+	  "Lease": 25,
+	  "Rounds": [ 1, 4, 9],
+	}
  */
 @interface BindingsReceivedChannelMessageReport : NSObject <goSeqRefInterface> {
 }
@@ -2271,22 +2290,25 @@ JSON Example:
 channel's share URL and password, if it needs one.
 
 JSON example for a public channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
-   "password": ""
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&2Level=Public&3Created=1665489600000000000&e=%2FWNZvuHPuv%2Bx23XbZXVNzCi7y8rUSxkh75MpR9UrsCo%3D&k=ddX1CH52xH%2F%2Fb6lKrbvDghdSmCQr90ktsOAZ%2FrhEonI%3D&l=2&m=0&p=328&s=%2FD%2FoQP2mio3XAWfhmWF0xmZrpj4nAsb9JLXj%2B0Mzq9Y%3D&v=1",
+	  "password": ""
+	}
 
 JSON example for a private channel:
- {
-   "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
-   "password": "tribune gangrene labrador italics nutmeg process exhume legal"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?0Name=name&1Description=desc&3Created=1665489600000000000&d=5AZQirb%2FYrmUITLn%2FFzCaGek1APfJnd2q0KwORGj%2BnbGg26kTShG6cfD3w6c%2BA3RDzxuKDSDN0zS4n1LbjiGe0KYdb8eJVeyRZtld516hfojNDXNAwZq8zbeZy4jjbF627fcLHRNS%2FaII4uJ5UB3gLUeBeZGraaybCCu3FIj1N4RbcJ5cQgT7hBf93bHmJc%3D&m=0&v=1",
+	  "password": "tribune gangrene labrador italics nutmeg process exhume legal"
+	}
 
 JSON example for a secret channel:
- {
-   "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
-   "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
- }
+
+	{
+	  "url": "https://internet.speakeasy.tech/?d=w5evLthm%2Fq2j11g6PPtV0QoLaAqNCIER0OqxhxL%2FhpGVJI0057ZPgGBrKoJNE1%2FdoVuU35%2FhohuW%2BWvGlx6IuHoN6mDj0HfNj6Lo%2B8GwIaD6jOEwUcH%2FMKGsKnoqFsMaMPd5gXYgdHvA8l5SRe0gSCVqGKUaG6JgL%2FDu4iyjY7v4ykwZdQ7soWOcBLHDixGEkVLpwsCrPVHkT2K0W6gV74GIrQ%3D%3D&m=0&v=1",
+	  "password": "frenzy contort staple thicket consuming affiliate scion demeanor"
+	}
  */
 @interface BindingsShareURL : NSObject <goSeqRefInterface> {
 }
@@ -2481,11 +2503,11 @@ FOUNDATION_EXPORT BOOL BindingsAsyncRequestRestLike(long e2eID, NSData* _Nullabl
 and codeset version.
 
 Parameters:
- - pubKey - The Ed25519 public key.
- - codesetVersion - The version of the codeset used to generate the identity.
+  - pubKey - The Ed25519 public key.
+  - codesetVersion - The version of the codeset used to generate the identity.
 
 Returns:
- - JSON of [channel.Identity].
+  - JSON of [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsConstructIdentity(NSData* _Nullable pubKey, long codesetVersion, NSError* _Nullable* _Nullable error);
 
@@ -2512,12 +2534,12 @@ pretty print. This function can only be used for private or secret channel
 URLs. To get the privacy level of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
- - password - The password needed to decrypt the secret data in the URL.
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
+  - password - The password needed to decrypt the secret data in the URL.
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePrivateURL(NSString* _Nullable url, NSString* _Nullable password, NSError* _Nullable* _Nullable error);
 
@@ -2527,11 +2549,11 @@ function can only be used for public channel URLs. To get the privacy level
 of a channel URL, use [GetShareUrlType].
 
 Parameters:
- - url - The channel's share URL. Should be received from another user or
-   generated via [GetShareURL].
+  - url - The channel's share URL. Should be received from another user or
+    generated via [GetShareURL].
 
 Returns:
- - The channel pretty print.
+  - The channel pretty print.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsDecodePublicURL(NSString* _Nullable url, NSError* _Nullable* _Nullable error);
 
@@ -2561,33 +2583,33 @@ the admin. It is only for making new channels, not joining existing ones.
 It returns a pretty print of the channel and the private key.
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - name - The name of the new channel. The name must be between 3 and 24
-   characters inclusive. It can only include upper and lowercase unicode
-   letters, digits 0 through 9, and underscores (_). It cannot be changed
-   once a channel is created.
- - description - The description of a channel. The description is optional
-   but cannot be longer than 144 characters and can include all unicode
-   characters. It cannot be changed once a channel is created.
- - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
-   1 = private, and 2 = secret. Refer to the comment below for more
-   information.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - name - The name of the new channel. The name must be between 3 and 24
+    characters inclusive. It can only include upper and lowercase unicode
+    letters, digits 0 through 9, and underscores (_). It cannot be changed
+    once a channel is created.
+  - description - The description of a channel. The description is optional
+    but cannot be longer than 144 characters and can include all unicode
+    characters. It cannot be changed once a channel is created.
+  - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public,
+    1 = private, and 2 = secret. Refer to the comment below for more
+    information.
 
 Returns:
- - []byte - [ChannelGeneration] describes a generated channel. It contains
-   both the public channel info and the private key for the channel in PEM
-   format.
+  - []byte - [ChannelGeneration] describes a generated channel. It contains
+    both the public channel info and the private key for the channel in PEM
+    format.
 
 The [broadcast.PrivacyLevel] of a channel indicates the level of channel
 information revealed when sharing it via URL. For any channel besides public
 channels, the secret information is encrypted and a password is required to
 share and join a channel.
- - A privacy level of [broadcast.Public] reveals all the information
-   including the name, description, privacy level, public key and salt.
- - A privacy level of [broadcast.Private] reveals only the name and
-   description.
- - A privacy level of [broadcast.Secret] reveals nothing.
+  - A privacy level of [broadcast.Public] reveals all the information
+    including the name, description, privacy level, public key and salt.
+  - A privacy level of [broadcast.Private] reveals only the name and
+    description.
+  - A privacy level of [broadcast.Secret] reveals nothing.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSString* _Nullable name, NSString* _Nullable description, long privacyLevel, NSError* _Nullable* _Nullable error);
 
@@ -2597,11 +2619,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannel(long cmixID, NSStrin
 via [GetPublicChannelIdentityFromPrivate].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
 
 Returns:
- - Marshalled bytes of [channel.PrivateIdentity].
+  - Marshalled bytes of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGenerateChannelIdentity(long cmixID, NSError* _Nullable* _Nullable error);
 
@@ -2625,13 +2647,14 @@ FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsGetChannelDbCipherT
  * GetChannelInfo returns the info about a channel from its public description.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 The pretty print will be of the format:
- <Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
+
+	<Speakeasy-v3:Test_Channel|description:Channel description.|level:Public|created:1666718081766741100|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=>
 
 Returns:
- - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
+  - []byte - JSON of [ChannelInfo], which describes all relevant channel info.
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2639,23 +2662,24 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelInfo(NSString* _Nullable p
  * GetChannelJSON returns the JSON of the channel for the given pretty print.
 
 Parameters:
- - prettyPrint - The pretty print of the channel.
+  - prettyPrint - The pretty print of the channel.
 
 Returns:
- - JSON of the [broadcast.Channel] object.
+  - JSON of the [broadcast.Channel] object.
 
 Example JSON of [broadcast.Channel]:
- {
-   "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
-   "Name": "My_Channel",
-   "Description": "Here is information about my channel.",
-   "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
-   "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
-   "RsaPubKeyLength": 5,
-   "RSASubPayloads": 1,
-   "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
-   "Level": 0
- }
+
+	{
+	  "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD",
+	  "Name": "My_Channel",
+	  "Description": "Here is information about my channel.",
+	  "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=",
+	  "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=",
+	  "RsaPubKeyLength": 5,
+	  "RSASubPayloads": 1,
+	  "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=",
+	  "Level": 0
+	}
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetChannelJSON(NSString* _Nullable prettyPrint, NSError* _Nullable* _Nullable error);
 
@@ -2762,10 +2786,10 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPubkeyFromContact(NSData* _Nullab
 from a bytes version and returns it JSON marshaled.
 
 Parameters:
- - marshaledPublic - Bytes of the public identity ([channel.Identity]).
+  - marshaledPublic - Bytes of the public identity ([channel.Identity]).
 
 Returns:
- - JSON of the constructed [channel.Identity].
+  - JSON of the constructed [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nullable marshaledPublic, NSError* _Nullable* _Nullable error);
 
@@ -2775,11 +2799,11 @@ FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentity(NSData* _Nu
 ([channel.PrivateIdentity]).
 
 Parameters:
- - marshaledPrivate - Bytes of the private identity
-   (channel.PrivateIdentity]).
+  - marshaledPrivate - Bytes of the private identity
+    (channel.PrivateIdentity]).
 
 Returns:
- - JSON of the public [channel.Identity].
+  - JSON of the public [channel.Identity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsGetPublicChannelIdentityFromPrivate(NSData* _Nullable marshaledPrivate, NSError* _Nullable* _Nullable error);
 
@@ -2790,11 +2814,11 @@ given channel ID.
 NOTE: This function is unsafe and only for debugging purposes only.
 
 Parameters:
- - cmixID - ID of [Cmix] object in tracker.
- - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
+  - cmixID - ID of [Cmix] object in tracker.
+  - channelIdBase64 - The [id.ID] of the channel in base 64 encoding.
 
 Returns:
- - The PEM file of the private key.
+  - The PEM file of the private key.
  */
 FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(long cmixID, NSString* _Nullable channelIdBase64, NSError* _Nullable* _Nullable error);
 
@@ -2803,15 +2827,16 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetSavedChannelPrivateKeyUNSAFE(lon
 If the URL is an invalid channel URL, an error is returned.
 
 Parameters:
- - url - The channel share URL.
+  - url - The channel share URL.
 
 Returns:
- - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
+  - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below.
 
 Possible returns:
- 0 = public channel
- 1 = private channel
- 2 = secret channel
+
+	0 = public channel
+	1 = private channel
+	2 = secret channel
  */
 FOUNDATION_EXPORT BOOL BindingsGetShareUrlType(NSString* _Nullable url, long* _Nullable ret0_, NSError* _Nullable* _Nullable error);
 
@@ -2825,11 +2850,11 @@ FOUNDATION_EXPORT NSString* _Nonnull BindingsGetVersion(void);
 data.
 
 Parameters:
- - password - The password used to encrypt the identity.
- - data - The encrypted data.
+  - password - The password used to encrypt the identity.
+  - data - The encrypted data.
 
 Returns:
- - JSON of [channel.PrivateIdentity].
+  - JSON of [channel.PrivateIdentity].
  */
 FOUNDATION_EXPORT NSData* _Nullable BindingsImportPrivateIdentity(NSString* _Nullable password, NSData* _Nullable data, NSError* _Nullable* _Nullable error);
 
@@ -2909,12 +2934,12 @@ The channel manager should have previously been created with
 [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - storageTag - The storage tag associated with the previously created
-   channel manager and retrieved with [ChannelsManager.GetStorageTag].
- - event - An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - storageTag - The storage tag associated with the previously created
+    channel manager and retrieved with [ChannelsManager.GetStorageTag].
+  - event - An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsLoadChannelsManager(long cmixID, NSString* _Nullable storageTag, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
@@ -3014,12 +3039,12 @@ FOUNDATION_EXPORT BOOL BindingsMultiLookupUD(long e2eID, NSData* _Nullable udCon
  * NewChannelsDatabaseCipher constructs a ChannelDbCipher object.
 
 Parameters:
- - cmixID - The tracked [Cmix] object ID.
- - password - The password for storage. This should be the same password
-   passed into [NewCmix].
- - plaintTextBlockSize - The maximum size of a payload to be encrypted.
-   A payload passed into [ChannelDbCipher.Encrypt] that is larger than
-   plaintTextBlockSize will result in an error.
+  - cmixID - The tracked [Cmix] object ID.
+  - password - The password for storage. This should be the same password
+    passed into [NewCmix].
+  - plaintTextBlockSize - The maximum size of a payload to be encrypted.
+    A payload passed into [ChannelDbCipher.Encrypt] that is larger than
+    plaintTextBlockSize will result in an error.
  */
 FOUNDATION_EXPORT BindingsChannelDbCipher* _Nullable BindingsNewChannelsDatabaseCipher(long cmixID, NSData* _Nullable password, long plaintTextBlockSize, NSError* _Nullable* _Nullable error);
 
@@ -3033,12 +3058,12 @@ reload this channel manager, use [LoadChannelsManager], passing in the
 storage tag retrieved by [ChannelsManager.GetStorageTag].
 
 Parameters:
- - cmixID - The tracked Cmix object ID. This can be retrieved using
-   [Cmix.GetID].
- - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
-   that is generated by [GenerateChannelIdentity].
- - event -  An interface that contains a function that initialises and returns
-   the event model that is bindings-compatible.
+  - cmixID - The tracked Cmix object ID. This can be retrieved using
+    [Cmix.GetID].
+  - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity])
+    that is generated by [GenerateChannelIdentity].
+  - event -  An interface that contains a function that initialises and returns
+    the event model that is bindings-compatible.
  */
 FOUNDATION_EXPORT BindingsChannelsManager* _Nullable BindingsNewChannelsManager(long cmixID, NSData* _Nullable privateIdentity, id<BindingsEventModelBuilder> _Nullable eventBuilder, NSError* _Nullable* _Nullable error);
 
diff --git a/Package.resolved b/Package.resolved
index d7760fa278975208f6d8594eac3dbd5c29784cbe..ec74561fcbec29a3917ef111bf0afe9fbcd9c674 100644
--- a/Package.resolved
+++ b/Package.resolved
@@ -14,8 +14,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/swift-custom-dump.git",
       "state" : {
-        "revision" : "819d9d370cd721c9d87671e29d947279292e4541",
-        "version" : "0.6.0"
+        "revision" : "ead7d30cc224c3642c150b546f4f1080d1c411a8",
+        "version" : "0.6.1"
       }
     },
     {
@@ -32,8 +32,8 @@
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
       "state" : {
-        "revision" : "16e6409ee82e1b81390bdffbf217b9c08ab32784",
-        "version" : "0.5.0"
+        "revision" : "5a5457a744239896e9b0b03a8e1a5069c3e7b91f",
+        "version" : "0.6.0"
       }
     }
   ],
diff --git a/Package.swift b/Package.swift
index b1d78d2adb57ef25fa0056fae812cc04b853cf7e..d5844ed406969435ec67a1b268e042f87fa9e146 100644
--- a/Package.swift
+++ b/Package.swift
@@ -21,11 +21,11 @@ let package = Package(
   dependencies: [
     .package(
       url: "https://github.com/pointfreeco/swift-custom-dump.git",
-      .upToNextMajor(from: "0.6.0")
+      .upToNextMajor(from: "0.6.1")
     ),
     .package(
       url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git",
-      .upToNextMajor(from: "0.5.0")
+      .upToNextMajor(from: "0.6.0")
     ),
     .package(
       url: "https://github.com/kishikawakatsumi/KeychainAccess.git",
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsGroupChatRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsGroupChatRunning.swift
new file mode 100644
index 0000000000000000000000000000000000000000..ce177d5d8c2d9858b6218d540131e4bb4c103b27
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsGroupChatRunning.swift
@@ -0,0 +1,21 @@
+import XCTestDynamicOverlay
+
+public struct MessengerIsGroupChatRunning {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsGroupChatRunning {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsGroupChatRunning {
+    MessengerIsGroupChatRunning { env.groupChat.get() != nil }
+  }
+}
+
+extension MessengerIsGroupChatRunning {
+  public static let unimplemented = MessengerIsGroupChatRunning(
+    run: XCTestDynamicOverlay.unimplemented("\(Self.self)", placeholder: false)
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index 44b213cb05805352ecffd0d7e07ca6f4880a42a7..aefef17da05277b628e9fd7f5f2b5b356566b890 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -51,6 +51,7 @@ public struct Messenger {
   public var getNotificationReports: MessengerGetNotificationReports
   public var registerGroupRequestHandler: MessengerRegisterGroupRequestHandler
   public var registerGroupChatProcessor: MessengerRegisterGroupChatProcessor
+  public var isGroupChatRunning: MessengerIsGroupChatRunning
   public var startGroupChat: MessengerStartGroupChat
 }
 
@@ -107,6 +108,7 @@ extension Messenger {
       getNotificationReports: .live(env),
       registerGroupRequestHandler: .live(env),
       registerGroupChatProcessor: .live(env),
+      isGroupChatRunning: .live(env),
       startGroupChat: .live(env)
     )
   }
@@ -164,6 +166,7 @@ extension Messenger {
     getNotificationReports: .unimplemented,
     registerGroupRequestHandler: .unimplemented,
     registerGroupChatProcessor: .unimplemented,
+    isGroupChatRunning: .unimplemented,
     startGroupChat: .unimplemented
   )
 }
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsGroupChatRunningTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsGroupChatRunningTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..364c12039396687de0520ec9465f3671b810e564
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsGroupChatRunningTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsGroupChatRunningTests: XCTestCase {
+  func testIsRunning() {
+    var env: MessengerEnvironment = .unimplemented
+    env.groupChat.get = { .unimplemented }
+    let isRunning: MessengerIsGroupChatRunning = .live(env)
+
+    XCTAssertTrue(isRunning())
+  }
+
+  func testIsNotRunning() {
+    var env: MessengerEnvironment = .unimplemented
+    env.groupChat.get = { nil }
+    let isRunning: MessengerIsGroupChatRunning = .live(env)
+
+    XCTAssertFalse(isRunning())
+  }
+}