diff --git a/packages/examples/sdk-backend-node/.env.sample b/packages/examples/sdk-backend-node/.env.sample index a444eee26..77f92678b 100644 --- a/packages/examples/sdk-backend-node/.env.sample +++ b/packages/examples/sdk-backend-node/.env.sample @@ -37,6 +37,21 @@ NFT_HOLDER_WALLET_PRIVATE_KEY_2=your_nft_holder_wallet_private_key # NFT PROFILE WALLET PASSWORD NFT_PROFILE_PASSWORD_2=your_nft_profile_password +# NFT CONTRACT ADDRESS +NFT_CONTRACT_ADDRESS_3=your_nft_contract_address + +# NFT CHAIN ID +NFT_CHAIN_ID_3=your_nft_chain_id + +# NFT TOKEN ID +NFT_TOKEN_ID_3=your_nft_token_id + +# NFT HOLDER WALLET PRIVATE KEY +NFT_HOLDER_WALLET_PRIVATE_KEY_3=your_nft_holder_wallet_private_key + +# NFT PROFILE WALLET PASSWORD +NFT_PROFILE_PASSWORD_3=your_nft_profile_password + # VIDEO CHAIN ID VIDEO_CHAIN_ID=your_video_chain_id diff --git a/packages/examples/sdk-backend-node/chat/index.ts b/packages/examples/sdk-backend-node/chat/index.ts index 85621540f..89ab69ed2 100644 --- a/packages/examples/sdk-backend-node/chat/index.ts +++ b/packages/examples/sdk-backend-node/chat/index.ts @@ -1,6 +1,7 @@ import { runChatLowlevelUseCases } from './chat.lowlevel'; import { runNFTChatLowLevelUseCases } from './nftChat.lowlevel'; import { runChatClassUseCases } from './chat'; +import { runNFTChatClassUseCases } from './nftChat'; export const runChatUseCases = async (): Promise => { console.log(` @@ -13,12 +14,25 @@ export const runChatUseCases = async (): Promise => { `); await runChatClassUseCases(); + + console.log(` +███╗░░██╗███████╗████████╗ ░█████╗░██╗░░██╗░█████╗░████████╗ +████╗░██║██╔════╝╚══██╔══╝ ██╔══██╗██║░░██║██╔══██╗╚══██╔══╝ +██╔██╗██║█████╗░░░░░██║░░░ ██║░░╚═╝███████║███████║░░░██║░░░ +██║╚████║██╔══╝░░░░░██║░░░ ██║░░██╗██╔══██║██╔══██║░░░██║░░░ +██║░╚███║██║░░░░░░░░██║░░░ ╚█████╔╝██║░░██║██║░░██║░░░██║░░░ +╚═╝░░╚══╝╚═╝░░░░░░░░╚═╝░░░ ░╚════╝░╚═╝░░╚═╝╚═╝░░╚═╝░░░╚═╝░░░ + `); + + await runNFTChatClassUseCases(); + console.log(` ▒█▀▀█ ▒█░▒█ ░█▀▀█ ▀▀█▀▀ ░ ▒█░░░ ▒█▀▀▀█ ▒█░░▒█ ▒█░░░ ▒█▀▀▀ ▒█░░▒█ ▒█▀▀▀ ▒█░░░ ▒█░░░ ▒█▀▀█ ▒█▄▄█ ░▒█░░ ▄ ▒█░░░ ▒█░░▒█ ▒█▒█▒█ ▒█░░░ ▒█▀▀▀ ░▒█▒█░ ▒█▀▀▀ ▒█░░░ ▒█▄▄█ ▒█░▒█ ▒█░▒█ ░▒█░░ █ ▒█▄▄█ ▒█▄▄▄█ ▒█▄▀▄█ ▒█▄▄█ ▒█▄▄▄ ░░▀▄▀░ ▒█▄▄▄ ▒█▄▄█ `); await runChatLowlevelUseCases(); + console.log(` ▒█▄░▒█ ▒█▀▀▀ ▀▀█▀▀ ▒█▀▀█ ▒█░▒█ ░█▀▀█ ▀▀█▀▀ ░ ▒█░░░ ▒█▀▀▀█ ▒█░░▒█ ▒█░░░ ▒█▀▀▀ ▒█░░▒█ ▒█▀▀▀ ▒█░░░ ▒█▒█▒█ ▒█▀▀▀ ░▒█░░ ▒█░░░ ▒█▀▀█ ▒█▄▄█ ░▒█░░ ▄ ▒█░░░ ▒█░░▒█ ▒█▒█▒█ ▒█░░░ ▒█▀▀▀ ░▒█▒█░ ▒█▀▀▀ ▒█░░░ diff --git a/packages/examples/sdk-backend-node/chat/nftChat.ts b/packages/examples/sdk-backend-node/chat/nftChat.ts new file mode 100644 index 000000000..ea2892983 --- /dev/null +++ b/packages/examples/sdk-backend-node/chat/nftChat.ts @@ -0,0 +1,343 @@ +import { CONSTANTS, PushAPI } from '@pushprotocol/restapi'; +import { + adjectives, + animals, + colors, + uniqueNamesGenerator, +} from 'unique-names-generator'; +import { config } from '../config'; +import { PushStream } from '@pushprotocol/restapi/src/lib/pushstream/PushStream'; +import { ethers } from 'ethers'; + +// CONFIGS +const { env, showAPIResponse } = config; + +/*********************** SAMPLE NFT DATA **************************/ +const nftChainId1 = process.env.NFT_CHAIN_ID_1 || '11155111'; +const nftContractAddress1 = process.env.NFT_CONTRACT_ADDRESS_1 || ''; +const nftTokenId1 = process.env.NFT_TOKEN_ID_1 || ''; +const nftHolderWalletPrivatekey1 = + process.env.NFT_HOLDER_WALLET_PRIVATE_KEY_1 || ''; +const nftSigner1 = new ethers.Wallet(`0x${nftHolderWalletPrivatekey1}`); +// NFT Account structure for Push Chat : nft:eip155:{chainId}:{contractAddress}:{tokenId} +const nftAccount1 = `nft:eip155:${nftChainId1}:${nftContractAddress1}:${nftTokenId1}`; +// NFT Profile Password ( Used for recovery in case of NFT transfers ) +const nftProfilePassword1 = process.env.NFT_PROFILE_PASSWORD_1 || ''; + +const nftChainId2 = process.env.NFT_CHAIN_ID_2 || '11155111'; +const nftContractAddress2 = process.env.NFT_CONTRACT_ADDRESS_2 || ''; +const nftTokenId2 = process.env.NFT_TOKEN_ID_2 || ''; +const nftHolderWalletPrivatekey2 = + process.env.NFT_HOLDER_WALLET_PRIVATE_KEY_2 || ''; +const nftSigner2 = new ethers.Wallet(`0x${nftHolderWalletPrivatekey2}`); +const nftAccount2 = `nft:eip155:${nftChainId2}:${nftContractAddress2}:${nftTokenId2}`; +const nftProfilePassword2 = process.env.NFT_PROFILE_PASSWORD_2 || ''; + +const nftChainId3 = process.env.NFT_CHAIN_ID_3 || '11155111'; +const nftContractAddress3 = process.env.NFT_CONTRACT_ADDRESS_3 || ''; +const nftTokenId3 = process.env.NFT_TOKEN_ID_3 || ''; +const nftHolderWalletPrivatekey3 = + process.env.NFT_HOLDER_WALLET_PRIVATE_KEY_3 || ''; +const nftSigner3 = new ethers.Wallet(`0x${nftHolderWalletPrivatekey3}`); +const nftAccount3 = `nft:eip155:${nftChainId3}:${nftContractAddress3}:${nftTokenId3}`; +const nftProfilePassword3 = process.env.NFT_PROFILE_PASSWORD_3 || ''; + +const randomWallet1 = ethers.Wallet.createRandom().address; +const randomWallet2 = ethers.Wallet.createRandom().address; +const randomWallet3 = ethers.Wallet.createRandom().address; +/****************************************************************/ + +/***************** SAMPLE GROUP DATA ****************************/ +const groupName = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], +}); +const groupDescription = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], +}); +const groupImage = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAvklEQVR4AcXBsW2FMBiF0Y8r3GQb6jeBxRauYRpo4yGQkMd4A7kg7Z/GUfSKe8703fKDkTATZsJsrr0RlZSJ9r4RLayMvLmJjnQS1d6IhJkwE2bT13U/DBzp5BN73xgRZsJMmM1HOolqb/yWiWpvjJSUiRZWopIykTATZsJs5g+1N6KSMiO1N/5DmAkzYTa9Lh6MhJkwE2ZzSZlo7xvRwson3txERzqJhJkwE2bT6+JhoKTMJ2pvjAgzYSbMfgDlXixqjH6gRgAAAABJRU5ErkJggg=='; +/***************** SAMPLE GROUP DATA ****************************/ + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const eventlistener = async ( + stream: PushStream, + eventName: string +): Promise => { + stream.on(eventName, (data: any) => { + if (showAPIResponse) { + console.log('Stream Event Received'); + console.log(data); + console.log('\n'); + } + }); +}; + +const skipExample = () => { + const requiredEnvVars = [ + 'NFT_CHAIN_ID_1', + 'NFT_CONTRACT_ADDRESS_1', + 'NFT_TOKEN_ID_1', + 'NFT_HOLDER_WALLET_PRIVATE_KEY_1', + 'NFT_PROFILE_PASSWORD_1', + 'NFT_CHAIN_ID_2', + 'NFT_CONTRACT_ADDRESS_2', + 'NFT_TOKEN_ID_2', + 'NFT_HOLDER_WALLET_PRIVATE_KEY_2', + 'NFT_PROFILE_PASSWORD_2', + 'NFT_CHAIN_ID_3', + 'NFT_CONTRACT_ADDRESS_3', + 'NFT_TOKEN_ID_3', + 'NFT_HOLDER_WALLET_PRIVATE_KEY_3', + 'NFT_PROFILE_PASSWORD_3', + ]; + + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + return true; // Skip the example if any of the required env vars is missing + } + } + + return false; // All required env vars are present, don't skip the example +}; + +export const runNFTChatClassUseCases = async (): Promise => { + if (skipExample()) { + console.log('Skipping examples as required env vars are missing'); + return; + } + + const userAlice = await PushAPI.initialize(nftSigner1, { + env, + account: nftAccount1, + versionMeta: { NFTPGP_V1: { password: nftProfilePassword1 } }, + }); + + const stream = await userAlice.initStream( + [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: true, // 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 + } + ); + + stream.on(CONSTANTS.STREAM.CONNECT, (a) => { + console.log('Stream Connected'); + }); + + await stream.connect(); + + stream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('Stream Disconnected'); + }); + + const userBob = await await PushAPI.initialize(nftSigner2, { + env, + account: nftAccount2, + versionMeta: { NFTPGP_V1: { password: nftProfilePassword2 } }, + }); + + const userKate = await await PushAPI.initialize(nftSigner3, { + env, + account: nftAccount3, + versionMeta: { NFTPGP_V1: { password: nftProfilePassword3 } }, + }); + + // Listen stream events to receive websocket events + console.log(`Listening ${CONSTANTS.STREAM.CHAT} Events`); + eventlistener(stream, CONSTANTS.STREAM.CHAT); + console.log(`Listening ${CONSTANTS.STREAM.CHAT_OPS} Events`); + eventlistener(stream, CONSTANTS.STREAM.CHAT_OPS); + console.log('\n\n'); + + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.list'); + const aliceChats = await userAlice.chat.list(CONSTANTS.CHAT.LIST_TYPE.CHATS); + const aliceRequests = await userAlice.chat.list( + CONSTANTS.CHAT.LIST_TYPE.REQUESTS + ); + if (showAPIResponse) { + console.log(aliceChats); + console.log(aliceRequests); + } + console.log('PushAPI.chat.list | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.latest'); + const aliceLatestChatWithBob = await userAlice.chat.latest(nftAccount2); + if (showAPIResponse) { + console.log(aliceLatestChatWithBob); + } + console.log('PushAPI.chat.latest | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.history'); + const aliceChatHistoryWithBob = await userAlice.chat.history(nftAccount2); + if (showAPIResponse) { + console.log(aliceChatHistoryWithBob); + } + console.log('PushAPI.chat.history | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.send'); + const aliceMessagesBob = await userAlice.chat.send(nftAccount2, { + content: 'Hello Bob!', + type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT, + }); + if (showAPIResponse) { + console.log(aliceMessagesBob); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.chat.send | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.accept'); + try { + const bobAcceptsRequest = await userBob.chat.accept(nftAccount1); + if (showAPIResponse) { + console.log(bobAcceptsRequest); + } + } catch (e) { + console.log("Ignoring error as Bob's request is already accepted"); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.chat.accept | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.reject'); + await userKate.chat.send(nftAccount1, { + content: 'Sending malicious message', + type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT, + }); + const AliceRejectsRequest = await userAlice.chat.reject(nftAccount3); + if (showAPIResponse) { + console.log(AliceRejectsRequest); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.chat.reject | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.block'); + const AliceBlocksBob = await userAlice.chat.block([nftAccount2]); + if (showAPIResponse) { + console.log(AliceBlocksBob); + } + console.log('PushAPI.chat.block | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.chat.unblock'); + const AliceUnblocksBob = await userAlice.chat.unblock([nftAccount2]); + if (showAPIResponse) { + console.log(AliceUnblocksBob); + } + console.log('PushAPI.chat.unblock | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.create'); + const createdGroup = await userAlice.chat.group.create(groupName, { + description: groupDescription, + image: groupImage, + members: [randomWallet1, randomWallet2], + admins: [], + private: false, + }); + const groupChatId = createdGroup.chatId; // to be used in other examples + if (showAPIResponse) { + console.log(createdGroup); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.create | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.permissions'); + const grouppermissions = await userAlice.chat.group.permissions(groupChatId); + if (showAPIResponse) { + console.log(grouppermissions); + } + console.log('PushAPI.group.permissions | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.info'); + const groupInfo = await userAlice.chat.group.info(groupChatId); + if (showAPIResponse) { + console.log(groupInfo); + } + console.log('PushAPI.group.info | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.update'); + const updatedGroup = await userAlice.chat.group.update(groupChatId, { + description: 'Updated Description', + }); + if (showAPIResponse) { + console.log(updatedGroup); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.update | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.add'); + const addMember = await userAlice.chat.group.add(groupChatId, { + role: 'MEMBER', + accounts: [randomWallet3], + }); + if (showAPIResponse) { + console.log(addMember); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.add | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.remove'); + const removeMember = await userAlice.chat.group.remove(groupChatId, { + role: 'MEMBER', + accounts: [randomWallet3], + }); + if (showAPIResponse) { + console.log(removeMember); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.remove | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.join'); + const joinGrp = await userBob.chat.group.join(groupChatId); + if (showAPIResponse) { + console.log(joinGrp); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.join | Response - 200 OK\n\n'); + //------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.leave'); + const leaveGrp = await userBob.chat.group.leave(groupChatId); + if (showAPIResponse) { + console.log(leaveGrp); + } + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.leave | Response - 200 OK\n\n'); + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + console.log('PushAPI.group.reject'); + const sampleGrp = await userAlice.chat.group.create('Sample Grp', { + description: groupDescription, + image: groupImage, + members: [nftAccount2], // invite bob + admins: [], + private: true, + }); + await userBob.chat.group.reject(sampleGrp.chatId); + await delay(2000); // Delay added to log the events in order + console.log('PushAPI.group.reject | Response - 200 OK\n\n'); +}; diff --git a/packages/examples/sdk-backend-node/package.json b/packages/examples/sdk-backend-node/package.json index 47b5d3196..c27d9cfd6 100644 --- a/packages/examples/sdk-backend-node/package.json +++ b/packages/examples/sdk-backend-node/package.json @@ -11,7 +11,7 @@ "author": "", "license": "ISC", "dependencies": { - "@pushprotocol/restapi": "@latest", + "@pushprotocol/restapi": "1.5.0", "@pushprotocol/socket": "^0.5.2" } } diff --git a/packages/restapi/README.md b/packages/restapi/README.md index e86e1bb72..3ac0492cf 100644 --- a/packages/restapi/README.md +++ b/packages/restapi/README.md @@ -34,6 +34,7 @@ This package gives access to Push Protocol (Push Nodes) APIs. Visit [Developer D - [Stream Notifications](#stream-notifications) - [For Push Chat](#for-push-chat) - [Initialize](#initialize) + - [Reinitialize](#reinitialize) - [Fetch Info](#fetch-info) - [Fetch Profile Info](#fetch-profile-info) - [Update Profile Info](#update-profile-info) @@ -175,7 +176,7 @@ const userAlice = await PushAPI.initialize(signer, { | `options.progressHook`\* | `(progress: ProgressHookType) => void` | - | A callback function to receive progress updates during initialization. | | `options.account` \* | `string` | - | The account to associate with the PushAPI. If not provided, it is derived from signer. | | `options.version` \* | `string` | `ENC_TYPE_V3` | The encryption version to use for the PushAPI. | -| `options.versionMeta` \* | `{ NFTPGP_V1 ?: password: string }` | - | Metadata related to the encryption version, including a password if needed. | +| `options.versionMeta` \* | `{ NFTPGP_V1 ?: { password: string } }` | - | Metadata related to the encryption version, including a password if needed, and reset for resetting nft profile | | `options.autoUpgrade` \* | `boolean` | `true` | If `true`, upgrades encryption keys to the latest encryption version. | | `options.origin` \* | `string` | - | Specify origin or source while creating a Push Profile. | @@ -739,6 +740,23 @@ const userAlice = await PushAPI.initialize(signer, { | `CHAT_OPS` | `STREAM.CHAT_OPS` | +--- + +### **Reinitialize** + +```typescript +// Reinitialize PushAPI for fresh start of NFT Account +// Reinitialize only succeeds if the signer account is the owner of the NFT +await userAlice.reinitialize({ versionMeta: { NFTPGP_V1: { password: 'NewPassword' } } }); +``` + +## Parameters + +| Param | Type | Default | Remarks | +| --------------------------------------- | ------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | +| `options` | `PushAPIInitializeProps` | - | Optional configuration properties for initializing the PushAPI. | +| `options.versionMeta` | `{ NFTPGP_V1 ?: password: string }` | - | Metadata related to the encryption version, including a password if needed. | + --- ### **Fetch Info** diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index 4b8de488d..82e6e4f54 100644 --- a/packages/restapi/src/lib/pushapi/PushAPI.ts +++ b/packages/restapi/src/lib/pushapi/PushAPI.ts @@ -16,6 +16,7 @@ import { STREAM, } from '../pushstream/pushStreamTypes'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { isValidCAIP10NFTAddress } from '../helpers'; export class PushAPI { private signer?: SignerType; @@ -36,6 +37,9 @@ export class PushAPI { public channel!: Channel; public notification!: Notification; + // error object to maintain errors and warnings + public errors: { type: 'WARN' | 'ERROR'; message: string }[]; + private constructor( env: ENV, account: string, @@ -44,7 +48,8 @@ export class PushAPI { decryptedPgpPvtKey?: string, pgpPublicKey?: string, signer?: SignerType, - progressHook?: (progress: ProgressHookType) => void + progressHook?: (progress: ProgressHookType) => void, + initializationErrors?: { type: 'WARN' | 'ERROR'; message: string }[] ) { this.signer = signer; this.readMode = readMode; @@ -81,6 +86,7 @@ export class PushAPI { this.progressHook ); this.user = new User(this.account, this.env); + this.errors = initializationErrors || []; } // Overloaded initialize method signatures static async initialize( @@ -142,7 +148,11 @@ export class PushAPI { : ALPHA_FEATURE_CONFIG[PACKAGE_BUILD], }; - const readMode = !signer; + let readMode = !signer; + const initializationErrors: { + type: 'WARN' | 'ERROR'; + message: string; + }[] = []; // Get account // Derives account from signer if not provided @@ -178,14 +188,35 @@ export class PushAPI { 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, - }); + try { + decryptedPGPPrivateKey = await PUSH_CHAT.decryptPGPKey({ + encryptedPGPPrivateKey: user.encryptedPrivateKey, + signer: signer, + toUpgrade: settings.autoUpgrade, + additionalMeta: settings.versionMeta, + progressHook: settings.progressHook, + env: settings.env, + }); + } catch (error) { + const decryptionError = + 'Error decrypting PGP private key ...swiching to Guest mode'; + initializationErrors.push({ + type: 'ERROR', + message: decryptionError, + }); + console.error(decryptionError); + if (isValidCAIP10NFTAddress(derivedAccount)) { + const nftDecryptionError = + 'NFT Account Detected. If this NFT was recently transferred to you, please ensure you have received the correct password from the previous owner. Alternatively, you can reinitialize for a fresh start. Please be aware that reinitialization will result in the loss of all previous account data.'; + + initializationErrors.push({ + type: 'WARN', + message: nftDecryptionError, + }); + console.warn(nftDecryptionError); + } + readMode = true; + } pgpPublicKey = user.publicKey; } else { const newUser = await PUSH_USER.create({ @@ -211,7 +242,8 @@ export class PushAPI { decryptedPGPPrivateKey, pgpPublicKey, signer, - settings.progressHook + settings.progressHook, + initializationErrors ); return api; @@ -221,6 +253,52 @@ export class PushAPI { } } + /** + * This method is used to reinitialize the PushAPI instance + * @notice - This method should only be used for fresh start of NFT accounts + * @notice - All data will be lost after reinitialization + */ + async reinitialize(options: { + versionMeta: { NFTPGP_V1: { password: string } }; + }): Promise { + const newUser = await PUSH_USER.create({ + env: this.env, + account: this.account, + signer: this.signer, + additionalMeta: options.versionMeta, + progressHook: this.progressHook, + }); + + this.decryptedPgpPvtKey = newUser.decryptedPrivateKey as string; + this.pgpPublicKey = newUser.publicKey; + this.readMode = false; + this.errors = []; + + // Initialize the instances of the four classes + this.chat = new Chat( + this.account, + this.env, + this.alpha, + this.decryptedPgpPvtKey, + this.signer, + this.progressHook + ); + this.profile = new Profile( + this.account, + this.env, + this.decryptedPgpPvtKey, + this.progressHook + ); + this.encryption = new Encryption( + this.account, + this.env, + this.decryptedPgpPvtKey, + this.pgpPublicKey, + this.signer, + this.progressHook + ); + } + async initStream( listen: STREAM[], options?: PushStreamInitializeProps diff --git a/packages/restapi/src/lib/pushapi/chat.ts b/packages/restapi/src/lib/pushapi/chat.ts index 930d2a30f..66e5eabee 100644 --- a/packages/restapi/src/lib/pushapi/chat.ts +++ b/packages/restapi/src/lib/pushapi/chat.ts @@ -12,7 +12,6 @@ import { IMessageIPFS, GroupInfoDTO, ChatMemberProfile, - ChatMemberCounts, GroupParticipantCounts, } from '../types'; import { @@ -146,6 +145,7 @@ export class Chat { const sendParams: ChatSendOptionsType = { message: options, to: recipient, + account: this.account, signer: this.signer, pgpPrivateKey: this.decryptedPgpPvtKey, env: this.env, @@ -287,6 +287,7 @@ export class Chat { } const groupParams: PUSH_CHAT.ChatCreateGroupTypeV2 = { + account: this.account, signer: this.signer, pgpPrivateKey: this.decryptedPgpPvtKey, env: this.env, @@ -325,13 +326,17 @@ export class Chat { chatId: string, options?: GetGroupParticipantsOptions ): Promise<{ members: ChatMemberProfile[] }> => { - const { page = 1, limit = 20,filter={pending:undefined,role:undefined} } = options ?? {}; + const { + page = 1, + limit = 20, + filter = { pending: undefined, role: undefined }, + } = options ?? {}; const getGroupMembersOptions: PUSH_CHAT.FetchChatGroupInfoType = { chatId, page, limit, - pending:filter.pending, - role:filter.role, + pending: filter.pending, + role: filter.role, env: this.env, }; @@ -344,10 +349,10 @@ export class Chat { chatId, env: this.env, }); - return { - participants: count.overallCount - count.pendingCount, - pending: count.pendingCount, - }; + return { + participants: count.overallCount - count.pendingCount, + pending: count.pendingCount, + }; }, status: async ( @@ -388,6 +393,7 @@ export class Chat { env: this.env, }); }, + update: async ( chatId: string, options: GroupUpdateOptions diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index 278e43250..d62edd9d0 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -1,5 +1,5 @@ import Constants, { ENV } from '../constants'; -import { ChatMemberCounts, ChatMemberProfile, ChatStatus, ProgressHookType, Rules } from '../types'; +import { ChatStatus, ProgressHookType, Rules } from '../types'; export enum ChatListType { CHATS = 'CHATS', @@ -61,9 +61,8 @@ export interface InfoOptions { overrideAccount?: string; } - export interface ParticipantStatus { pending: boolean; role: 'ADMIN' | 'MEMBER'; participant: boolean; -} \ No newline at end of file +} diff --git a/packages/restapi/tests/lib/chat/initialize.test.ts b/packages/restapi/tests/lib/chat/initialize.test.ts index 4598ea588..b67fa93e8 100644 --- a/packages/restapi/tests/lib/chat/initialize.test.ts +++ b/packages/restapi/tests/lib/chat/initialize.test.ts @@ -118,7 +118,7 @@ describe('PushAPI.initialize functionality', () => { expect(progressInfo[i].progressId).to.deep.equal(expectedHooks[i]); } }); - it('Should not initialize on wrong version meta', async () => { + it('Should initialize with read Mode on wrong version meta', async () => { const nftSigner = new ethers.Wallet( `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_1']}` ); @@ -129,12 +129,11 @@ describe('PushAPI.initialize functionality', () => { progressInfo.push(info); }; // Already Existing NFT User - await expect( - PushAPI.initialize(nftSigner, { - account: nftAccount, - progressHook: updateProgressInfo, - versionMeta: { NFTPGP_V1: { password: 'wrongpassword' } }, - }) - ).to.be.rejected; + const userAlice = await PushAPI.initialize(nftSigner, { + account: nftAccount, + progressHook: updateProgressInfo, + versionMeta: { NFTPGP_V1: { password: 'wrongpassword' } }, + }); + expect(userAlice.errors.length).to.deep.equal(2); }); }); diff --git a/packages/restapi/tests/lib/chat/nftChat.test.ts b/packages/restapi/tests/lib/chat/nftChat.test.ts new file mode 100644 index 000000000..7da7b1605 --- /dev/null +++ b/packages/restapi/tests/lib/chat/nftChat.test.ts @@ -0,0 +1,358 @@ +import * as path from 'path'; +import * as dotenv from 'dotenv'; +import { PushAPI } from '../../../src/lib/pushapi/PushAPI'; +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import CONSTANTS from '../../../src/lib/constantsV2'; +import { + adjectives, + animals, + colors, + uniqueNamesGenerator, +} from 'unique-names-generator'; +import { PushStream } from '../../../src/lib/pushstream/PushStream'; +import { ENCRYPTION_TYPE } from '../../../src/lib/constants'; + +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +const env = CONSTANTS.ENV.DEV; +const showAPIResponse = false; + +describe('PushAPI.chat functionality For NFT Profile', () => { + let userAlice: PushAPI, userBob: PushAPI, userKate: PushAPI, stream; + let nftAccount1: string, nftAccount2: string, nftAccount3: string; + let groupChatId: string; + + const eventlistener = async ( + stream: PushStream, + eventName: string + ): Promise => { + stream.on(eventName, (data: any) => { + if (showAPIResponse) { + console.log('Stream Event Received'); + console.log(data); + console.log('\n'); + } + }); + }; + + const randomWallet1 = ethers.Wallet.createRandom().address; + const randomWallet2 = ethers.Wallet.createRandom().address; + const randomWallet3 = ethers.Wallet.createRandom().address; + + /***************** SAMPLE GROUP DATA ****************************/ + const groupName = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], + }); + const groupDescription = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], + }); + const groupImage = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAvklEQVR4AcXBsW2FMBiF0Y8r3GQb6jeBxRauYRpo4yGQkMd4A7kg7Z/GUfSKe8703fKDkTATZsJsrr0RlZSJ9r4RLayMvLmJjnQS1d6IhJkwE2bT13U/DBzp5BN73xgRZsJMmM1HOolqb/yWiWpvjJSUiRZWopIykTATZsJs5g+1N6KSMiO1N/5DmAkzYTa9Lh6MhJkwE2ZzSZlo7xvRwson3txERzqJhJkwE2bT6+JhoKTMJ2pvjAgzYSbMfgDlXixqjH6gRgAAAABJRU5ErkJggg=='; + /***************** SAMPLE GROUP DATA ****************************/ + + before(async () => { + // Initialization of shared variables + nftAccount1 = `nft:eip155:${process.env['NFT_CHAIN_ID_1']}:${process.env['NFT_CONTRACT_ADDRESS_1']}:${process.env['NFT_TOKEN_ID_1']}`; + nftAccount2 = `nft:eip155:${process.env['NFT_CHAIN_ID_2']}:${process.env['NFT_CONTRACT_ADDRESS_2']}:${process.env['NFT_TOKEN_ID_2']}`; + nftAccount3 = `nft:eip155:${process.env['NFT_CHAIN_ID_3']}:${process.env['NFT_CONTRACT_ADDRESS_3']}:${process.env['NFT_TOKEN_ID_3']}`; + + userAlice = await PushAPI.initialize( + new ethers.Wallet( + `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_1'] || ''}` + ), + { + env, + account: nftAccount1, + } + ); + + // Reinitialize for fresh start + await userAlice.reinitialize({ + versionMeta: { + NFTPGP_V1: { password: process.env['NFT_PROFILE_PASSWORD_1'] || '' }, + }, + }); + + userBob = await PushAPI.initialize( + new ethers.Wallet( + `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_2'] || ''}` + ), + { + env, + account: nftAccount2, + } + ); + + // Reinitialize for fresh start + await userBob.reinitialize({ + versionMeta: { + NFTPGP_V1: { password: process.env['NFT_PROFILE_PASSWORD_2'] || '' }, + }, + }); + + userKate = await PushAPI.initialize( + new ethers.Wallet( + `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_3'] || ''}` + ), + { + env, + account: nftAccount3, + } + ); + + // Reinitialize for fresh start + await userKate.reinitialize({ + versionMeta: { + NFTPGP_V1: { password: process.env['NFT_PROFILE_PASSWORD_3'] || '' }, + }, + }); + + // Stream Initialization + stream = await userAlice.initStream( + [CONSTANTS.STREAM.CHAT, CONSTANTS.STREAM.CHAT_OPS], + { + filter: { + channels: ['*'], + chats: ['*'], + }, + connection: { + auto: true, + retries: 3, + }, + raw: true, + } + ); + + await stream.connect(); + + stream.on(CONSTANTS.STREAM.CONNECT, () => { + console.log('Stream Connected'); + }); + + stream.on(CONSTANTS.STREAM.DISCONNECT, () => { + console.log('Stream Disconnected'); + }); + + // Listen stream events to receive websocket events + console.log(`Listening ${CONSTANTS.STREAM.CHAT} Events`); + eventlistener(stream, CONSTANTS.STREAM.CHAT); + console.log(`Listening ${CONSTANTS.STREAM.CHAT_OPS} Events`); + eventlistener(stream, CONSTANTS.STREAM.CHAT_OPS); + console.log('\n\n'); + }); + + it('should change Profile Password', async () => { + console.log('PushAPI.encryption.update'); + const updatedEncryption = await userAlice.encryption.update( + ENCRYPTION_TYPE.NFTPGP_V1, + { + versionMeta: { + NFTPGP_V1: { + password: '#@MyNewPass001', + }, + }, + } + ); + if (showAPIResponse) { + console.log(updatedEncryption); + } + console.log('PushAPI.group.reject | Response - 200 OK\n\n'); + + // revert + await userAlice.encryption.update(ENCRYPTION_TYPE.NFTPGP_V1, { + versionMeta: { + NFTPGP_V1: { + password: process.env['NFT_PROFILE_PASSWORD_1'] || '', + }, + }, + }); + }); + + it('should list chats and requests correctly', async () => { + console.log('PushAPI.chat.list'); + const aliceChats = await userAlice.chat.list( + CONSTANTS.CHAT.LIST_TYPE.CHATS + ); + const aliceRequests = await userAlice.chat.list( + CONSTANTS.CHAT.LIST_TYPE.REQUESTS + ); + if (showAPIResponse) { + console.log(aliceChats); + console.log(aliceRequests); + } + console.log('PushAPI.chat.list | Response - 200 OK\n\n'); + }); + + it('should get the latest chat correctly', async () => { + console.log('PushAPI.chat.latest'); + const aliceLatestChatWithBob = await userAlice.chat.latest(nftAccount2); + if (showAPIResponse) { + console.log(aliceLatestChatWithBob); + } + console.log('PushAPI.chat.latest | Response - 200 OK\n\n'); + }); + + it('should retrieve chat history correctly', async () => { + console.log('PushAPI.chat.history'); + const aliceChatHistoryWithBob = await userAlice.chat.history(nftAccount2); + if (showAPIResponse) { + console.log(aliceChatHistoryWithBob); + } + console.log('PushAPI.chat.history | Response - 200 OK\n\n'); + }); + + it('should send a chat message correctly', async () => { + console.log('PushAPI.chat.send'); + const aliceMessagesBob = await userAlice.chat.send(nftAccount2, { + content: 'Hello Bob!', + type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT, + }); + if (showAPIResponse) { + console.log(aliceMessagesBob); + } + console.log('PushAPI.chat.send | Response - 200 OK\n\n'); + }); + + it('should accept a chat correctly', async () => { + console.log('PushAPI.chat.accept'); + const bobAcceptsRequest = await userBob.chat.accept(nftAccount1); + if (showAPIResponse) { + console.log(bobAcceptsRequest); + } + console.log('PushAPI.chat.accept | Response - 200 OK\n\n'); + }); + + it('should reject a chat correctly', async () => { + console.log('PushAPI.chat.reject'); + await userKate.chat.send(nftAccount1, { + content: 'Sending malicious message', + type: CONSTANTS.CHAT.MESSAGE_TYPE.TEXT, + }); + const AliceRejectsRequest = await userAlice.chat.reject(nftAccount3); + if (showAPIResponse) { + console.log(AliceRejectsRequest); + } + console.log('PushAPI.chat.reject | Response - 200 OK\n\n'); + }); + + it('should block a chat correctly', async () => { + console.log('PushAPI.chat.block'); + const AliceBlocksBob = await userAlice.chat.block([nftAccount2]); + if (showAPIResponse) { + console.log(AliceBlocksBob); + } + console.log('PushAPI.chat.block | Response - 200 OK\n\n'); + }); + + it('should unblock a chat correctly', async () => { + console.log('PushAPI.chat.unblock'); + const AliceUnblocksBob = await userAlice.chat.unblock([nftAccount2]); + if (showAPIResponse) { + console.log(AliceUnblocksBob); + } + console.log('PushAPI.chat.unblock | Response - 200 OK\n\n'); + }); + + it('should create a group correctly', async () => { + console.log('PushAPI.group.create'); + const createdGroup = await userAlice.chat.group.create(groupName, { + description: groupDescription, + image: groupImage, + members: [randomWallet1, randomWallet2], + admins: [], + private: false, + }); + groupChatId = createdGroup.chatId; + if (showAPIResponse) { + console.log(createdGroup); + } + console.log('PushAPI.group.create | Response - 200 OK\n\n'); + }); + + it('should retrieve group permissions correctly', async () => { + console.log('PushAPI.group.permissions'); + const grouppermissions = await userAlice.chat.group.permissions( + groupChatId + ); + if (showAPIResponse) { + console.log(grouppermissions); + } + console.log('PushAPI.group.permissions | Response - 200 OK\n\n'); + }); + + it('should retrieve group info correctly', async () => { + console.log('PushAPI.group.info'); + const groupInfo = await userAlice.chat.group.info(groupChatId); + if (showAPIResponse) { + console.log(groupInfo); + } + console.log('PushAPI.group.info | Response - 200 OK\n\n'); + }); + + it('should update a group correctly', async () => { + console.log('PushAPI.group.update'); + const updatedGroup = await userAlice.chat.group.update(groupChatId, { + description: 'Updated Description', + }); + if (showAPIResponse) { + console.log(updatedGroup); + } + console.log('PushAPI.group.update | Response - 200 OK\n\n'); + }); + + it('should add a member to the group correctly', async () => { + console.log('PushAPI.group.add'); + const addMember = await userAlice.chat.group.add(groupChatId, { + role: 'MEMBER', + accounts: [randomWallet3], + }); + if (showAPIResponse) { + console.log(addMember); + } + console.log('PushAPI.group.add | Response - 200 OK\n\n'); + }); + + it('should remove a member from the group correctly', async () => { + console.log('PushAPI.group.remove'); + const removeMember = await userAlice.chat.group.remove(groupChatId, { + role: 'MEMBER', + accounts: [randomWallet3], + }); + if (showAPIResponse) { + console.log(removeMember); + } + console.log('PushAPI.group.remove | Response - 200 OK\n\n'); + }); + + it('should join a group correctly', async () => { + console.log('PushAPI.group.join'); + const joinGrp = await userBob.chat.group.join(groupChatId); + if (showAPIResponse) { + console.log(joinGrp); + } + console.log('PushAPI.group.join | Response - 200 OK\n\n'); + }); + + it('should leave a group correctly', async () => { + console.log('PushAPI.group.leave'); + const leaveGrp = await userBob.chat.group.leave(groupChatId); + if (showAPIResponse) { + console.log(leaveGrp); + } + console.log('PushAPI.group.leave | Response - 200 OK\n\n'); + }); + + it('should reject a group invite correctly', async () => { + console.log('PushAPI.group.reject'); + const sampleGrp = await userAlice.chat.group.create('Sample Grp', { + description: groupDescription, + image: groupImage, + members: [nftAccount2], + admins: [], + private: true, + }); + await userBob.chat.group.reject(sampleGrp.chatId); + console.log('PushAPI.group.reject | Response - 200 OK\n\n'); + }); +});