From 5914c232b6f919e6085d17ff780c6dd125cefbd5 Mon Sep 17 00:00:00 2001
From: Mohammed S <shoaib@push.org>
Date: Thu, 23 Nov 2023 17:16:02 +0530
Subject: [PATCH] 853 improvement proposal add read only low functions to high
 level functions (#873)

* fix: PUSH API read only mode

* fix: fix error message
---
 .../pushNotification/pushNotificationBase.ts  |   3 +-
 packages/restapi/src/lib/pushapi/PushAPI.ts   | 141 ++++--
 packages/restapi/src/lib/pushapi/chat.ts      |  51 ++-
 .../restapi/src/lib/pushapi/encryption.ts     |  47 +-
 packages/restapi/src/lib/pushapi/profile.ts   |   6 +-
 .../restapi/src/lib/pushstream/PushStream.ts  |  25 +-
 .../restapi/tests/lib/pushapi/chat.test.ts    |  55 +++
 .../restapi/tests/lib/pushapi/profile.test.ts |  25 +
 .../tests/lib/pushstream/initialize.test.ts   | 433 ++++++++++++++++++
 9 files changed, 704 insertions(+), 82 deletions(-)

diff --git a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts
index 4748d4a8e..2bbfc8557 100644
--- a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts
+++ b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts
@@ -25,6 +25,7 @@ import {
   validateCAIP,
 } from '../helpers';
 import * as PUSH_ALIAS from '../alias';
+import { PushAPI } from '../pushapi/PushAPI';
 
 // ERROR CONSTANTS
 const ERROR_ACCOUNT_NEEDED = 'Account is required';
@@ -113,7 +114,7 @@ export class PushNotificationBaseClass {
 
   // checks if the signer object is supplied
   protected checkSignerObjectExists() {
-    if (!this.signer) throw new Error(ERROR_SIGNER_NEEDED);
+    if (!this.signer) throw new Error(PushAPI.ensureSignerMessage());
     return true;
   }
 
diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts
index 5201b5ef5..33bfad85b 100644
--- a/packages/restapi/src/lib/pushapi/PushAPI.ts
+++ b/packages/restapi/src/lib/pushapi/PushAPI.ts
@@ -17,10 +17,11 @@ import {
 } from '../pushstream/pushStreamTypes';
 
 export class PushAPI {
-  private signer: SignerType;
+  private signer?: SignerType;
+  private readMode: boolean;
   private account: string;
-  private decryptedPgpPvtKey: string;
-  private pgpPublicKey: string;
+  private decryptedPgpPvtKey?: string;
+  private pgpPublicKey?: string;
   private env: ENV;
   private progressHook?: (progress: ProgressHookType) => void;
 
@@ -34,14 +35,16 @@ export class PushAPI {
   public notification!: Notification;
 
   private constructor(
-    signer: SignerType,
     env: ENV,
     account: string,
-    decryptedPgpPvtKey: string,
-    pgpPublicKey: string,
+    readMode: boolean,
+    decryptedPgpPvtKey?: string,
+    pgpPublicKey?: string,
+    signer?: SignerType,
     progressHook?: (progress: ProgressHookType) => void
   ) {
     this.signer = signer;
+    this.readMode = readMode;
     this.env = env;
     this.account = account;
     this.decryptedPgpPvtKey = decryptedPgpPvtKey;
@@ -53,33 +56,61 @@ export class PushAPI {
     // Initialize the instances of the four classes
     this.chat = new Chat(
       this.account,
-      this.decryptedPgpPvtKey,
       this.env,
+      this.decryptedPgpPvtKey,
       this.signer,
       this.progressHook
     );
     this.profile = new Profile(
       this.account,
-      this.decryptedPgpPvtKey,
       this.env,
+      this.decryptedPgpPvtKey,
       this.progressHook
     );
     this.encryption = new Encryption(
       this.account,
+      this.env,
       this.decryptedPgpPvtKey,
       this.pgpPublicKey,
-      this.env,
       this.signer,
       this.progressHook
     );
     this.user = new User(this.account, this.env);
   }
-
+  // Overloaded initialize method signatures
   static async initialize(
-    signer: SignerType,
+    signer?: SignerType,
     options?: PushAPIInitializeProps
-  ): Promise<PushAPI> {
+  ): Promise<PushAPI>;
+  static async initialize(options?: PushAPIInitializeProps): Promise<PushAPI>;
+
+  static async initialize(...args: any[]): Promise<PushAPI> {
     try {
+      let signer: SignerType | undefined;
+      let options: PushAPIInitializeProps | undefined;
+
+      if (
+        args.length === 1 &&
+        typeof args[0] === 'object' &&
+        'account' in args[0]
+      ) {
+        // Single options object provided
+        options = args[0];
+      } else if (args.length === 1) {
+        // Only signer provided
+        [signer] = args;
+      } else if (args.length === 2) {
+        // Separate signer and options arguments provided
+        [signer, options] = args;
+      } else {
+        // Handle other cases or throw an error
+        throw new Error('Invalid arguments provided to initialize method.');
+      }
+
+      if (!signer && !options?.account) {
+        throw new Error("Either 'signer' or 'account' must be provided.");
+      }
+
       // Default options
       const defaultOptions: PushAPIInitializeProps = {
         env: ENV.STAGING,
@@ -101,17 +132,29 @@ export class PushAPI {
             : defaultOptions.autoUpgrade,
       };
 
+      const readMode = !signer;
+
       // Get account
       // Derives account from signer if not provided
-      const derivedAccount = await getAccountAddress(
-        getWallet({
-          account: settings.account as string | null,
-          signer: signer,
-        })
-      );
 
-      let decryptedPGPPrivateKey: string;
-      let pgpPublicKey: string;
+      let derivedAccount;
+      if (signer) {
+        derivedAccount = await getAccountAddress(
+          getWallet({
+            account: settings.account as string | null,
+            signer: signer,
+          })
+        );
+      } else {
+        derivedAccount = options?.account;
+      }
+
+      if (!derivedAccount) {
+        throw new Error('Account could not be derived.');
+      }
+
+      let decryptedPGPPrivateKey;
+      let pgpPublicKey;
 
       /**
        * Decrypt PGP private key
@@ -122,37 +165,41 @@ export class PushAPI {
         account: derivedAccount,
         env: settings.env,
       });
-      if (user && user.encryptedPrivateKey) {
-        decryptedPGPPrivateKey = await PUSH_CHAT.decryptPGPKey({
-          encryptedPGPPrivateKey: user.encryptedPrivateKey,
-          signer: signer,
-          toUpgrade: settings.autoUpgrade,
-          additionalMeta: settings.versionMeta,
-          progressHook: settings.progressHook,
-          env: settings.env,
-        });
-        pgpPublicKey = user.publicKey;
-      } else {
-        const newUser = await PUSH_USER.create({
-          env: settings.env,
-          account: derivedAccount,
-          signer,
-          version: settings.version,
-          additionalMeta: settings.versionMeta,
-          origin: settings.origin,
-          progressHook: settings.progressHook,
-        });
-        decryptedPGPPrivateKey = newUser.decryptedPrivateKey as string;
-        pgpPublicKey = newUser.publicKey;
+
+      if (!readMode) {
+        if (user && user.encryptedPrivateKey) {
+          decryptedPGPPrivateKey = await PUSH_CHAT.decryptPGPKey({
+            encryptedPGPPrivateKey: user.encryptedPrivateKey,
+            signer: signer,
+            toUpgrade: settings.autoUpgrade,
+            additionalMeta: settings.versionMeta,
+            progressHook: settings.progressHook,
+            env: settings.env,
+          });
+          pgpPublicKey = user.publicKey;
+        } else {
+          const newUser = await PUSH_USER.create({
+            env: settings.env,
+            account: derivedAccount,
+            signer,
+            version: settings.version,
+            additionalMeta: settings.versionMeta,
+            origin: settings.origin,
+            progressHook: settings.progressHook,
+          });
+          decryptedPGPPrivateKey = newUser.decryptedPrivateKey as string;
+          pgpPublicKey = newUser.publicKey;
+        }
       }
 
       // Initialize PushAPI instance
       const api = new PushAPI(
-        signer,
         settings.env as ENV,
         derivedAccount,
+        readMode,
         decryptedPGPPrivateKey,
         pgpPublicKey,
+        signer,
         settings.progressHook
       );
 
@@ -173,11 +220,11 @@ export class PushAPI {
 
     this.stream = await PushStream.initialize(
       this.account,
-      this.decryptedPgpPvtKey,
-      this.signer,
       listen,
       this.env,
+      this.decryptedPgpPvtKey,
       this.progressHook,
+      this.signer,
       options
     );
 
@@ -190,4 +237,8 @@ export class PushAPI {
       env: this.env,
     });
   }
+
+  static ensureSignerMessage(): string {
+    return 'Operation not allowed in read-only mode. Signer is required.';
+  }
 }
diff --git a/packages/restapi/src/lib/pushapi/chat.ts b/packages/restapi/src/lib/pushapi/chat.ts
index 193db597a..6cd5117e2 100644
--- a/packages/restapi/src/lib/pushapi/chat.ts
+++ b/packages/restapi/src/lib/pushapi/chat.ts
@@ -26,14 +26,15 @@ import {
   updateGroupProfile,
 } from '../chat/updateGroupProfile';
 import { User } from './user';
+import { PushAPI } from './PushAPI';
 export class Chat {
   private userInstance: User;
 
   constructor(
     private account: string,
-    private decryptedPgpPvtKey: string,
     private env: ENV,
-    private signer: SignerType,
+    private decryptedPgpPvtKey?: string,
+    private signer?: SignerType,
     private progressHook?: (progress: ProgressHookType) => void
   ) {
     this.userInstance = new User(this.account, this.env);
@@ -55,7 +56,7 @@ export class Chat {
       page: options?.page,
       limit: options?.limit,
       env: this.env,
-      toDecrypt: true,
+      toDecrypt: !!this.signer, // Set to false if signer is undefined or null,
     };
 
     switch (type) {
@@ -77,7 +78,7 @@ export class Chat {
 
     return await PUSH_CHAT.latest({
       threadhash: threadHash,
-      toDecrypt: true,
+      toDecrypt: !!this.signer, // Set to false if signer is undefined or null,
       pgpPrivateKey: this.decryptedPgpPvtKey,
       account: this.account,
       env: this.env,
@@ -111,12 +112,16 @@ export class Chat {
       env: this.env,
       threadhash: reference,
       pgpPrivateKey: this.decryptedPgpPvtKey,
-      toDecrypt: true,
+      toDecrypt: !!this.signer, // Set to false if signer is undefined or null,
       limit: options?.limit,
     });
   }
 
   async send(recipient: string, options: Message): Promise<MessageWithCID> {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
+
     if (!options.type) {
       options.type = MessageType.TEXT;
     }
@@ -131,6 +136,9 @@ export class Chat {
   }
 
   async decrypt(messagePayloads: IMessageIPFS[]) {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
     return await PUSH_CHAT.decryptConversation({
       pgpPrivateKey: this.decryptedPgpPvtKey,
       env: this.env,
@@ -140,6 +148,9 @@ export class Chat {
   }
 
   async accept(target: string): Promise<string> {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
     return await PUSH_CHAT.approve({
       senderAddress: target,
       env: this.env,
@@ -150,6 +161,9 @@ export class Chat {
   }
 
   async reject(target: string): Promise<void> {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
     await PUSH_CHAT.reject({
       senderAddress: target,
       env: this.env,
@@ -160,6 +174,9 @@ export class Chat {
   }
 
   async block(users: Array<string>): Promise<IUser> {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
     const user = await PUSH_USER.get({
       account: this.account,
       env: this.env,
@@ -194,6 +211,9 @@ export class Chat {
   }
 
   async unblock(users: Array<string>): Promise<IUser> {
+    if (!this.signer || !this.decryptedPgpPvtKey) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
     const user = await PUSH_USER.get({
       account: this.account,
       env: this.env,
@@ -236,6 +256,9 @@ export class Chat {
 
   group = {
     create: async (name: string, options?: GroupCreationOptions) => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const groupParams: PUSH_CHAT.ChatCreateGroupType = {
         groupName: name,
         groupDescription: options?.description,
@@ -271,6 +294,9 @@ export class Chat {
       chatId: string,
       options: GroupUpdateOptions
     ): Promise<GroupDTO> => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const group = await PUSH_CHAT.getGroup({
         chatId: chatId,
         env: this.env,
@@ -301,6 +327,9 @@ export class Chat {
     },
 
     add: async (chatId: string, options: ManageGroupOptions) => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const { role, accounts } = options;
 
       const validRoles = ['ADMIN', 'MEMBER'];
@@ -340,6 +369,9 @@ export class Chat {
     },
 
     remove: async (chatId: string, options: ManageGroupOptions) => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const { role, accounts } = options;
 
       const validRoles = ['ADMIN', 'MEMBER'];
@@ -379,6 +411,9 @@ export class Chat {
     },
 
     join: async (target: string): Promise<GroupDTO> => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const status = await PUSH_CHAT.getGroupMemberStatus({
         chatId: target,
         did: this.account,
@@ -411,6 +446,9 @@ export class Chat {
     },
 
     leave: async (target: string): Promise<GroupDTO> => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       const status = await PUSH_CHAT.getGroupMemberStatus({
         chatId: target,
         did: this.account,
@@ -438,6 +476,9 @@ export class Chat {
       }
     },
     reject: async (target: string): Promise<void> => {
+      if (!this.signer) {
+        throw new Error(PushAPI.ensureSignerMessage());
+      }
       await PUSH_CHAT.reject({
         senderAddress: target,
         env: this.env,
diff --git a/packages/restapi/src/lib/pushapi/encryption.ts b/packages/restapi/src/lib/pushapi/encryption.ts
index 14e1ec286..843c80236 100644
--- a/packages/restapi/src/lib/pushapi/encryption.ts
+++ b/packages/restapi/src/lib/pushapi/encryption.ts
@@ -1,9 +1,7 @@
 import { ENCRYPTION_TYPE, ENV } from '../constants';
-import {
-  SignerType,
-  ProgressHookType,
-} from '../types';
+import { SignerType, ProgressHookType } from '../types';
 import * as PUSH_USER from '../user';
+import { PushAPI } from './PushAPI';
 import { User } from './user';
 
 export class Encryption {
@@ -11,10 +9,10 @@ export class Encryption {
 
   constructor(
     private account: string,
-    private decryptedPgpPvtKey: string,
-    private pgpPublicKey: string,
     private env: ENV,
-    private signer: SignerType,
+    private decryptedPgpPvtKey?: string,
+    private pgpPublicKey?: string,
+    private signer?: SignerType,
     private progressHook?: (progress: ProgressHookType) => void
   ) {
     this.userInstance = new User(this.account, this.env);
@@ -22,19 +20,22 @@ export class Encryption {
 
   async info() {
     const userInfo = await this.userInstance.info();
-    const decryptedPassword = await PUSH_USER.decryptAuth({
-      account: this.account,
-      env: this.env,
-      signer: this.signer,
-      progressHook: this.progressHook,
-      additionalMeta: {
-        NFTPGP_V1: {
-          encryptedPassword: JSON.stringify(
-            JSON.parse(userInfo.encryptedPrivateKey).encryptedPassword
-          ),
+    let decryptedPassword;
+    if (this.signer) {
+      decryptedPassword = await PUSH_USER.decryptAuth({
+        account: this.account,
+        env: this.env,
+        signer: this.signer,
+        progressHook: this.progressHook,
+        additionalMeta: {
+          NFTPGP_V1: {
+            encryptedPassword: JSON.stringify(
+              JSON.parse(userInfo.encryptedPrivateKey).encryptedPassword
+            ),
+          },
         },
-      },
-    });
+      });
+    }
 
     return {
       decryptedPgpPrivateKey: this.decryptedPgpPvtKey,
@@ -53,6 +54,14 @@ export class Encryption {
       };
     }
   ) {
+    if (!this.signer) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
+
+    if (!this.decryptedPgpPvtKey || !this.pgpPublicKey) {
+      throw new Error(PushAPI.ensureSignerMessage());
+    }
+
     return await PUSH_USER.auth.update({
       account: this.account,
       pgpEncryptionVersion: updatedEncryptionType,
diff --git a/packages/restapi/src/lib/pushapi/profile.ts b/packages/restapi/src/lib/pushapi/profile.ts
index f3a8861b0..efe602841 100644
--- a/packages/restapi/src/lib/pushapi/profile.ts
+++ b/packages/restapi/src/lib/pushapi/profile.ts
@@ -1,12 +1,13 @@
 import { ProgressHookType } from '../types';
 import * as PUSH_USER from '../user';
 import { ENV } from '../constants';
+import { PushAPI } from './PushAPI';
 
 export class Profile {
   constructor(
     private account: string,
-    private decryptedPgpPvtKey: string,
     private env: ENV,
+    private decryptedPgpPvtKey?: string,
     private progressHook?: (progress: ProgressHookType) => void
   ) {}
 
@@ -19,6 +20,9 @@ export class Profile {
   }
 
   async update(options: { name?: string; desc?: string; picture?: string }) {
+     if (!this.decryptedPgpPvtKey) {
+       throw new Error(PushAPI.ensureSignerMessage());
+     }
     const { name, desc, picture } = options;
     const response = await PUSH_USER.profile.update({
       pgpPrivateKey: this.decryptedPgpPvtKey,
diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts
index ecc0b39d2..bc3cab957 100644
--- a/packages/restapi/src/lib/pushstream/PushStream.ts
+++ b/packages/restapi/src/lib/pushstream/PushStream.ts
@@ -25,11 +25,11 @@ export class PushStream extends EventEmitter {
 
   constructor(
     account: string,
-    private decryptedPgpPvtKey: string,
-    private signer: SignerType,
     private _listen: STREAM[],
     options: PushStreamInitializeProps,
-    private progressHook?: (progress: ProgressHookType) => void
+    private decryptedPgpPvtKey?: string,
+    private progressHook?: (progress: ProgressHookType) => void,
+    private signer?: SignerType
   ) {
     super();
 
@@ -41,8 +41,8 @@ export class PushStream extends EventEmitter {
 
     this.chatInstance = new Chat(
       this.account,
-      this.decryptedPgpPvtKey,
       this.options.env as ENV,
+      this.decryptedPgpPvtKey,
       this.signer,
       this.progressHook
     );
@@ -50,11 +50,11 @@ export class PushStream extends EventEmitter {
 
   static async initialize(
     account: string,
-    decryptedPgpPvtKey: string,
-    signer: SignerType,
     listen: STREAM[],
     env: ENV,
+    decryptedPgpPvtKey?: string,
     progressHook?: (progress: ProgressHookType) => void,
+    signer?: SignerType,
     options?: PushStreamInitializeProps
   ): Promise<PushStream> {
     const defaultOptions: PushStreamInitializeProps = {
@@ -81,11 +81,11 @@ export class PushStream extends EventEmitter {
 
     const stream = new PushStream(
       accountToUse,
-      decryptedPgpPvtKey,
-      signer,
       listen,
       settings,
-      progressHook
+      decryptedPgpPvtKey,
+      progressHook,
+      signer
     );
     return stream;
   }
@@ -265,8 +265,11 @@ export class PushStream extends EventEmitter {
               data.messageCategory == 'Chat' ||
               data.messageCategory == 'Request'
             ) {
-              data = await this.chatInstance.decrypt([data]);
-              data = data[0];
+              // Dont call this if read only mode ?
+              if (this.signer) {
+                data = await this.chatInstance.decrypt([data]);
+                data = data[0];
+              }
             }
 
             const modifiedData = DataModifier.handleChatEvent(data, this.raw);
diff --git a/packages/restapi/tests/lib/pushapi/chat.test.ts b/packages/restapi/tests/lib/pushapi/chat.test.ts
index 10b88c9d1..b78b1fc0a 100644
--- a/packages/restapi/tests/lib/pushapi/chat.test.ts
+++ b/packages/restapi/tests/lib/pushapi/chat.test.ts
@@ -38,6 +38,24 @@ describe('PushAPI.chat functionality', () => {
     expect(response).to.be.an('array');
     expect(response.length).to.equal(1);
   });
+
+  it('Should list request read only', async () => {
+    await userAlice.chat.send(account2, { content: MESSAGE });
+
+    const account = (await userBob.info()).did;
+
+    const userBobReadOnly = await PushAPI.initialize({
+      account: account,
+    });
+
+    const response = await userBobReadOnly.chat.list('REQUESTS', {
+      page: 1,
+      limit: 10,
+    });
+    expect(response).to.be.an('array');
+    expect(response.length).to.equal(1);
+  });
+
   it('Should list chats ', async () => {
     const response = await userAlice.chat.list('CHATS', {
       page: 1,
@@ -45,6 +63,19 @@ describe('PushAPI.chat functionality', () => {
     });
     expect(response).to.be.an('array');
   });
+  it('Should list chats read only', async () => {
+    const account = (await userAlice.info()).did;
+
+    const userAliceReadOnly = await PushAPI.initialize({
+      account: account,
+    });
+
+    const response = await userAliceReadOnly.chat.list('CHATS', {
+      page: 1,
+      limit: 10,
+    });
+    expect(response).to.be.an('array');
+  });
   it('Should send message ', async () => {
     const response = await userAlice.chat.send(account2, {
       content: 'Hello',
@@ -52,6 +83,30 @@ describe('PushAPI.chat functionality', () => {
     });
     expect(response).to.be.an('object');
   });
+  it('Should send message read only', async () => {
+    const account = (await userAlice.info()).did;
+
+    const userAliceReadOnly = await PushAPI.initialize({
+      account: account,
+    });
+
+    let errorCaught: any = null;
+
+    try {
+      await userAliceReadOnly.chat.send(account2, {
+        content: 'Hello',
+        type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT,
+      });
+    } catch (error) {
+      errorCaught = error;
+    }
+
+    expect(errorCaught).to.be.an('error');
+    expect(errorCaught.message).to.equal(
+      'Operation not allowed in read-only mode. Signer is required.'
+    );
+  });
+
   it('Should decrypt message ', async () => {
     await userAlice.chat.send(account2, {
       content: 'Hello',
diff --git a/packages/restapi/tests/lib/pushapi/profile.test.ts b/packages/restapi/tests/lib/pushapi/profile.test.ts
index ac7094f8c..1d2d9e71f 100644
--- a/packages/restapi/tests/lib/pushapi/profile.test.ts
+++ b/packages/restapi/tests/lib/pushapi/profile.test.ts
@@ -26,4 +26,29 @@ describe('PushAPI.profile functionality', () => {
     expect(response.name).to.equal(updatedName);
     expect(response.desc).to.equal(updatedDesc);
   });
+
+  it('Should get profile read only mode', async () => {
+    const updatedName = 'Bob The Builder';
+    const updatedDesc = 'Yes We Can';
+    await userAlice.profile.update({
+      name: updatedName,
+      desc: updatedDesc,
+    });
+
+    const response = await userAlice.profile.update({
+      name: updatedName,
+      desc: updatedDesc,
+    });
+    expect(response.name).to.equal(updatedName);
+    expect(response.desc).to.equal(updatedDesc);
+
+    const account = (await userAlice.info()).did;
+
+    const userAliceReadOnly = await PushAPI.initialize({
+      account: account,
+    });
+
+    expect((await userAliceReadOnly.info()).profile.name).to.equal(updatedName);
+    expect((await userAliceReadOnly.info()).profile.desc).to.equal(updatedDesc);
+  });
 });
diff --git a/packages/restapi/tests/lib/pushstream/initialize.test.ts b/packages/restapi/tests/lib/pushstream/initialize.test.ts
index 0e797f415..6b517c1df 100644
--- a/packages/restapi/tests/lib/pushstream/initialize.test.ts
+++ b/packages/restapi/tests/lib/pushstream/initialize.test.ts
@@ -392,6 +392,439 @@ describe('PushStream.initialize functionality', () => {
 
   
 
+    const w2wMessageResponse2 = await user2.chat.send(signer.address, {
+         content: MESSAGE,
+       });
+
+    const w2wMessageResponse2 = await user3.chat.send(signer.address, {
+      content: MESSAGE,
+    });
+    const w2wRejectRequest = await user.chat.reject(signer3.address);*/
+
+    let timeoutTriggered = false;
+
+    const timeout = new Promise((_, reject) => {
+      setTimeout(() => {
+        timeoutTriggered = true;
+        reject(new Error('Timeout after 5 seconds'));
+      }, 5000);
+    });
+
+    // Wrap the Promise.allSettled inside a Promise.race with the timeout
+    try {
+      const result = await Promise.race([
+        Promise.allSettled([
+          onDataReceived,
+          onMessageReceived,
+          onNoitificationsReceived,
+        ]),
+        timeout,
+      ]);
+
+      if (timeoutTriggered) {
+        console.error('Timeout reached before events were emitted.');
+      } else {
+        (result as PromiseSettledResult<any>[]).forEach((outcome) => {
+          if (outcome.status === 'fulfilled') {
+            //console.log(outcome.value);
+          } else if (outcome.status === 'rejected') {
+            console.error(outcome.reason);
+          }
+        });
+      }
+    } catch (error) {
+      console.error(error);
+    }
+  });
+
+  it('Should initialize new stream(readonly) and listen to events', async () => {
+    const MESSAGE = 'Hey There!!!';
+
+    const provider = ethers.getDefaultProvider();
+
+    const WALLET = ethers.Wallet.createRandom();
+    const signer = new ethers.Wallet(WALLET.privateKey, provider);
+    const user = await PushAPI.initialize(signer, {
+      env: CONSTANTS.ENV.LOCAL,
+    });
+
+    const WALLET2 = ethers.Wallet.createRandom();
+    const signer2 = new ethers.Wallet(WALLET2.privateKey, provider);
+    const user2 = await PushAPI.initialize(signer2, {
+      env: CONSTANTS.ENV.LOCAL,
+    });
+
+    const WALLET3 = ethers.Wallet.createRandom();
+    const signer3 = new ethers.Wallet(WALLET3.privateKey, provider);
+    const user3 = await PushAPI.initialize(signer3, {
+      env: CONSTANTS.ENV.LOCAL,
+    });
+
+    const WALLET4 = ethers.Wallet.createRandom();
+    const signer4 = new ethers.Wallet(WALLET4.privateKey, provider);
+    const user4 = await PushAPI.initialize(signer4, {
+      env: CONSTANTS.ENV.LOCAL,
+    });
+
+    const GROUP_RULES = {
+      entry: {
+        conditions: [
+          {
+            any: [
+              {
+                type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH,
+                category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.CUSTOM_ENDPOINT,
+                subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.GET,
+                data: {
+                  url: 'https://api.ud-staging.com/profile/badges/dead_pixel/validate/{{user_address}}?rule=join',
+                },
+              },
+            ],
+          },
+        ],
+      },
+      chat: {
+        conditions: [
+          {
+            any: [
+              {
+                type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH,
+                category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.CUSTOM_ENDPOINT,
+                subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.GET,
+                data: {
+                  url: 'https://api.ud-staging.com/profile/badges/dead_pixel/validate/{{user_address}}?rule=chat',
+                },
+              },
+            ],
+          },
+        ],
+      },
+    };
+
+    const CREATE_GROUP_REQUEST = {
+      description: 'test',
+      image: 'test',
+      members: [],
+      admins: [],
+      private: false,
+      rules: {
+        chat: {
+          conditions: {
+            any: [
+              {
+                type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH,
+                category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.ERC20,
+                subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.HOLDER,
+                data: {
+                  contract:
+                    'eip155:1:0xf418588522d5dd018b425E472991E52EBBeEEEEE',
+                  amount: 1,
+                  decimals: 18,
+                },
+              },
+              {
+                type: CONSTANTS.CHAT.GROUP.RULES.CONDITION_TYPE.PUSH,
+                category: CONSTANTS.CHAT.GROUP.RULES.CATEGORY.INVITE,
+                subcategory: CONSTANTS.CHAT.GROUP.RULES.SUBCATEGORY.DEFAULT,
+                data: {
+                  inviterRoles: [
+                    CONSTANTS.CHAT.GROUP.RULES.INVITER_ROLE.ADMIN,
+                    CONSTANTS.CHAT.GROUP.RULES.INVITER_ROLE.OWNER,
+                  ],
+                },
+              },
+            ],
+          },
+        },
+      },
+    };
+
+    const CREATE_GROUP_REQUEST_2 = {
+      description: 'test',
+      image: 'test',
+      members: [],
+      admins: [],
+      private: false,
+      rules: {},
+    };
+
+    /*const stream = await user.stream(
+      [CONSTANTS.STREAM.CHAT, CONSTANTS.STREAM.CHAT_OPS],
+      {
+        // stream supports other products as well, such as STREAM.CHAT, STREAM.CHAT_OPS
+        // more info can be found at push.org/docs/chat
+
+        filter: {
+          channels: ['*'],
+          chats: ['*'],
+        },
+        connection: {
+          auto: false, // should connection be automatic, else need to call stream.connect();
+          retries: 3, // number of retries in case of error
+        },
+        raw: true, // enable true to show all data
+      }
+    );
+
+    await stream.connect();*/
+
+    // Initialize wallet user, pass 'prod' instead of 'staging' for mainnet apps
+    const userAlice = await PushAPI.initialize(signer, {
+      env: CONSTANTS.ENV.STAGING,
+    });
+
+    const account = (await userAlice.info()).did;
+
+    const userAliceReadOnly = await PushAPI.initialize({
+      account: account,
+    });
+
+    // This will be the wallet address of the recipient
+    const pushAIWalletAddress = '0x99A08ac6254dcf7ccc37CeC662aeba8eFA666666';
+
+    // Listen for stream
+    // Checkout all chat stream listen options - https://push.org/docs/chat/build/stream-chats/
+    // Alternatively, just initialize userAlice.stream.initialize() without any listen options to listen to all events
+    const stream = await userAliceReadOnly.initStream(
+      [
+        CONSTANTS.STREAM.CHAT,
+        CONSTANTS.STREAM.CHAT_OPS,
+        CONSTANTS.STREAM.NOTIF,
+        CONSTANTS.STREAM.CONNECT,
+        CONSTANTS.STREAM.DISCONNECT,
+      ],
+      {}
+    );
+
+    stream.on(CONSTANTS.STREAM.CONNECT, (a) => {
+      console.log('Stream Connected');
+
+      // Send a message to Bob after socket connection so that messages as an example
+      console.log('Sending message to PushAI Bot');
+      userAlice.chat.send(pushAIWalletAddress, {
+        content: "Gm gm! It's a me... Mario",
+      });
+    });
+
+    await stream.connect();
+
+    stream.on(CONSTANTS.STREAM.DISCONNECT, () => {
+      console.log('Stream Disconnected');
+    });
+
+    // React to message payload getting recieved
+    stream.on(CONSTANTS.STREAM.CHAT, (message) => {
+      console.log('Encrypted Message Received');
+      console.log(message);
+      stream.disconnect();
+    });
+
+    const createEventPromise = (
+      expectedEvent: string,
+      eventType: string,
+      expectedEventCount: number
+    ) => {
+      return new Promise((resolve, reject) => {
+        let eventCount = 0;
+        if (expectedEventCount == 0) {
+          resolve('Done');
+        }
+        const receivedEvents: any[] = [];
+        stream.on(eventType, (data: any) => {
+          try {
+            receivedEvents.push(data);
+            eventCount++;
+
+            console.log(
+              `Event ${eventCount} for ${expectedEvent}:`,
+              util.inspect(JSON.stringify(data), {
+                showHidden: false,
+                depth: null,
+                colors: true,
+              })
+            );
+            expect(data).to.not.be.null;
+
+            if (eventCount === expectedEventCount) {
+              resolve(receivedEvents);
+            }
+          } catch (error) {
+            console.error('An error occurred:', error);
+            reject(error);
+          }
+        });
+      });
+    };
+
+    //  leave admin bug
+    //  group creator check remove add
+
+    const onDataReceived = createEventPromise(
+      'CHAT_OPS',
+      CONSTANTS.STREAM.CHAT_OPS,
+      5
+    );
+    const onMessageReceived = createEventPromise(
+      'CHAT',
+      CONSTANTS.STREAM.CHAT,
+      4
+    );
+    const onNoitificationsReceived = createEventPromise(
+      'NOTIF',
+      CONSTANTS.STREAM.NOTIF,
+      4
+    );
+
+    // Create and update group
+    const createdGroup = await user.chat.group.create(
+      'test',
+      CREATE_GROUP_REQUEST_2
+    );
+
+    const updatedGroup = await user.chat.group.update(createdGroup.chatId, {
+      description: 'Updated Description',
+    });
+
+    const updatedGroup2 = await user.chat.group.add(createdGroup.chatId, {
+      role: 'ADMIN',
+      accounts: [signer2.address, signer3.address, signer4.address],
+    });
+
+    const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId);
+
+    /*const w2wMessageResponse = await user2.chat.send(signer.address, {
+      content: MESSAGE,
+    });
+    const w2wAcceptsRequest = await user.chat.accept(signer2.address);
+
+    const w2wMessageResponse2 = await user2.chat.send(signer.address, {
+      content: MESSAGE,
+    });*/
+
+    /*const channelPrivateKey = process.env['WALLET_PRIVATE_KEY'];
+
+    const signerChannel = new ethers.Wallet(`0x${channelPrivateKey}`);
+    const channelAddress = signerChannel.address;
+
+    console.log(channelAddress);
+
+    const response = await subscribe({
+      signer: signer,
+      channelAddress: `eip155:5:${channelAddress}`, // channel address in CAIP
+      userAddress: `eip155:5:${signer.address}`, // user address in CAIP
+      onSuccess: () => {
+        console.log('opt in success');
+      },
+      onError: () => {
+        console.error('opt in error');
+      },
+      env: ENV.LOCAL,
+    });
+
+
+    const apiResponse = await sendNotification({
+      signer: signerChannel, // Needs to resolve to channel address
+      type: 1, // broadcast
+      identityType: 2, // direct payload
+      notification: {
+        title: `notification TITLE:`,
+        body: `notification BODY`,
+      },
+      payload: {
+        title: `payload title`,
+        body: `sample msg body`,
+        cta: '',
+        img: '',
+      },
+      channel: `eip155:5:${channelAddress}`, // your channel address
+      env: ENV.LOCAL,
+    });
+
+
+    const response2 = await unsubscribe({
+      signer: signer,
+      channelAddress: `eip155:5:${channelAddress}`, // channel address in CAIP
+      userAddress: `eip155:5:${signer.address}`, // user address in CAIP
+      onSuccess: () => {
+        console.log('opt out success');
+      },
+      onError: () => {
+        console.error('opt out error');
+      },
+      env: ENV.LOCAL,
+    });
+
+
+    const apiResponse2 = await sendNotification({
+      signer: signerChannel, // Needs to resolve to channel address
+      type: 3, // broadcast
+      identityType: 2, // direct payload
+      notification: {
+        title: `notification TITLE:`,
+        body: `notification BODY`,
+      },
+      payload: {
+        title: `payload title`,
+        body: `sample msg body`,
+        cta: '',
+        img: '',
+      },
+      recipients: `eip155:5:${signer.address}`,
+      channel: `eip155:5:${channelAddress}`, // your channel address
+      env: ENV.LOCAL,
+    });
+
+
+   
+    //const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId);
+    //const updatedGroup2 = await user2.chat.group.leave(createdGroup.chatId);
+
+    /*const updatedGroup3 = await user.chat.group.add(createdGroup.chatId, {
+      role: 'ADMIN',
+      accounts: [signer2.address],
+    });
+
+
+
+    const w2wAcceptsRequest = await user2.chat.group.join(createdGroup.chatId);
+
+           /* const updatedGroup4 = await user.chat.group.add(
+              createdGroup.chatId,
+              {
+                role: 'ADMIN',
+                accounts: [signer3.address],
+              }
+            );*/
+
+    /*const w2wMessageResponse = await user2.chat.send(signer.address, {
+      content: MESSAGE,
+    });
+    const w2wAcceptsRequest = await user.chat.accept(signer2.address);
+
+    const w2wMessageResponse2 = await user2.chat.send(signer.address, {
+      content: MESSAGE,
+    });
+
+    //const w2wRejectRequest = await user2.chat.group.join(createdGroup.chatId);
+
+    /*
+
+    
+    const updatedGroup2 = await user2.chat.group.leave(createdGroup.chatId);
+
+  
+
+    const updatedGroup = await user.chat.group.update(createdGroup.chatId, {
+      description: 'Updated Description',
+    });
+
+    const groupMessageResponse = await user.chat.send(createdGroup.chatId, {
+      content: 'Hello',
+      type: MessageType.TEXT,
+    });
+
+  
+
     const w2wMessageResponse2 = await user2.chat.send(signer.address, {
          content: MESSAGE,
        });