From 11a08017de405bbb61856f4d05fce1fdbf061a46 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Tue, 2 Jan 2024 18:16:05 +0530 Subject: [PATCH 1/4] add: ethers support (#952) * fix: fix viem support, add ethers support * fix: remove @pushprotocol/socket dependency from restapi * fix: changed ethers fn * fix: fix ether changes * fix: fix subscribev2 * chore: readme changes --- packages/restapi/README.md | 8 +- packages/restapi/package.json | 5 +- .../restapi/src/lib/channels/subscribe.ts | 61 +++-- .../restapi/src/lib/channels/subscribeV2.ts | 18 +- .../restapi/src/lib/channels/unsubscribe.ts | 63 +++-- .../restapi/src/lib/channels/unsubscribeV2.ts | 15 +- .../restapi/src/lib/chat/helpers/crypto.ts | 13 +- .../restapi/src/lib/chat/helpers/signature.ts | 42 ++-- .../restapi/src/lib/chat/helpers/wallet.ts | 35 ++- packages/restapi/src/lib/helpers/address.ts | 12 +- packages/restapi/src/lib/helpers/crypto.ts | 41 ++-- packages/restapi/src/lib/helpers/signer.ts | 131 ++++++---- packages/restapi/src/lib/payloads/helpers.ts | 10 +- .../src/lib/pushNotification/channel.ts | 73 ++---- .../src/lib/pushNotification/delegate.ts | 4 +- .../pushNotification/pushNotificationBase.ts | 232 +++++++++--------- packages/restapi/src/lib/types/index.ts | 27 +- .../tests/lib/benchmark/privateGroup.test.ts | 4 +- .../tests/lib/benchmark/publicGroup.test.ts | 4 +- .../tests/lib/notification/alias.test.ts | 8 +- .../tests/lib/notification/base.test.ts | 2 +- .../tests/lib/notification/channel.test.ts | 9 +- .../tests/lib/notification/delegate.test.ts | 40 +-- .../lib/notification/notification.test.ts | 56 ++--- .../tests/lib/stream/initialize.test.ts | 40 +-- packages/restapi/yarn.lock | 59 ++++- 26 files changed, 537 insertions(+), 475 deletions(-) diff --git a/packages/restapi/README.md b/packages/restapi/README.md index 3ac0492cf..780ce8d93 100644 --- a/packages/restapi/README.md +++ b/packages/restapi/README.md @@ -100,13 +100,13 @@ This package gives access to Push Protocol (Push Nodes) APIs. Visit [Developer D ## Installation ```bash -yarn add @pushprotocol/restapi@latest ethers@^5.6 +yarn add @pushprotocol/restapi@latest ethers ``` or ```bash -npm install @pushprotocol/restapi@latest ethers@^5.6 +npm install @pushprotocol/restapi@latest ethers ``` ## Import SDK @@ -170,7 +170,7 @@ const userAlice = await PushAPI.initialize(signer, { | Param | Type | Default | Remarks | | --------------------------------------- | ------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | -| `signer` | `SignerType` | - | EthersV5 or Viem Signer. | +| `signer` | `SignerType` | - | Ethers or Viem Signer. | | `options` \* | `PushAPIInitializeProps` | - | Optional configuration properties for initializing the PushAPI. | | `options.env` \* | `ENV` | `staging` | API env - 'prod', 'staging', 'dev'. | | `options.progressHook`\* | `(progress: ProgressHookType) => void` | - | A callback function to receive progress updates during initialization. | @@ -707,7 +707,7 @@ const userAlice = await PushAPI.initialize(signer, { | Param | Type | Default | Remarks | | --------------------------------------- | ------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | -| `signer` | `SignerType` | - | EthersV5 or Viem Signer. | +| `signer` | `SignerType` | - | Ethers or Viem Signer. | | `options` \* | `PushAPIInitializeProps` | - | Optional configuration properties for initializing the PushAPI. | | `options.env` \* | `ENV` | `staging` | API env - 'prod', 'staging', 'dev'. | | `options.progressHook`\* | `(progress: ProgressHookType) => void` | - | A callback function to receive progress updates during initialization. | diff --git a/packages/restapi/package.json b/packages/restapi/package.json index b35616bc1..b9e308eec 100644 --- a/packages/restapi/package.json +++ b/packages/restapi/package.json @@ -6,7 +6,7 @@ "registry": "https://registry.npmjs.org/" }, "peerDependencies": { - "ethers": "^5.6.8" + "ethers": "^5.0.0 || ^6.0.0" }, "dependencies": { "@ambire/signature-validator": "^1.3.1", @@ -19,7 +19,8 @@ "openpgp": "^5.5.0", "simple-peer": "^9.11.1", "socket.io-client": "^4.7.2", - "video-stream-merger": "^4.0.1" + "video-stream-merger": "^4.0.1", + "viem": "^1.20.3" }, "scripts": { "test": "TS_NODE_PROJECT='./tsconfig.mocha.json' NODE_OPTIONS='--loader ts-node/esm' mocha -r ts-node/register 'tests/**/*.test.ts' --timeout 1200000 --require tests/root.ts --serial" diff --git a/packages/restapi/src/lib/channels/subscribe.ts b/packages/restapi/src/lib/channels/subscribe.ts index 6e84984b2..3b9c44145 100644 --- a/packages/restapi/src/lib/channels/subscribe.ts +++ b/packages/restapi/src/lib/channels/subscribe.ts @@ -1,30 +1,23 @@ -import axios from "axios"; -import { - getCAIPAddress, - getConfig, - getCAIPDetails, - signTypedData -} from '../helpers'; +import axios from 'axios'; +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; import { getTypeInformation, getDomainInformation, - getSubscriptionMessage + getSubscriptionMessage, } from './signature.helpers'; -import Constants, {ENV} from '../constants'; -import { SignerType } from "../types"; +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; export type SubscribeOptionsType = { signer: SignerType; channelAddress: string; userAddress: string; verifyingContractAddress?: string; env?: ENV; - onSuccess?: () => void - onError?: (err: Error) => void, -} + onSuccess?: () => void; + onError?: (err: Error) => void; +}; -export const subscribe = async ( - options: SubscribeOptionsType -) => { +export const subscribe = async (options: SubscribeOptionsType) => { const { signer, channelAddress, @@ -36,7 +29,11 @@ export const subscribe = async ( } = options || {}; try { - const _channelAddress = await getCAIPAddress(env, channelAddress, 'Channel'); + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); const channelCAIPDetails = getCAIPDetails(_channelAddress); if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); @@ -44,11 +41,14 @@ export const subscribe = async ( const chainId = parseInt(channelCAIPDetails.networkId, 10); const _userAddress = await getCAIPAddress(env, userAddress, 'User'); - + const userCAIPDetails = getCAIPDetails(_userAddress); if (!userCAIPDetails) throw Error('Invalid User CAIP!'); - const { API_BASE_URL,EPNS_COMMUNICATOR_CONTRACT } = getConfig(env, channelCAIPDetails); + const { API_BASE_URL, EPNS_COMMUNICATOR_CONTRACT } = getConfig( + env, + channelCAIPDetails + ); const requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/subscribe`; @@ -59,17 +59,23 @@ export const subscribe = async ( ); // get type information - const typeInformation = getTypeInformation("Subscribe"); + const typeInformation = getTypeInformation('Subscribe'); // get message const messageInformation = getSubscriptionMessage( channelCAIPDetails.address, userCAIPDetails.address, - "Subscribe" + 'Subscribe' ); // sign a message using EIP712 - const signature = await signTypedData(signer, domainInformation, typeInformation, messageInformation, "Subscribe"); + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation as any, + messageInformation, + 'Subscribe' + ); const verificationProof = signature; // might change @@ -78,7 +84,7 @@ export const subscribe = async ( message: { ...messageInformation, channel: _channelAddress, - subscriber: _userAddress + subscriber: _userAddress, }, }; @@ -86,10 +92,13 @@ export const subscribe = async ( if (typeof onSuccess === 'function') onSuccess(); - return { status: "success", message: "successfully opted into channel" }; + return { status: 'success', message: 'successfully opted into channel' }; } catch (err) { if (typeof onError === 'function') onError(err as Error); - return { status: "error", message: err instanceof Error ? err.message : JSON.stringify(err) }; + return { + status: 'error', + message: err instanceof Error ? err.message : JSON.stringify(err), + }; } -} \ No newline at end of file +}; diff --git a/packages/restapi/src/lib/channels/subscribeV2.ts b/packages/restapi/src/lib/channels/subscribeV2.ts index b86b8e937..a710e56cc 100644 --- a/packages/restapi/src/lib/channels/subscribeV2.ts +++ b/packages/restapi/src/lib/channels/subscribeV2.ts @@ -1,10 +1,5 @@ import axios from 'axios'; -import { - getCAIPAddress, - getConfig, - getCAIPDetails, - signTypedData, -} from '../helpers'; +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; import { getDomainInformation, getTypeInformationV2, @@ -77,8 +72,8 @@ export const subscribeV2 = async (options: SubscribeOptionsV2Type) => { ), }; // sign a message using EIP712 - const signature = await signTypedData( - signer, + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( domainInformation, typeInformation, messageInformation, @@ -89,9 +84,7 @@ export const subscribeV2 = async (options: SubscribeOptionsV2Type) => { const body = { verificationProof: `eip712v2:${verificationProof}`, - message: - messageInformation.data, - + message: messageInformation.data, }; const res = await axios.post(requestUrl, body); @@ -100,11 +93,10 @@ export const subscribeV2 = async (options: SubscribeOptionsV2Type) => { return { status: res.status, message: 'successfully opted into channel' }; } catch (err: any) { - if (typeof onError === 'function') onError(err as Error); return { - status: err?.response?.status?? '' , + status: err?.response?.status ?? '', message: err instanceof Error ? err.message : JSON.stringify(err), }; } diff --git a/packages/restapi/src/lib/channels/unsubscribe.ts b/packages/restapi/src/lib/channels/unsubscribe.ts index 554fc6bf9..342f9d037 100644 --- a/packages/restapi/src/lib/channels/unsubscribe.ts +++ b/packages/restapi/src/lib/channels/unsubscribe.ts @@ -1,31 +1,24 @@ -import axios from "axios"; -import { - getCAIPAddress, - getConfig, - getCAIPDetails, - signTypedData -} from '../helpers'; +import axios from 'axios'; +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; import { getTypeInformation, getDomainInformation, - getSubscriptionMessage + getSubscriptionMessage, } from './signature.helpers'; -import Constants, {ENV} from '../constants'; -import { SignerType } from "../types"; - +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; + export type UnSubscribeOptionsType = { signer: SignerType; channelAddress: string; userAddress: string; verifyingContractAddress?: string; env?: ENV; - onSuccess?: () => void - onError?: (err: Error) => void, -} + onSuccess?: () => void; + onError?: (err: Error) => void; +}; -export const unsubscribe = async ( - options: UnSubscribeOptionsType -) => { +export const unsubscribe = async (options: UnSubscribeOptionsType) => { const { signer, channelAddress, @@ -37,7 +30,11 @@ export const unsubscribe = async ( } = options || {}; try { - const _channelAddress = await getCAIPAddress(env, channelAddress, 'Channel'); + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); const channelCAIPDetails = getCAIPDetails(_channelAddress); if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); @@ -45,11 +42,14 @@ export const unsubscribe = async ( const chainId = parseInt(channelCAIPDetails.networkId, 10); const _userAddress = await getCAIPAddress(env, userAddress, 'User'); - + const userCAIPDetails = getCAIPDetails(_userAddress); if (!userCAIPDetails) throw Error('Invalid User CAIP!'); - const { API_BASE_URL,EPNS_COMMUNICATOR_CONTRACT } = getConfig(env, channelCAIPDetails); + const { API_BASE_URL, EPNS_COMMUNICATOR_CONTRACT } = getConfig( + env, + channelCAIPDetails + ); const requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/unsubscribe`; @@ -60,17 +60,23 @@ export const unsubscribe = async ( ); // get type information - const typeInformation = getTypeInformation("Unsubscribe"); + const typeInformation = getTypeInformation('Unsubscribe'); // get message const messageInformation = getSubscriptionMessage( channelCAIPDetails.address, userCAIPDetails.address, - "Unsubscribe" + 'Unsubscribe' ); // sign a message using EIP712 - const signature = await signTypedData(signer, domainInformation, typeInformation, messageInformation, "Unsubscribe"); + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation as any, + messageInformation, + 'Unsubscribe' + ); const verificationProof = signature; // might change @@ -79,7 +85,7 @@ export const unsubscribe = async ( message: { ...messageInformation, channel: _channelAddress, - unsubscriber: _userAddress + unsubscriber: _userAddress, }, }; @@ -87,10 +93,13 @@ export const unsubscribe = async ( if (typeof onSuccess === 'function') onSuccess(); - return { status: "success", message: "successfully opted out channel" }; + return { status: 'success', message: 'successfully opted out channel' }; } catch (err) { if (typeof onError === 'function') onError(err as Error); - return { status: "error", message: err instanceof Error ? err.message : JSON.stringify(err) }; + return { + status: 'error', + message: err instanceof Error ? err.message : JSON.stringify(err), + }; } -} \ No newline at end of file +}; diff --git a/packages/restapi/src/lib/channels/unsubscribeV2.ts b/packages/restapi/src/lib/channels/unsubscribeV2.ts index f792fb782..743d2420f 100644 --- a/packages/restapi/src/lib/channels/unsubscribeV2.ts +++ b/packages/restapi/src/lib/channels/unsubscribeV2.ts @@ -1,14 +1,7 @@ import axios from 'axios'; +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; import { - getCAIPAddress, - getConfig, - getCAIPDetails, - signTypedData, -} from '../helpers'; -import { - getTypeInformation, getDomainInformation, - getSubscriptionMessage, getTypeInformationV2, getSubscriptionMessageV2, } from './signature.helpers'; @@ -79,8 +72,8 @@ export const unsubscribeV2 = async (options: UnSubscribeOptionsV2Type) => { }; // sign a message using EIP712 - const signature = await signTypedData( - signer, + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( domainInformation, typeInformation, messageInformation, @@ -103,7 +96,7 @@ export const unsubscribeV2 = async (options: UnSubscribeOptionsV2Type) => { if (typeof onError === 'function') onError(err as Error); return { - status: err?.response?.status?? '' , + status: err?.response?.status ?? '', message: err instanceof Error ? err.message : JSON.stringify(err), }; } diff --git a/packages/restapi/src/lib/chat/helpers/crypto.ts b/packages/restapi/src/lib/chat/helpers/crypto.ts index d2787955e..411c3a450 100644 --- a/packages/restapi/src/lib/chat/helpers/crypto.ts +++ b/packages/restapi/src/lib/chat/helpers/crypto.ts @@ -13,12 +13,10 @@ import { } from '../../types'; import { get } from '../../user'; import { + Signer, decryptPGPKey, decryptWithWalletRPCMethod, isValidETHAddress, - walletToPCAIP10, - signTypedData, - signMessage, } from '../../helpers'; import { get as getUser } from '../../user'; import { createUserService } from './service'; @@ -363,7 +361,8 @@ export const getEip191Signature = async ( const _signer = wallet?.signer; // EIP191 signature - const signature = await signMessage(_signer, message); + const pushSigner = new Signer(_signer); + const signature = await pushSigner.signMessage(message); const sigType = version === 'v1' ? 'eip191' : 'eip191v2'; return { verificationProof: `${sigType}:${signature}` }; }; @@ -381,17 +380,17 @@ export const getEip712Signature = async ( const typeInformation = getTypeInformation(); const _signer = wallet?.signer; + const pushSigner = new Signer(_signer); let chainId: number; try { - chainId = await _signer.getChainId(); + chainId = await pushSigner.getChainId(); } catch (err) { chainId = 1; } const domain = getDomainInformation(chainId); // sign a message using EIP712 - const signedMessage = await signTypedData( - _signer, + const signedMessage = await pushSigner.signTypedData( isDomainEmpty ? {} : domain, typeInformation, { data: hash }, diff --git a/packages/restapi/src/lib/chat/helpers/signature.ts b/packages/restapi/src/lib/chat/helpers/signature.ts index acc9f2fa2..4bccfdb3e 100644 --- a/packages/restapi/src/lib/chat/helpers/signature.ts +++ b/packages/restapi/src/lib/chat/helpers/signature.ts @@ -2,9 +2,7 @@ import { recoverTypedSignature, SignTypedDataVersion, } from '@metamask/eth-sig-util'; -import * as ethers from 'ethers'; -import { hashMessage } from 'ethers/lib/utils'; -import { verifyMessage } from '@ambire/signature-validator'; +import * as viem from 'viem'; /** * @@ -138,26 +136,32 @@ export const verifyProfileSignature = async ( // EIP191 sig validation try { // EOA Wallet - const recoveredAddress = ethers.utils.recoverAddress( - hashMessage(signedData), - signature - ); + const recoveredAddress = await viem.recoverAddress({ + hash: viem.hashMessage(signedData), + signature: signature as `0x${string}`, + }); if (recoveredAddress.toLowerCase() === address.toLowerCase()) { return true; } else return false; } catch (err) { - try { - // SCW Wallet - const verificationResult: boolean = await verifyMessage({ - signer: address.toLowerCase(), - message: signedData, - signature: signature, - provider: ethers.getDefaultProvider(1), - }); - return verificationResult; - } catch (err) { - return false; - } + return false; + // TODO - Add support for SCW Wallet + + /** + * @todo - Add support for SCW Wallet + * @notice - verifyMessage does not work with EthersV6 + */ + // try { + // const verificationResult: boolean = await verifyMessage({ + // signer: address.toLowerCase(), + // message: signedData, + // signature: signature, + // provider: ethers.getDefaultProvider(1), + // }); + // return verificationResult; + // } catch (err) { + // return false; + // } } } }; diff --git a/packages/restapi/src/lib/chat/helpers/wallet.ts b/packages/restapi/src/lib/chat/helpers/wallet.ts index 1481e62bf..1935f188b 100644 --- a/packages/restapi/src/lib/chat/helpers/wallet.ts +++ b/packages/restapi/src/lib/chat/helpers/wallet.ts @@ -1,26 +1,25 @@ -import { pCAIP10ToWallet, getAddress } from "../../helpers"; -import { SignerType, walletType } from "../../types"; +import { Signer, pCAIP10ToWallet } from '../../helpers'; +import { SignerType, walletType } from '../../types'; -export const getWallet = (options: walletType): { - account: string | null, - signer: SignerType | null +export const getWallet = ( + options: walletType +): { + account: string | null; + signer: SignerType | null; } => { - const { - account, - signer - } = options || {}; + const { account, signer } = options || {}; return { account: account ? pCAIP10ToWallet(account) : account, - signer + signer, }; -} +}; -export const getAccountAddress = async (options: walletType): Promise => { - const { - account, - signer - } = options || {}; +export const getAccountAddress = async ( + options: walletType +): Promise => { + const { account, signer } = options || {}; - return account || (await getAddress(signer as SignerType)) || '' -} \ No newline at end of file + const pushSigner = new Signer(signer as SignerType); + return account || (await pushSigner.getAddress()) || ''; +}; diff --git a/packages/restapi/src/lib/helpers/address.ts b/packages/restapi/src/lib/helpers/address.ts index 917ba1b90..c8e2b40d5 100644 --- a/packages/restapi/src/lib/helpers/address.ts +++ b/packages/restapi/src/lib/helpers/address.ts @@ -1,4 +1,4 @@ -import * as ethers from 'ethers'; +import * as viem from 'viem'; import Constants, {ENV} from '../constants'; import { getUserDID } from '../chat/helpers'; @@ -11,18 +11,18 @@ export function isValidETHAddress(address: string) { if (address.includes('eip155:')) { const splittedAddress = address.split(':'); if(splittedAddress.length === 3){ - return ethers.utils.isAddress(splittedAddress[2]); + return viem.isAddress(splittedAddress[2]); } if(splittedAddress.length === 2) - return ethers.utils.isAddress(splittedAddress[1]); + return viem.isAddress(splittedAddress[1]); } - return ethers.utils.isAddress(address); + return viem.isAddress(address); } export function isValidNFTCAIP10Address (realCAIP10: string) { const walletComponent = realCAIP10.split(':'); if (isNaN(Number(walletComponent[1]))) return false - return (walletComponent.length === 3 && walletComponent[0] === 'eip155' && ethers.utils.isAddress(walletComponent[2])) + return (walletComponent.length === 3 && walletComponent[0] === 'eip155' && viem.isAddress(walletComponent[2])) } /** @@ -40,7 +40,7 @@ export const isValidCAIP10NFTAddress = (wallet: string): boolean => { Number(walletComponent[4]) > 0 && !isNaN(Number(walletComponent[2])) && Number(walletComponent[2]) > 0 && - ethers.utils.isAddress(walletComponent[3]) && + viem.isAddress(walletComponent[3]) && walletComponent[1] === 'eip155' ); } catch (err) { diff --git a/packages/restapi/src/lib/helpers/crypto.ts b/packages/restapi/src/lib/helpers/crypto.ts index cb2cb18fb..d5f949a34 100644 --- a/packages/restapi/src/lib/helpers/crypto.ts +++ b/packages/restapi/src/lib/helpers/crypto.ts @@ -4,7 +4,6 @@ import { getEncryptionPublicKey, } from '@metamask/eth-sig-util'; import * as CryptoJS from 'crypto-js'; -import { ethers } from 'ethers'; import { aesDecrypt, getAccountAddress, @@ -32,7 +31,9 @@ import { import { verifyProfileSignature } from '../chat/helpers/signature'; import { upgrade } from '../user/upgradeUser'; import PROGRESSHOOK from '../progressHook'; -import { getAddress } from './signer'; +import { Signer } from './signer'; +import * as viem from 'viem'; +import { mainnet } from 'viem/chains'; const KDFSaltSize = 32; // bytes const AESGCMNonceSize = 12; // property iv @@ -49,25 +50,29 @@ if (typeof window !== 'undefined' && window.crypto) { } } -/** DEPRECATED */ +/** + * @deprecated + */ export const getPublicKey = async (options: walletType): Promise => { const { account, signer } = options || {}; - const address: string = - account || (await getAddress(signer as SignerType)) || ''; - const metamaskProvider = new ethers.providers.Web3Provider( - (window as any).ethereum - ); - const web3Provider: any = signer?.provider || metamaskProvider; - - const keyB64 = await web3Provider.provider.request({ + const pushSigner = signer ? new Signer(signer) : undefined; + const address: string = account || (await pushSigner?.getAddress()) || ''; + const metamaskProvider = viem.createWalletClient({ + chain: mainnet, + transport: viem.custom((window as any).ethereum), + }); + const web3Provider: any = signer?.provider?.provider || metamaskProvider; + const keyB64 = await web3Provider.request({ method: 'eth_getEncryptionPublicKey', params: [address], }); return keyB64; }; -/** DEPRECATED */ -// x25519-xsalsa20-poly1305 enryption +/** + * @deprecated + * x25519-xsalsa20-poly1305 enryption + */ export const encryptV1 = ( text: string, encryptionPublicKey: string, @@ -146,10 +151,12 @@ export const decryptPGPKey = async (options: decryptPgpKeyProps) => { privateKey: wallet?.signer?.privateKey.substring(2), }); } else { - const metamaskProvider = new ethers.providers.Web3Provider( - (window as any).ethereum - ); - const web3Provider: any = signer?.provider || metamaskProvider; + const metamaskProvider = viem.createWalletClient({ + chain: mainnet, + transport: viem.custom((window as any).ethereum), + }); + const web3Provider: any = + signer?.provider?.provider || metamaskProvider; privateKey = await web3Provider.provider.request({ method: 'eth_decrypt', params: [encryptedPGPPrivateKey, address], diff --git a/packages/restapi/src/lib/helpers/signer.ts b/packages/restapi/src/lib/helpers/signer.ts index bb950e9fd..e7a96292e 100644 --- a/packages/restapi/src/lib/helpers/signer.ts +++ b/packages/restapi/src/lib/helpers/signer.ts @@ -1,60 +1,89 @@ -import { SignerType } from '../types'; +import { SignerType, viemSignerType } from '../types'; +import { TypedDataDomain, TypedDataField } from 'ethers'; -export const signMessage = async ( - signer: SignerType, - message: string -): Promise => { - // Check the signer type using type guards - if ('signMessage' in signer) { - // If the signer has a signMessage function with the ethersV5SignerType signature - if ('_signTypedData' in signer) { - // It's ethersV5SignerType, use its signMessage function - const signature = await signer.signMessage(message); - return signature; +export class Signer { + private signer: SignerType; + + constructor(signer: SignerType) { + this.signer = signer; + } + + /** + * Determine if the signer is a Viem signer + */ + isViemSigner(signer: SignerType): signer is viemSignerType { + return ( + typeof (signer as any).signTypedData === 'function' && + typeof (signer as any).getChainId === 'function' && + signer.signMessage.length === 1 && // Checking if the function takes one argument + (signer as any).signTypedData.length === 1 // Checking if the function takes one argument + ); + } + + async signMessage(message: string | Uint8Array): Promise { + if ( + 'signMessage' in this.signer && + typeof this.signer.signMessage === 'function' + ) { + if (this.isViemSigner(this.signer)) { + // Viem signer requires additional arguments + return this.signer.signMessage({ + message, + account: this.signer.account, + }); + } else { + // EthersV5 and EthersV6 + return this.signer.signMessage(message); + } } else { - // It's viemSignerType, use its signMessage function - const signature = await signer.signMessage({ - message, - account: signer.account, + throw new Error('Signer does not support signMessage'); + } + } + + async signTypedData( + domain: TypedDataDomain, + types: Record, + value: Record, + primaryType?: string + ): Promise { + if (this.isViemSigner(this.signer)) { + // Call Viem's signTypedData with its specific structure + return this.signer.signTypedData({ + domain: domain, + types: types, + primaryType: primaryType, + message: value, + account: this.signer.account, }); - return signature; + } else if ('_signTypedData' in this.signer) { + // ethersV5 signer uses _signTypedData + return this.signer._signTypedData(domain, types, value); + } else if ('signTypedData' in this.signer) { + // ethersV6 signer uses signTypedData + return this.signer.signTypedData(domain, types, value); + } else { + throw new Error('Signer does not support signTypedData'); } - } else { - throw new Error('Invalid signer type provided.'); } -}; -export const signTypedData = async ( - signer: SignerType, - domain: any, - types: any, - value: any, - primaryType: string -): Promise => { - // Check the signer type using type guards - if ('_signTypedData' in signer) { - // It's ethersV5SignerType, use its functions - const signature = await signer._signTypedData(domain, types, value); - return signature; - } else if ('signTypedData' in signer) { - // It's viemSignerType, use its functions - const signature = await signer.signTypedData({ - account: signer.account, - domain, - types, - primaryType: primaryType, - message: value, - }); - return signature; - } else { - throw new Error('Invalid signer type provided.'); + async getAddress(): Promise { + if (this.isViemSigner(this.signer)) { + return this.signer.account['address'] ?? ''; + } else { + return await this.signer.getAddress(); + } } -}; -export const getAddress = async (signer: SignerType): Promise => { - if ('getAddress' in signer) { - return await signer.getAddress(); - } else { - return signer.account['address'] ?? ''; + async getChainId(): Promise { + if (this.isViemSigner(this.signer)) { + // Viem signer has a direct method for getChainId + return this.signer.getChainId(); + } else if ('provider' in this.signer && this.signer.provider) { + // EthersV5 and EthersV6 + const network = await this.signer.provider.getNetwork(); + return Number(network.chainId); + } else { + return 1; // Return default chainId + } } -}; +} diff --git a/packages/restapi/src/lib/payloads/helpers.ts b/packages/restapi/src/lib/payloads/helpers.ts index 779154e6f..d6bb076f4 100644 --- a/packages/restapi/src/lib/payloads/helpers.ts +++ b/packages/restapi/src/lib/payloads/helpers.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { ENV } from '../constants'; -import { getCAIPAddress, signTypedData } from '../helpers'; +import { Signer, getCAIPAddress } from '../helpers'; import * as CryptoJS from 'crypto-js'; import { @@ -258,8 +258,8 @@ export async function getVerificationProof({ chainId: chainId, verifyingContract: verifyingContract, }; - const signature = await signTypedData( - signer, + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( domain, type, message, @@ -328,7 +328,9 @@ export function getSource( export function getCAIPFormat(chainId: number, address: string) { // EVM based chains if ( - [1, 11155111, 42, 137, 80001, 56, 97, 10, 420, 1442, 1101, 421613, 42161].includes(chainId) + [ + 1, 11155111, 42, 137, 80001, 56, 97, 10, 420, 1442, 1101, 421613, 42161, + ].includes(chainId) ) { return `eip155:${chainId}:${address}`; } diff --git a/packages/restapi/src/lib/pushNotification/channel.ts b/packages/restapi/src/lib/pushNotification/channel.ts index 824f5d52b..6e2794a17 100644 --- a/packages/restapi/src/lib/pushNotification/channel.ts +++ b/packages/restapi/src/lib/pushNotification/channel.ts @@ -20,7 +20,7 @@ import { getFallbackETHCAIPAddress, } from '../helpers'; import PROGRESSHOOK from '../progressHook'; -import { ethers } from 'ethers'; +import * as viem from 'viem'; import { PushNotificationBaseClass } from './pushNotificationBase'; import { Delegate } from './delegate'; @@ -153,17 +153,6 @@ export class Channel extends PushNotificationBaseClass { progressHook, } = options || {}; try { - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); - } - } else if ('signTypedData' in this.signer!) { - if (!this.coreContract.write) { - throw new Error('viem signer is not provided'); - } - } else { - throw new Error('Unsupported Signer'); - } // create push token instance let aliasInfo; // validate all the parameters and length @@ -175,11 +164,11 @@ export class Channel extends PushNotificationBaseClass { config.TOKEN_VIEM_NETWORK_MAP[this.env!] ); const balance = await this.fetchBalance(pushTokenContract, this.account!); - const fees = ethers.utils.parseUnits( + const fees = viem.parseUnits( config.MIN_TOKEN_BALANCE[this.env!].toString(), 18 ); - if (fees.gt(balance)) { + if (fees > balance) { throw new Error('Insufficient PUSH balance'); } // if alias is passed, check for the caip @@ -208,7 +197,7 @@ export class Channel extends PushNotificationBaseClass { this.account!, config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT ); - if (!allowanceAmount.gte(fees)) { + if (!(allowanceAmount >= fees)) { progressHook?.(PROGRESSHOOK['PUSH-CREATE-02'] as ProgressHookType); const approvalRes = await this.approveToken( pushTokenContract, @@ -222,7 +211,7 @@ export class Channel extends PushNotificationBaseClass { // generate the contract parameters const channelType = config.CHANNEL_TYPE['GENERAL']; const identity = '1+' + cid; - const identityBytes = ethers.utils.toUtf8Bytes(identity); + const identityBytes = viem.stringToBytes(identity); // call contract progressHook?.(PROGRESSHOOK['PUSH-CREATE-03'] as ProgressHookType); const createChannelRes = await this.createChannel( @@ -256,17 +245,6 @@ export class Channel extends PushNotificationBaseClass { try { // create push token instance let aliasInfo; - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); - } - } else if ('signTypedData' in this.signer!) { - if (!this.coreContract.write) { - throw new Error('viem signer is not provided'); - } - } else { - throw new Error('Unsupported Signer'); - } // validate all the parameters and length this.validateChannelParameters(options); // check for PUSH balance @@ -281,12 +259,12 @@ export class Channel extends PushNotificationBaseClass { this.coreContract, this.account! ); - const fees = ethers.utils.parseUnits( + const fees = viem.parseUnits( config.MIN_TOKEN_BALANCE[this.env!].toString(), 18 ); - const totalFees = fees.mul(counter); - if (totalFees.gt(balance)) { + const totalFees = fees * counter; + if (totalFees > balance) { throw new Error('Insufficient PUSH balance'); } // if alias is passed, check for the caip @@ -317,7 +295,7 @@ export class Channel extends PushNotificationBaseClass { config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT ); // if allowance is not greater than the fees, dont call approval again - if (!allowanceAmount.gte(totalFees)) { + if (!(allowanceAmount >= totalFees)) { progressHook?.(PROGRESSHOOK['PUSH-UPDATE-02'] as ProgressHookType); const approvalRes = await this.approveToken( pushTokenContract, @@ -330,7 +308,7 @@ export class Channel extends PushNotificationBaseClass { } // generate the contract parameters const identity = '1+' + cid; - const identityBytes = ethers.utils.toUtf8Bytes(identity); + const identityBytes = viem.stringToBytes(identity); // call contract progressHook?.(PROGRESSHOOK['PUSH-UPDATE-03'] as ProgressHookType); const updateChannelRes = await this.updateChannel( @@ -357,22 +335,11 @@ export class Channel extends PushNotificationBaseClass { verify = async (channelToBeVerified: string) => { try { this.checkSignerObjectExists(); - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); - } - } else if ('signTypedData' in this.signer!) { - if (!this.coreContract.write) { - throw new Error('viem signer is not provided'); - } - } else { - throw new Error('Unsupported Signer'); - } if (validateCAIP(channelToBeVerified)) { channelToBeVerified = channelToBeVerified.split(':')[2]; } // checks if it is a valid address - if (!ethers.utils.isAddress(channelToBeVerified)) { + if (!viem.isAddress(channelToBeVerified)) { throw new Error('Invalid channel address'); } const channelDetails = await this.info(this.account); @@ -396,18 +363,6 @@ export class Channel extends PushNotificationBaseClass { setting = async (configuration: NotificationSettings) => { try { this.checkSignerObjectExists(); - //TODO: create a separate function later - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); - } - } else if ('signTypedData' in this.signer!) { - if (!this.coreContract.write) { - throw new Error('viem signer is not provided'); - } - } else { - throw new Error('Unsupported Signer'); - } // check for PUSH balance const pushTokenContract = await this.createContractInstance( config.TOKEN[this.env!], @@ -415,11 +370,11 @@ export class Channel extends PushNotificationBaseClass { config.TOKEN_VIEM_NETWORK_MAP[this.env!] ); const balance = await this.fetchBalance(pushTokenContract, this.account!); - const fees = ethers.utils.parseUnits( + const fees = viem.parseUnits( config.MIN_TOKEN_BALANCE[this.env!].toString(), 18 ); - if (fees.gt(balance)) { + if (fees > balance) { throw new Error('Insufficient PUSH balance'); } const allowanceAmount = await this.fetchAllownace( @@ -428,7 +383,7 @@ export class Channel extends PushNotificationBaseClass { config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT ); // if allowance is not greater than the fees, dont call approval again - if (!allowanceAmount.gte(fees)) { + if (!(allowanceAmount >= fees)) { const approveRes = await this.approveToken( pushTokenContract, config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT, diff --git a/packages/restapi/src/lib/pushNotification/delegate.ts b/packages/restapi/src/lib/pushNotification/delegate.ts index 90d7ad0e2..ba548328e 100644 --- a/packages/restapi/src/lib/pushNotification/delegate.ts +++ b/packages/restapi/src/lib/pushNotification/delegate.ts @@ -56,7 +56,7 @@ export class Delegate extends PushNotificationBaseClass { if (validateCAIP(delegate)) { delegate = this.getAddressFromCaip(delegate); } - const networkDetails = await this.getChianId(this.signer!); + const networkDetails = await this.getChainId(this.signer!); const caip = `eip155:${networkDetails}`; if (!CONFIG[this.env!][caip] || !config.VIEM_CONFIG[this.env!][caip]) { throw new Error('Unsupported Chainid'); @@ -88,7 +88,7 @@ export class Delegate extends PushNotificationBaseClass { if (validateCAIP(delegate)) { delegate = this.getAddressFromCaip(delegate); } - const networkDetails = await this.getChianId(this.signer!); + const networkDetails = await this.getChainId(this.signer!); const caip = `eip155:${networkDetails}`; if (!CONFIG[this.env!][caip] || !config.VIEM_CONFIG[this.env!][caip]) { throw new Error('Unsupported Chainid'); diff --git a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts index b61435fa6..e022ce4ed 100644 --- a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts +++ b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts @@ -9,7 +9,7 @@ import { import * as config from '../config'; import { getAccountAddress } from '../chat/helpers'; import { IDENTITY_TYPE, NOTIFICATION_TYPE } from '../payloads/constants'; -import { ethers, Signer, BigNumber } from 'ethers'; +import { ethers, Signer as EthersSigner } from 'ethers'; import axios from 'axios'; import { createPublicClient, @@ -20,6 +20,7 @@ import { } from 'viem'; import * as PUSH_CHANNEL from '../channels'; import { + Signer, getAPIBaseUrls, getFallbackETHCAIPAddress, validateCAIP, @@ -70,25 +71,12 @@ export class PushNotificationBaseClass { let derivedAccount; let coreContract; if (signer) { - if (!('_signTypedData' in signer!) && !('signTypedData' in signer!)) { - throw new Error('Unsupported signer type'); - } else if ('_signTypedData' in signer) { - derivedAccount = await getAccountAddress({ - account: null, - signer: signer, - }); - if (signer?.provider) { - coreContract = new ethers.Contract( - config.CORE_CONFIG[env].EPNS_CORE_CONTRACT, - config.ABIS.CORE, - signer as unknown as Signer - ); - } - } else if ('signTypedData' in signer) { - derivedAccount = await getAccountAddress({ - account: null, - signer: signer, - }); + derivedAccount = await getAccountAddress({ + account: null, + signer: signer, + }); + const pushSigner = new Signer(signer); + if (pushSigner.isViemSigner(signer)) { const client = createPublicClient({ chain: config.TOKEN_VIEM_NETWORK_MAP[env], transport: http(), @@ -99,6 +87,12 @@ export class PushNotificationBaseClass { publicClient: client, walletClient: signer as unknown as WalletClient, }); + } else { + coreContract = new ethers.Contract( + config.CORE_CONFIG[env].EPNS_CORE_CONTRACT, + config.ABIS.CORE, + signer as unknown as EthersSigner + ); } } @@ -293,22 +287,12 @@ export class PushNotificationBaseClass { contractABI: any, network: Chain ) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } let contract: any; - if ( - !('_signTypedData' in this.signer!) && - !('signTypedData' in this.signer!) - ) { - throw new Error('Unsupported signer type'); - } else if ('_signTypedData' in this.signer) { - if (!this.signer?.provider) { - throw new Error('Provider is required'); - } - contract = new ethers.Contract( - contractAddress, - contractABI, - this.signer as unknown as Signer - ); - } else if ('signTypedData' in this.signer) { + const pushSigner = this.signer ? new Signer(this.signer) : null; + if (pushSigner?.isViemSigner(this.signer)) { const client = createPublicClient({ chain: network, transport: http(), @@ -320,28 +304,33 @@ export class PushNotificationBaseClass { walletClient: this.signer as unknown as WalletClient, }); } else { - throw new Error('Unsupported signer type'); + contract = new ethers.Contract( + contractAddress, + contractABI, + this.signer as unknown as EthersSigner + ); } return contract; } protected async fetchBalance(contract: any, userAddress: string) { - let balance: BigNumber; + if (!this.signer) { + throw new Error('Signer is not provided'); + } + let balance: bigint; + const pushSigner = new Signer(this.signer); try { - if ('_signTypedData' in this.signer!) { - balance = await contract!['balanceOf'](userAddress); - } else if ('signTypedData' in this.signer!) { + if (pushSigner.isViemSigner(this.signer)) { const balanceInBigInt = await contract.read.balanceOf({ args: [userAddress], }); - balance = ethers.BigNumber.from(balanceInBigInt); + balance = BigInt(balanceInBigInt); } else { - throw new Error('Unsupported signer'); + balance = await contract.balanceOf(userAddress); } return balance; - } catch (error) { - console.error(error); - throw new Error(JSON.stringify(error)); + } catch (err) { + throw new Error(JSON.stringify(err)); } } @@ -350,17 +339,20 @@ export class PushNotificationBaseClass { userAddress: string, spenderAddress: string ) { - let allowance: BigNumber; + if (!this.signer) { + throw new Error('Signer is not provided'); + } + + const pushSigner = new Signer(this.signer); + let allowance: bigint; try { - if ('_signTypedData' in this.signer!) { + if (!pushSigner.isViemSigner(this.signer)) { allowance = await contract!['allowance'](userAddress, spenderAddress); - } else if ('signTypedData' in this.signer!) { + } else { const allowanceInBigInt = await contract.read.allowance({ args: [userAddress, spenderAddress], }); - allowance = ethers.BigNumber.from(allowanceInBigInt); - } else { - throw new Error('Unsupported signer'); + allowance = BigInt(allowanceInBigInt); } return allowance; } catch (error) { @@ -369,20 +361,22 @@ export class PushNotificationBaseClass { } protected async fetchUpdateCounter(contract: any, userAddress: string) { - let count: BigNumber; + if (!this.signer) { + throw new Error('Signer is not provided'); + } + let count: bigint; + const pushSigner = new Signer(this.signer); try { - if ('_signTypedData' in this.signer!) { + if (!pushSigner.isViemSigner(this.signer)) { count = await contract!['channelUpdateCounter'](userAddress); - } else if ('signTypedData' in this.signer!) { + } else { const countInBigInt = await contract.read.channelUpdateCounter({ args: [userAddress], }); - count = ethers.BigNumber.from(countInBigInt); - } else { - throw new Error('Unsupported signer'); + count = BigInt(countInBigInt); } // add one and return the count - return count.add(ethers.BigNumber.from(1)); + return count + BigInt(1); } catch (error) { throw new Error(JSON.stringify(error)); } @@ -391,17 +385,22 @@ export class PushNotificationBaseClass { protected async approveToken( contract: any, spenderAddress: string, - amount: string | BigNumber + amount: string | bigint ) { try { - if ('_signTypedData' in this.signer!) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + + if (!pushSigner.isViemSigner(this.signer)) { if (!this.signer || !this.signer.provider) { throw new Error('ethers provider/signer is not provided'); } const approvalTrxPromise = contract!['approve'](spenderAddress, amount); const approvalTrx = await approvalTrxPromise; await this.signer?.provider?.waitForTransaction(approvalTrx.hash); - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -409,8 +408,6 @@ export class PushNotificationBaseClass { args: [spenderAddress, amount], }); const approvalTrxRes = await approvalTrxPromise; - } else { - throw new Error('Unsupported signer'); } return true; } catch (error) { @@ -423,14 +420,15 @@ export class PushNotificationBaseClass { contract: any, channelType: number, identityBytes: Uint8Array, - fees: BigNumber + fees: bigint ) { let createChannelRes; try { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!this.signer) { + throw new Error('Signer is not provided'); } - if ('_signTypedData' in this.signer!) { + const pushSigner = new Signer(this.signer); + if (!pushSigner.isViemSigner(this.signer)) { const createChannelPromise = contract!['createChannelWithPUSH']( channelType, identityBytes, @@ -449,7 +447,7 @@ export class PushNotificationBaseClass { throw new Error('Something Went wrong while creating your channel'); } createChannelRes = createChannelTrx.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -458,7 +456,6 @@ export class PushNotificationBaseClass { }); createChannelRes = await createChannelPromise; } - return createChannelRes; } catch (error: any) { throw new Error(error?.message); @@ -469,14 +466,15 @@ export class PushNotificationBaseClass { contract: any, account: string, identityBytes: Uint8Array, - fees: BigNumber + fees: bigint ) { let updateChannelRes; try { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!this.signer) { + throw new Error('Signer is not provided'); } - if ('_signTypedData' in this.signer!) { + const pushSigner = new Signer(this.signer); + if (!pushSigner.isViemSigner(this.signer)) { const updateChannelPromise = contract!['updateChannelMeta']( account, identityBytes, @@ -494,7 +492,7 @@ export class PushNotificationBaseClass { throw new Error('Something Went wrong while updating your channel'); } updateChannelRes = updateChannelTrx.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -512,16 +510,20 @@ export class PushNotificationBaseClass { protected async verifyChannel(contract: any, channelToBeVerified: string) { try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); let verifyTrxRes; - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); } const verifyTrxPromise = contract!['verify'](channelToBeVerified); const verifyTrx = await verifyTrxPromise; await this.signer?.provider?.waitForTransaction(verifyTrx.hash); verifyTrxRes = verifyTrx.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -529,8 +531,6 @@ export class PushNotificationBaseClass { args: [channelToBeVerified], }); verifyTrxRes = await verifyTrxPromise; - } else { - throw new Error('Unsupported signer'); } return verifyTrxRes; } catch (error: any) { @@ -543,13 +543,17 @@ export class PushNotificationBaseClass { numberOfSettings: number, settings: string, description: string, - fees: BigNumber + fees: bigint ) { try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); let createSettingsRes; - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); } const createSettingsPromise = contract!['createChannelSettings']( numberOfSettings, @@ -560,7 +564,7 @@ export class PushNotificationBaseClass { const createSettings = await createSettingsPromise; await this.signer?.provider?.waitForTransaction(createSettings.hash); createSettingsRes = createSettings.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -568,8 +572,6 @@ export class PushNotificationBaseClass { args: [numberOfSettings, settings, description, fees], }); createSettingsRes = await createSettingsTrxPromise; - } else { - throw new Error('Unsupported signer'); } return createSettingsRes; } catch (error: any) { @@ -579,16 +581,20 @@ export class PushNotificationBaseClass { protected async addDelegator(contract: any, delegatee: string) { try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); let addDelegateRes; - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); } const addDelegateTrxPromise = contract!['addDelegate'](delegatee); const addDelegateTrx = await addDelegateTrxPromise; await this.signer?.provider?.waitForTransaction(addDelegateTrx.hash); addDelegateRes = addDelegateTrx.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -596,8 +602,6 @@ export class PushNotificationBaseClass { args: [delegatee], }); addDelegateRes = await addDelegateTrxPromise; - } else { - throw new Error('Unsupported signer'); } return addDelegateRes; } catch (error: any) { @@ -607,16 +611,20 @@ export class PushNotificationBaseClass { protected async removeDelegator(contract: any, delegatee: string) { try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); let removeDelegateRes; - if ('_signTypedData' in this.signer!) { - if (!this.signer || !this.signer.provider) { - throw new Error('ethers provider/signer is not provided'); + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); } const removeDelegateTrxPromise = contract!['removeDelegate'](delegatee); const removeDelegateTrx = await removeDelegateTrxPromise; await this.signer?.provider?.waitForTransaction(removeDelegateTrx.hash); removeDelegateRes = removeDelegateTrx.hash; - } else if ('signTypedData' in this.signer!) { + } else { if (!contract.write) { throw new Error('viem signer is not provided'); } @@ -624,8 +632,6 @@ export class PushNotificationBaseClass { args: [delegatee], }); removeDelegateRes = await removeDelegateTrxPromise; - } else { - throw new Error('Unsupported signer'); } return removeDelegateRes; } catch (error: any) { @@ -633,30 +639,12 @@ export class PushNotificationBaseClass { } } - protected async getChianId(signer: SignerType) { - let chainId; - const isProviderExists = await this.checkProvider(signer); - if (!isProviderExists) { - throw new Error('Provider doesnt exists'); - } - if ('_signTypedData' in signer!) { - const chainDetails = await signer?.provider?.getNetwork(); - chainId = chainDetails?.chainId; - } else if ('signTypedData' in signer!) { - chainId = await signer.getChainId(); - } - return chainId; - } - - protected async checkProvider(signer: SignerType) { - let res = false; - if ('_signTypedData' in signer!) { - res = signer && signer?.provider ? true : false; - } else if ('signTypedData' in signer!) { - const chainId = await signer.getChainId(); - res = !!chainId; + protected async getChainId(signer: SignerType) { + if (!this.signer) { + throw new Error('Signer is not provided'); } - return res; + const pushSigner = new Signer(this.signer); + return pushSigner.getChainId(); } protected async uploadToIPFSViaPushNode(data: string): Promise { diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index ae8ddded1..c4d5d86a7 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -1,4 +1,4 @@ -import { Bytes, TypedDataDomain, TypedDataField, providers } from 'ethers'; +import { TypedDataDomain, TypedDataField, Provider } from 'ethers'; import { ADDITIONAL_META_TYPE, IDENTITY_TYPE, @@ -661,12 +661,24 @@ export type ethersV5SignerType = { types: Record>, value: Record ) => Promise; - getChainId: () => Promise; getAddress: () => Promise; - signMessage: (message: Bytes | string) => Promise; + signMessage: (message: Uint8Array | string) => Promise; + privateKey?: string; + provider?: Provider | null; +}; + +export type ethersV6SignerType = { + signTypedData: ( + domain: TypedDataDomain, + types: Record>, + value: Record + ) => Promise; + getAddress: () => Promise; + signMessage: (message: Uint8Array | string) => Promise; privateKey?: string; - provider?: providers.Provider; + provider?: Provider | null; }; + export type viemSignerType = { signTypedData: (args: { account: any; @@ -683,10 +695,13 @@ export type viemSignerType = { }) => Promise<`0x${string}`>; account: { [key: string]: any }; privateKey?: string; - provider?: providers.Provider; + provider?: Provider | null; }; -export type SignerType = ethersV5SignerType | viemSignerType; +export type SignerType = + | ethersV5SignerType + | ethersV6SignerType + | viemSignerType; export type EnvOptionsType = { env?: ENV; diff --git a/packages/restapi/tests/lib/benchmark/privateGroup.test.ts b/packages/restapi/tests/lib/benchmark/privateGroup.test.ts index dc25729ae..d7b0d7ffd 100644 --- a/packages/restapi/tests/lib/benchmark/privateGroup.test.ts +++ b/packages/restapi/tests/lib/benchmark/privateGroup.test.ts @@ -28,7 +28,7 @@ describe.skip('Private Groups', () => { account = `eip155:${signer.address}`; userAlice = await PushAPI.initialize(signer, { env: _env, - featureTag: 'ALPHA', + alpha: { feature: [Constants.ALPHA_FEATURES.SCALABILITY_V2] }, }); // UserBob @@ -37,7 +37,7 @@ describe.skip('Private Groups', () => { account2 = `eip155:${signer2.address}`; userBob = await PushAPI.initialize(signer2, { env: _env, - featureTag: 'ALPHA', + alpha: { feature: [Constants.ALPHA_FEATURES.SCALABILITY_V2] }, }); }); diff --git a/packages/restapi/tests/lib/benchmark/publicGroup.test.ts b/packages/restapi/tests/lib/benchmark/publicGroup.test.ts index 4420e77bd..f3b838235 100644 --- a/packages/restapi/tests/lib/benchmark/publicGroup.test.ts +++ b/packages/restapi/tests/lib/benchmark/publicGroup.test.ts @@ -28,7 +28,7 @@ describe.skip('Public Groups', () => { account = `eip155:${signer.address}`; userAlice = await PushAPI.initialize(signer, { env: _env, - featureTag: 'ALPHA', + alpha: { feature: [Constants.ALPHA_FEATURES.SCALABILITY_V2] }, }); // UserBob @@ -37,7 +37,7 @@ describe.skip('Public Groups', () => { account2 = `eip155:${signer2.address}`; userBob = await PushAPI.initialize(signer2, { env: _env, - featureTag: 'ALPHA', + alpha: { feature: [Constants.ALPHA_FEATURES.SCALABILITY_V2] }, }); }); diff --git a/packages/restapi/tests/lib/notification/alias.test.ts b/packages/restapi/tests/lib/notification/alias.test.ts index bada09f36..653abc724 100644 --- a/packages/restapi/tests/lib/notification/alias.test.ts +++ b/packages/restapi/tests/lib/notification/alias.test.ts @@ -18,11 +18,9 @@ describe('PushAPI.alias functionality', () => { beforeEach(async () => { signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); account1 = await signer1.getAddress(); - - const provider = new ethers.providers.JsonRpcProvider( - // PUBLIC RPC - 'https://rpc.sepolia.org' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, diff --git a/packages/restapi/tests/lib/notification/base.test.ts b/packages/restapi/tests/lib/notification/base.test.ts index e37391a72..b9c2a7e96 100644 --- a/packages/restapi/tests/lib/notification/base.test.ts +++ b/packages/restapi/tests/lib/notification/base.test.ts @@ -13,7 +13,7 @@ import { } from 'viem'; import { abi } from './tokenABI'; import { goerli, polygonMumbai } from 'viem/chains'; -import { BigNumber, ethers } from 'ethers'; +import { ethers } from 'ethers'; enum ENV { PROD = 'prod', diff --git a/packages/restapi/tests/lib/notification/channel.test.ts b/packages/restapi/tests/lib/notification/channel.test.ts index 8475adcd6..2ffa8a47c 100644 --- a/packages/restapi/tests/lib/notification/channel.test.ts +++ b/packages/restapi/tests/lib/notification/channel.test.ts @@ -22,10 +22,9 @@ describe('PushAPI.channel functionality', () => { signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); account1 = await signer1.getAddress(); - const provider = new ethers.providers.JsonRpcProvider( - // PUBLIC RPC - 'https://rpc.sepolia.org' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, @@ -410,6 +409,8 @@ describe('PushAPI.channel functionality', () => { describe.skip('channel :: create', () => { it('Should create channel', async () => { + const channelInfo = await userKate.channel.info(); + if (channelInfo) return; // skip if already exists const res = await userKate.channel.create({ name: 'SDK Test', description: 'Testing new description', diff --git a/packages/restapi/tests/lib/notification/delegate.test.ts b/packages/restapi/tests/lib/notification/delegate.test.ts index fc3c21ac7..bb14b4162 100644 --- a/packages/restapi/tests/lib/notification/delegate.test.ts +++ b/packages/restapi/tests/lib/notification/delegate.test.ts @@ -19,9 +19,9 @@ describe('PushAPI.delegate functionality', () => { signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); account1 = await signer1.getAddress(); - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc.sepolia.org' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, @@ -81,10 +81,13 @@ describe('PushAPI.delegate functionality', () => { it('With viem signer: Should add delegate', async () => { // create polygon mumbai provider - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc-mumbai.maticvigil.com' - ); - + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ) + : new (ethers as any).JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, provider @@ -99,9 +102,13 @@ describe('PushAPI.delegate functionality', () => { it('With viem signer: Should add delegate', async () => { // create polygon mumbai provider - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc-mumbai.maticvigil.com' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ) + : new (ethers as any).JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, @@ -152,10 +159,13 @@ describe('PushAPI.delegate functionality', () => { it('With viem signer: Should remove delegate', async () => { // create polygon mumbai provider - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc-mumbai.maticvigil.com' - ); - + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ) + : new (ethers as any).JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, provider @@ -178,7 +188,7 @@ describe('PushAPI.delegate functionality', () => { channel: '0xD8634C39BBFd4033c0d3289C4515275102423681', }); // console.log(res) - expect(res).not.null + expect(res).not.null; }); it('Without signer : Should fetch delegates', async () => { diff --git a/packages/restapi/tests/lib/notification/notification.test.ts b/packages/restapi/tests/lib/notification/notification.test.ts index 0a32ba32a..733ef3a19 100644 --- a/packages/restapi/tests/lib/notification/notification.test.ts +++ b/packages/restapi/tests/lib/notification/notification.test.ts @@ -20,14 +20,12 @@ describe('PushAPI.notification functionality', () => { let viemSigner: any; let userViem: PushAPI; beforeEach(async () => { - signer1 = new ethers.Wallet( - `0x${process.env['WALLET_PRIVATE_KEY']}` - ); + signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); account1 = await signer1.getAddress(); - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc.sepolia.org' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, @@ -35,9 +33,7 @@ describe('PushAPI.notification functionality', () => { ); account2 = await signer2.getAddress(); viemSigner = createWalletClient({ - account: privateKeyToAccount( - `0x${process.env['WALLET_PRIVATE_KEY']}` - ), + account: privateKeyToAccount(`0x${process.env['WALLET_PRIVATE_KEY']}`), chain: sepolia, transport: http(), }); @@ -51,7 +47,7 @@ describe('PushAPI.notification functionality', () => { LOCAL = 'local', } // initialisation with signer and provider - userKate = await PushAPI.initialize(signer2, {env:ENV.DEV}); + userKate = await PushAPI.initialize(signer2, { env: ENV.DEV }); // initialisation with signer userAlice = await PushAPI.initialize(signer1); // TODO: remove signer1 after signer becomes optional @@ -92,7 +88,7 @@ describe('PushAPI.notification functionality', () => { it('Should return feeds when signer with provider is used', async () => { const response = await userKate.notification.list('SPAM', { - account: "0xD8634C39BBFd4033c0d3289C4515275102423681" + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', }); // console.log(response) expect(response).not.null; @@ -117,23 +113,20 @@ describe('PushAPI.notification functionality', () => { describe('notification :: subscribe', () => { beforeEach(async () => { - // await userAlice.notification.unsubscribe( - // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' - // ); - - // await userKate.notification.unsubscribe( - // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' - // ); - // }); - - // afterEach(async () => { - // await userAlice.notification.unsubscribe( - // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' - // ); - - // await userKate.notification.unsubscribe( - // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' - // ); + // await userAlice.notification.unsubscribe( + // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + // ); + // await userKate.notification.unsubscribe( + // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + // ); + // }); + // afterEach(async () => { + // await userAlice.notification.unsubscribe( + // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + // ); + // await userKate.notification.unsubscribe( + // 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + // ); }); it.skip('Without signer object: should throw error', async () => { await expect(() => @@ -207,7 +200,7 @@ describe('PushAPI.notification functionality', () => { ], } ); - console.log(res) + console.log(res); expect(res).not.null; }); @@ -235,17 +228,16 @@ describe('PushAPI.notification functionality', () => { const response = await userAlice.notification.subscriptions({ account: 'eip155:80001:0xD8634C39BBFd4033c0d3289C4515275102423681', }); - // console.log(response); + // console.log(response); expect(response).not.null; expect(response.lenth).not.equal(0); }); - it('Signer with account: Should return response', async () => { const response = await userKate.notification.subscriptions({ account: '0xD8634C39BBFd4033c0d3289C4515275102423681', }); - // console.log(response); + // console.log(response); expect(response).not.null; expect(response.lenth).not.equal(0); }); diff --git a/packages/restapi/tests/lib/stream/initialize.test.ts b/packages/restapi/tests/lib/stream/initialize.test.ts index 6b517c1df..6346fae44 100644 --- a/packages/restapi/tests/lib/stream/initialize.test.ts +++ b/packages/restapi/tests/lib/stream/initialize.test.ts @@ -14,30 +14,32 @@ describe('PushStream.initialize functionality', () => { it('Should initialize new stream and listen to events', async () => { const MESSAGE = 'Hey There!!!'; - const provider = ethers.getDefaultProvider(); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); const WALLET = ethers.Wallet.createRandom(); const signer = new ethers.Wallet(WALLET.privateKey, provider); const user = await PushAPI.initialize(signer, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET2 = ethers.Wallet.createRandom(); const signer2 = new ethers.Wallet(WALLET2.privateKey, provider); const user2 = await PushAPI.initialize(signer2, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET3 = ethers.Wallet.createRandom(); const signer3 = new ethers.Wallet(WALLET3.privateKey, provider); const user3 = await PushAPI.initialize(signer3, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET4 = ethers.Wallet.createRandom(); const signer4 = new ethers.Wallet(WALLET4.privateKey, provider); const user4 = await PushAPI.initialize(signer4, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const GROUP_RULES = { @@ -285,7 +287,7 @@ describe('PushStream.initialize functionality', () => { onError: () => { console.error('opt in error'); }, - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -304,7 +306,7 @@ describe('PushStream.initialize functionality', () => { img: '', }, channel: `eip155:5:${channelAddress}`, // your channel address - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -318,7 +320,7 @@ describe('PushStream.initialize functionality', () => { onError: () => { console.error('opt out error'); }, - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -338,7 +340,7 @@ describe('PushStream.initialize functionality', () => { }, recipients: `eip155:5:${signer.address}`, channel: `eip155:5:${channelAddress}`, // your channel address - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -440,30 +442,32 @@ describe('PushStream.initialize functionality', () => { it('Should initialize new stream(readonly) and listen to events', async () => { const MESSAGE = 'Hey There!!!'; - const provider = ethers.getDefaultProvider(); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider('https://rpc.sepolia.org') + : new (ethers as any).JsonRpcProvider('https://rpc.sepolia.org'); const WALLET = ethers.Wallet.createRandom(); const signer = new ethers.Wallet(WALLET.privateKey, provider); const user = await PushAPI.initialize(signer, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET2 = ethers.Wallet.createRandom(); const signer2 = new ethers.Wallet(WALLET2.privateKey, provider); const user2 = await PushAPI.initialize(signer2, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET3 = ethers.Wallet.createRandom(); const signer3 = new ethers.Wallet(WALLET3.privateKey, provider); const user3 = await PushAPI.initialize(signer3, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const WALLET4 = ethers.Wallet.createRandom(); const signer4 = new ethers.Wallet(WALLET4.privateKey, provider); const user4 = await PushAPI.initialize(signer4, { - env: CONSTANTS.ENV.LOCAL, + env: CONSTANTS.ENV.DEV, }); const GROUP_RULES = { @@ -718,7 +722,7 @@ describe('PushStream.initialize functionality', () => { onError: () => { console.error('opt in error'); }, - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -737,7 +741,7 @@ describe('PushStream.initialize functionality', () => { img: '', }, channel: `eip155:5:${channelAddress}`, // your channel address - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -751,7 +755,7 @@ describe('PushStream.initialize functionality', () => { onError: () => { console.error('opt out error'); }, - env: ENV.LOCAL, + env: ENV.DEV, }); @@ -771,7 +775,7 @@ describe('PushStream.initialize functionality', () => { }, recipients: `eip155:5:${signer.address}`, channel: `eip155:5:${channelAddress}`, // your channel address - env: ENV.LOCAL, + env: ENV.DEV, }); diff --git a/packages/restapi/yarn.lock b/packages/restapi/yarn.lock index dd49f0efd..d5acc64d4 100644 --- a/packages/restapi/yarn.lock +++ b/packages/restapi/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" + integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== + "@ambire/signature-validator@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@ambire/signature-validator/-/signature-validator-1.3.1.tgz#d899aae3b26f65a3557b4d43b6fe4b50c6f9074b" @@ -448,17 +453,29 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@1.2.0", "@noble/curves@~1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + "@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== -"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== -"@scure/base@~1.1.0": +"@scure/base@~1.1.0", "@scure/base@~1.1.2": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== @@ -472,6 +489,15 @@ "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" + integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== + dependencies: + "@noble/curves" "~1.2.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.2" + "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -554,6 +580,11 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +abitype@0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" + integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== + acorn-walk@^8.1.1: version "8.3.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.1.tgz#2f10f5b69329d90ae18c58bf1fa8fccd8b959a43" @@ -1760,6 +1791,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isows@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" + integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== + joi@^17.9.2: version "17.11.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" @@ -2849,6 +2885,20 @@ video-stream-merger@^4.0.1: resolved "https://registry.yarnpkg.com/video-stream-merger/-/video-stream-merger-4.0.1.tgz#b0061251bd211121d1256ccf9e2be9477e59d5cb" integrity sha512-VazYSr8tk6S/zkOq5jpR/ryy1HnGxm5XCw+d2Ejpqy1m6d71oZpyFG82dUkgAo7dg/lk3k4TqvJPtuRUtR8URA== +viem@^1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.20.3.tgz#8b8360daee622295f5385949c02c86d943d14e0f" + integrity sha512-7CrmeCb2KYkeCgUmUyb1hsf+IX/PLwi+Np+Vm4YUTPeG82y3HRSgGHSaCOp3d0YtR2kXD3nv9y5kE7LBFE+wWw== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "0.9.8" + isows "1.0.3" + ws "8.13.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -2938,6 +2988,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@~8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" From 40e5941b296e068e00804ee45beb9b6a2bc3ef8d Mon Sep 17 00:00:00 2001 From: aman035 Date: Wed, 3 Jan 2024 13:33:44 +0530 Subject: [PATCH 2/4] fix: fix ethers provider issue --- packages/restapi/src/lib/types/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index c4d5d86a7..fd90638c3 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -1,4 +1,4 @@ -import { TypedDataDomain, TypedDataField, Provider } from 'ethers'; +import { TypedDataDomain, TypedDataField } from 'ethers'; import { ADDITIONAL_META_TYPE, IDENTITY_TYPE, @@ -664,7 +664,7 @@ export type ethersV5SignerType = { getAddress: () => Promise; signMessage: (message: Uint8Array | string) => Promise; privateKey?: string; - provider?: Provider | null; + provider?: any; }; export type ethersV6SignerType = { @@ -676,7 +676,7 @@ export type ethersV6SignerType = { getAddress: () => Promise; signMessage: (message: Uint8Array | string) => Promise; privateKey?: string; - provider?: Provider | null; + provider?: any; }; export type viemSignerType = { @@ -695,7 +695,7 @@ export type viemSignerType = { }) => Promise<`0x${string}`>; account: { [key: string]: any }; privateKey?: string; - provider?: Provider | null; + provider?: any; }; export type SignerType = From 4bccf071fecc2a3eabe6966d156eef9051dd9993 Mon Sep 17 00:00:00 2001 From: aman035 Date: Wed, 3 Jan 2024 15:41:08 +0530 Subject: [PATCH 3/4] fix: fix channel.update --- .../pushNotification/pushNotificationBase.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts index e022ce4ed..b0f0a611a 100644 --- a/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts +++ b/packages/restapi/src/lib/pushNotification/pushNotificationBase.ts @@ -321,12 +321,13 @@ export class PushNotificationBaseClass { const pushSigner = new Signer(this.signer); try { if (pushSigner.isViemSigner(this.signer)) { - const balanceInBigInt = await contract.read.balanceOf({ - args: [userAddress], - }); - balance = BigInt(balanceInBigInt); + balance = BigInt( + await contract.read.balanceOf({ + args: [userAddress], + }) + ); } else { - balance = await contract.balanceOf(userAddress); + balance = BigInt(await contract.balanceOf(userAddress)); } return balance; } catch (err) { @@ -347,12 +348,15 @@ export class PushNotificationBaseClass { let allowance: bigint; try { if (!pushSigner.isViemSigner(this.signer)) { - allowance = await contract!['allowance'](userAddress, spenderAddress); + allowance = BigInt( + await contract!['allowance'](userAddress, spenderAddress) + ); } else { - const allowanceInBigInt = await contract.read.allowance({ - args: [userAddress, spenderAddress], - }); - allowance = BigInt(allowanceInBigInt); + allowance = BigInt( + await contract.read.allowance({ + args: [userAddress, spenderAddress], + }) + ); } return allowance; } catch (error) { @@ -368,12 +372,13 @@ export class PushNotificationBaseClass { const pushSigner = new Signer(this.signer); try { if (!pushSigner.isViemSigner(this.signer)) { - count = await contract!['channelUpdateCounter'](userAddress); + count = BigInt(await contract!['channelUpdateCounter'](userAddress)); } else { - const countInBigInt = await contract.read.channelUpdateCounter({ - args: [userAddress], - }); - count = BigInt(countInBigInt); + count = BigInt( + await contract.read.channelUpdateCounter({ + args: [userAddress], + }) + ); } // add one and return the count return count + BigInt(1); From aacfea3a5bd5a3301f60cb1d1c04a1754e6f03b8 Mon Sep 17 00:00:00 2001 From: Madhur Gupta Date: Mon, 15 Jan 2024 13:16:22 +0530 Subject: [PATCH 4/4] feat(video-v2): add video v2 class and stream (#930) * feat(video-v2): add highlevel video class * feat(video): add video stream * fix(sendnotification): modify rules.access.data to be an object & code cleanup * fix(video): remove signer from input, throw err if signer, decrypted pgp key not found * feat(video): add sendNotification calls in connect, disconnect methods * fix(videonotificationrules): typo in VideoNotificationRules interface * feat(video stream): handle connect, retry internally from the SDK * feat(video connect): remove the connect method from the videov2 SDK * fix(videov2): create push signer instance to get chain id in initialize() * fix(video stream): add backwards compatibilty to rules object for older SDK versions * fix(video stream): fix bug in rules object creation * feat(video): update param names for video.initialize() * feat(video): add internal event handlers for stream in video SDK --------- Co-authored-by: Mohammed S --- packages/restapi/src/lib/payloads/helpers.ts | 4 +- .../src/lib/payloads/sendNotifications.ts | 4 +- packages/restapi/src/lib/pushapi/PushAPI.ts | 10 + .../restapi/src/lib/pushapi/pushAPITypes.ts | 10 + packages/restapi/src/lib/pushapi/video.ts | 146 ++++++++++++ .../src/lib/pushstream/DataModifier.ts | 75 +++++++ .../restapi/src/lib/pushstream/PushStream.ts | 53 ++++- .../src/lib/pushstream/pushStreamTypes.ts | 23 +- packages/restapi/src/lib/types/index.ts | 14 +- packages/restapi/src/lib/types/videoTypes.ts | 9 + packages/restapi/src/lib/video/Video.ts | 96 +++++--- packages/restapi/src/lib/video/VideoV2.ts | 209 ++++++++++++++++++ .../helpers/sendVideoCallNotification.ts | 6 +- .../src/lib/video/helpers/validatePeerInfo.ts | 22 ++ .../lib/video/helpers/validateVideoRules.ts | 13 ++ .../lib/video/sendVideoNotification.test.ts | 2 +- 16 files changed, 643 insertions(+), 53 deletions(-) create mode 100644 packages/restapi/src/lib/pushapi/video.ts create mode 100644 packages/restapi/src/lib/types/videoTypes.ts create mode 100644 packages/restapi/src/lib/video/VideoV2.ts create mode 100644 packages/restapi/src/lib/video/helpers/validatePeerInfo.ts create mode 100644 packages/restapi/src/lib/video/helpers/validateVideoRules.ts diff --git a/packages/restapi/src/lib/payloads/helpers.ts b/packages/restapi/src/lib/payloads/helpers.ts index d6bb076f4..449f74962 100644 --- a/packages/restapi/src/lib/payloads/helpers.ts +++ b/packages/restapi/src/lib/payloads/helpers.ts @@ -7,7 +7,7 @@ import { ISendNotificationInputOptions, INotificationPayload, walletType, - VideNotificationRules, + VideoNotificationRules, } from '../types'; import { IDENTITY_TYPE, @@ -212,7 +212,7 @@ export async function getVerificationProof({ wallet?: walletType; pgpPrivateKey?: string; env?: ENV; - rules?:VideNotificationRules; + rules?:VideoNotificationRules; }) { let message = null; let verificationProof = null; diff --git a/packages/restapi/src/lib/payloads/sendNotifications.ts b/packages/restapi/src/lib/payloads/sendNotifications.ts index 482faa3d3..2d6eec58e 100644 --- a/packages/restapi/src/lib/payloads/sendNotifications.ts +++ b/packages/restapi/src/lib/payloads/sendNotifications.ts @@ -185,7 +185,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { uuid, // for the pgpv2 verfication proof chatId: - rules?.access.data ?? // for backwards compatibilty with 'chatId' param + rules?.access.data.chatId ?? // for backwards compatibilty with 'chatId' param chatId, pgpPrivateKey, }); @@ -231,7 +231,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { ? { rules: rules ?? { access: { - data: chatId, + data: { chatId }, type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, }, }, diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index 82e6e4f54..8beea967d 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 { Video } from './video'; import { isValidCAIP10NFTAddress } from '../helpers'; export class PushAPI { @@ -29,6 +30,8 @@ export class PushAPI { private progressHook?: (progress: ProgressHookType) => void; public chat: Chat; // Public instances to be accessed from outside the class + public video: Video; + public profile: Profile; public encryption: Encryption; private user: User; @@ -86,6 +89,13 @@ export class PushAPI { this.progressHook ); this.user = new User(this.account, this.env); + + this.video = new Video(this.account, + this.env, + this.decryptedPgpPvtKey, + this.signer + ); + this.errors = initializationErrors || []; } // Overloaded initialize method signatures diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index e58757d86..48753dbc4 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -1,4 +1,5 @@ import Constants, { ENV } from '../constants'; +import type { PushStream } from '../pushstream/PushStream'; import { ChatStatus, ProgressHookType, Rules } from '../types'; export enum ChatListType { @@ -66,3 +67,12 @@ export interface ParticipantStatus { role: 'admin' | 'member'; participant: boolean; } + +export interface VideoInitializeOptions { + stream: PushStream; + config: { + video?: boolean; + audio?: boolean; + }; + media?: MediaStream; +} diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts new file mode 100644 index 000000000..9a1495841 --- /dev/null +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -0,0 +1,146 @@ +import { ENV } from '../constants'; +import CONSTANTS from '../constantsV2'; +import { SignerType, VideoCallData, VideoCallStatus } from '../types'; +import { Signer as PushSigner } from '../helpers'; + +import { Video as VideoV1, initVideoCallData } from '../video/Video'; +import { VideoV2 } from '../video/VideoV2'; +import { VideoInitializeOptions } from './pushAPITypes'; +import { VideoEvent, VideoEventType } from '../pushstream/pushStreamTypes'; +import { produce } from 'immer'; +import { endStream } from '../video/helpers/mediaToggle'; + +export class Video { + constructor( + private account: string, + private env: ENV, + private decryptedPgpPvtKey?: string, + private signer?: SignerType + ) {} + + async initialize( + onChange: (fn: (data: VideoCallData) => VideoCallData) => void, + options: VideoInitializeOptions + ) { + const { stream, config, media } = options; + + if (!this.signer) { + throw new Error('Signer is required for push video'); + } + + if (!this.decryptedPgpPvtKey) { + throw new Error( + 'PushSDK was initialized in readonly mode. Video functionality is not available.' + ); + } + + const chainId = await new PushSigner(this.signer).getChainId(); + + if (!chainId) { + throw new Error('Chain Id not retrievable from signer'); + } + + // Initialize the video instance with the provided options + const videoV1Instance = new VideoV1({ + signer: this.signer!, + chainId, + pgpPrivateKey: this.decryptedPgpPvtKey!, + env: this.env, + setData: onChange, + }); + + // Create the media stream with the provided options + await videoV1Instance.create({ + ...(media && { + stream: media, + }), + ...(config?.audio && { + audio: config.audio, + }), + ...(config?.video && { + video: config.video, + }), + }); + + // Setup video event handlers + stream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { + const { + address, + signal, + meta: { rules }, + } = data.peerInfo; + + const chatId = rules.access.data.chatId; + + // If the event is RequestVideo, update the video call 'data' state with the incoming call data + if (data.event === VideoEventType.RequestVideo) { + videoV1Instance.setData((oldData) => { + return produce(oldData, (draft) => { + draft.local.address = this.account; + draft.incoming[0].address = address; + draft.incoming[0].status = VideoCallStatus.RECEIVED; + draft.meta.chatId = chatId!; + draft.meta.initiator.address = address; + draft.meta.initiator.signal = signal; + }); + }); + } + + // Check if the chatId from the incoming video event matches the chatId of the current video instance + if (chatId && chatId === videoV1Instance.data.meta.chatId) { + // If the event is DenyVideo, destroy the local stream & reset the video call data + if (data.event === VideoEventType.DenyVideo) { + // destroy the local stream + if (videoV1Instance.data.local.stream) { + endStream(videoV1Instance.data.local.stream); + } + + videoV1Instance.setData(() => initVideoCallData); + } + + // If the event is ApproveVideo or RetryApproveVideo, connect to the video + if ( + data.event === VideoEventType.ApproveVideo || + data.event === VideoEventType.RetryApproveVideo + ) { + videoV1Instance.connect({ peerAddress: address, signalData: signal }); + } + + // If the event is RetryRequestVideo and the current instance is the initiator, send a request + if ( + data.event === VideoEventType.RetryRequestVideo && + videoV1Instance.isInitiator() + ) { + videoV1Instance.request({ + senderAddress: this.account, + recipientAddress: address, + rules, + retry: true, + }); + } + + // If the event is RetryRequestVideo and the current instance is not the initiator, accept the request + if ( + data.event === VideoEventType.RetryRequestVideo && + !videoV1Instance.isInitiator() + ) { + videoV1Instance.acceptRequest({ + signalData: signal, + senderAddress: this.account, + recipientAddress: address, + rules, + retry: true, + }); + } + } + }); + + // Return an instance of the video v2 class + return new VideoV2({ + videoV1Instance, + account: this.account, + decryptedPgpPvtKey: this.decryptedPgpPvtKey!, + env: this.env, + }); + } +} diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 8b18e8cf1..2c4cb62cd 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -17,7 +17,13 @@ import { NotificationType, NOTIFICATION, ProposedEventNames, + VideoEventType, + MessageOrigin, + VideoEvent, } from './pushStreamTypes'; +import { VideoCallStatus, VideoPeerInfo } from '../types'; +import { VideoDataType } from '../video'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; export class DataModifier { public static handleChatGroupEvent(data: any, includeRaw = false): any { @@ -388,4 +394,73 @@ export class DataModifier { break; } } + + public static convertToProposedNameForVideo( + currentVideoStatus: VideoCallStatus + ): VideoEventType { + switch (currentVideoStatus) { + case VideoCallStatus.INITIALIZED: + return VideoEventType.RequestVideo; + case VideoCallStatus.RECEIVED: + return VideoEventType.ApproveVideo; + case VideoCallStatus.CONNECTED: + return VideoEventType.ConnectVideo; + case VideoCallStatus.ENDED: + return VideoEventType.DisconnectVideo; + case VideoCallStatus.DISCONNECTED: + return VideoEventType.DenyVideo; + case VideoCallStatus.RETRY_INITIALIZED: + return VideoEventType.RetryRequestVideo; + case VideoCallStatus.RETRY_RECEIVED: + return VideoEventType.RetryApproveVideo; + default: + throw new Error(`Unknown video call status: ${currentVideoStatus}`); + } + } + + public static mapToVideoEvent( + data: any, + origin: MessageOrigin, + includeRaw = false + ): VideoEvent { + const { senderAddress, signalData, status, chatId }: VideoDataType = + JSON.parse(data.payload.data.additionalMeta?.data); + + // To maintain backward compatibility, if the rules object is not present in the payload, + // we create a new rules object with chatId from additionalMeta.data + const rules = data.payload.rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId, + }, + }, + }; + + const peerInfo: VideoPeerInfo = { + address: senderAddress, + signal: signalData, + meta: { + rules, + }, + }; + + const videoEventType: VideoEventType = + DataModifier.convertToProposedNameForVideo(status); + + const videoEvent: VideoEvent = { + event: videoEventType, + origin: origin, + timestamp: data.epoch, + peerInfo, + }; + + if (includeRaw) { + videoEvent.raw = { + verificationProof: data.payload.verificationProof, + }; + } + + return videoEvent; + } } diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts index 3f270fc3d..3b944a884 100644 --- a/packages/restapi/src/lib/pushstream/PushStream.ts +++ b/packages/restapi/src/lib/pushstream/PushStream.ts @@ -4,16 +4,18 @@ import { ENV, PACKAGE_BUILD } from '../constants'; import { GroupEventType, MessageEventType, + MessageOrigin, NotificationEventType, PushStreamInitializeProps, STREAM, - EVENTS, + EVENTS } from './pushStreamTypes'; import { DataModifier } from './DataModifier'; import { pCAIP10ToWallet, walletToPCAIP10 } from '../helpers'; import { Chat } from '../pushapi/chat'; import { ProgressHookType, SignerType } from '../types'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { ADDITIONAL_META_TYPE } from '../payloads'; export class PushStream extends EventEmitter { private pushChatSocket: any; @@ -103,7 +105,8 @@ export class PushStream extends EventEmitter { !this.listen || this.listen.length === 0 || this.listen.includes(STREAM.NOTIF) || - this.listen.includes(STREAM.NOTIF_OPS); + this.listen.includes(STREAM.NOTIF_OPS) || + this.listen.includes(STREAM.VIDEO); let isChatSocketConnected = false; let isNotifSocketConnected = false; @@ -313,16 +316,33 @@ export class PushStream extends EventEmitter { this.pushNotificationSocket.on(EVENTS.USER_FEEDS, (data: any) => { try { - const modifiedData = DataModifier.mapToNotificationEvent( - data, - NotificationEventType.INBOX, - this.account === data.sender ? 'self' : 'other', - this.raw - ); + if ( + data.payload.data.additionalMeta?.type === + `${ADDITIONAL_META_TYPE.PUSH_VIDEO}+1` && + shouldEmit(STREAM.VIDEO) && + this.shouldEmitVideo(data.sender) + ) { + // Video Notification + const modifiedData = DataModifier.mapToVideoEvent( + data, + this.account === data.sender ? MessageOrigin.Self : MessageOrigin.Other, + this.raw + ); - if (this.shouldEmitChannel(modifiedData.from)) { - if (shouldEmit(STREAM.NOTIF)) { - this.emit(STREAM.NOTIF, modifiedData); + this.emit(STREAM.VIDEO, modifiedData); + } else { + // Channel Notification + const modifiedData = DataModifier.mapToNotificationEvent( + data, + NotificationEventType.INBOX, + this.account === data.sender ? 'self' : 'other', + this.raw + ); + + if (this.shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + this.emit(STREAM.NOTIF, modifiedData); + } } } } catch (error) { @@ -397,4 +417,15 @@ export class PushStream extends EventEmitter { } return this.options.filter.channels.includes(dataChannelId); } + + private shouldEmitVideo(dataVideoId: string): boolean { + if ( + !this.options.filter?.video || + this.options.filter.video.length === 0 || + this.options.filter.video.includes('*') + ) { + return true; + } + return this.options.filter.video.includes(dataVideoId); + } } diff --git a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts index a25bd1a74..01cf61e7b 100644 --- a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts +++ b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts @@ -1,10 +1,11 @@ -import { Rules } from '../types'; +import { Rules, VideoPeerInfo } from '../types'; import { ENV } from '../constants'; export type PushStreamInitializeProps = { filter?: { channels?: string[]; chats?: string[]; + video?: string[]; }; connection?: { auto?: boolean; @@ -22,6 +23,7 @@ export enum STREAM { NOTIF_OPS = 'STREAM.NOTIF_OPS', CHAT = 'STREAM.CHAT', CHAT_OPS = 'STREAM.CHAT_OPS', + VIDEO = 'STREAM.VIDEO', CONNECT = 'STREAM.CONNECT', DISCONNECT = 'STREAM.DISCONNECT', } @@ -51,6 +53,17 @@ export enum GroupEventType { Remove = 'remove', } +export enum VideoEventType { + RequestVideo = 'video.request', + ApproveVideo = 'video.approve', + DenyVideo = 'video.deny', + ConnectVideo = 'video.connect', + DisconnectVideo = 'video.disconnect', + // retry events + RetryRequestVideo = 'video.retry.request', + RetryApproveVideo = 'video.retry.approve' +} + export enum ProposedEventNames { Message = 'chat.message', Request = 'chat.request', @@ -225,6 +238,14 @@ export interface MessageRawData { previousReference: string; } +export interface VideoEvent { + event: VideoEventType; + origin: MessageOrigin; + timestamp: string; + peerInfo: VideoPeerInfo; + raw?: GroupEventRawData; +} + export enum EVENTS { // Websocket CONNECT = 'connect', diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index fd90638c3..841229adc 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -13,6 +13,7 @@ import { ENV, MessageType } from '../constants'; import { EthEncryptedData } from '@metamask/eth-sig-util'; import { Message, MessageObj } from './messageTypes'; export * from './messageTypes'; +export * from './videoTypes'; export type Env = typeof ENV[keyof typeof ENV]; @@ -83,15 +84,17 @@ export type ParsedResponseType = { }; }; -export interface VideNotificationRules { +export interface VideoNotificationRules { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE; - data: string; + data: { + chatId?: string; + }; }; } // SendNotificationRules can be extended in the future for other use cases -export type SendNotificationRules = VideNotificationRules; +export type SendNotificationRules = VideoNotificationRules; export interface ISendNotificationInputOptions { senderType?: 0 | 1; @@ -778,6 +781,7 @@ export enum VideoCallStatus { RECEIVED, CONNECTED, DISCONNECTED, + ENDED, RETRY_INITIALIZED, RETRY_RECEIVED, } @@ -824,7 +828,7 @@ export type VideoRequestInputOptions = { recipientAddress: string | string[]; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { @@ -839,7 +843,7 @@ export type VideoAcceptRequestInputOptions = { recipientAddress: string; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { diff --git a/packages/restapi/src/lib/types/videoTypes.ts b/packages/restapi/src/lib/types/videoTypes.ts new file mode 100644 index 000000000..e6f018a2c --- /dev/null +++ b/packages/restapi/src/lib/types/videoTypes.ts @@ -0,0 +1,9 @@ +import { VideoNotificationRules } from "."; + +export type VideoPeerInfo = { + address: string; + signal: any; + meta: { + rules: VideoNotificationRules; + }; +}; diff --git a/packages/restapi/src/lib/video/Video.ts b/packages/restapi/src/lib/video/Video.ts index 380e3c507..66870c62d 100644 --- a/packages/restapi/src/lib/video/Video.ts +++ b/packages/restapi/src/lib/video/Video.ts @@ -34,7 +34,9 @@ import { SPACE_DISCONNECT_TYPE, SPACE_REQUEST_TYPE, VIDEO_CALL_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, } from '../payloads/constants'; +import { validateVideoRules } from './helpers/validateVideoRules'; export const initVideoCallData: VideoCallData = { meta: { @@ -86,7 +88,7 @@ export class Video { [key: string]: any; } = {}; - protected data: VideoCallData; + data: VideoCallData; setData: (fn: (data: VideoCallData) => VideoCallData) => void; constructor({ @@ -171,6 +173,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + const recipientAddresses = Array.isArray(recipientAddress) ? recipientAddress : [recipientAddress]; @@ -181,7 +186,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -382,7 +387,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); @@ -424,6 +429,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + try { // if peerInstance is not null -> acceptRequest/request was called before if (this.peerInstances[recipientAddress]) { @@ -448,7 +456,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -674,7 +682,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); @@ -749,6 +757,32 @@ export class Video { draft.incoming[incomingIndex].status = VideoCallStatus.CONNECTED; }); }); + + // Notifying the recipient that the video call is now connected + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: peerAddress + ? peerAddress + : this.data.incoming[0].address, + status: VideoCallStatus.CONNECTED, + rules: { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: this.data.meta.chatId, + }, + }, + }, + signalData, + env: this.env, + } + ); } catch (err) { console.error('error in connect', err); } @@ -766,37 +800,43 @@ export class Video { ? getIncomingIndexFromAddress(this.data.incoming, peerAddress) : 0; - if ( - this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED - ) { + const isCallConnected = + this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED; + + if (isCallConnected) { this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.send(JSON.stringify({ type: 'endCall', value: true, details })); this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.destroy(); - } else { - // for disconnecting during status INITIALIZED, RECEIVED, RETRY_INITIALIZED, RETRY_RECEIVED - // send a notif to the other user signaling status = DISCONNECTED - sendVideoCallNotification( - { - signer: this.signer, - chainId: this.chainId, - pgpPrivateKey: this.pgpPrivateKey, - }, - { - senderAddress: this.data.local.address, - recipientAddress: this.data.incoming[incomingIndex].address, - status: VideoCallStatus.DISCONNECTED, - chatId: this.data.meta.chatId, - signalData: null, - env: this.env, - callType: this.callType, - callDetails: details, - } - ); } + /* + * Send a notification to the other user signaling: + * status = ENDED if the call was connected + * status = DISCONNECTED otherwise. + */ + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: this.data.incoming[incomingIndex].address, + status: isCallConnected + ? VideoCallStatus.ENDED + : VideoCallStatus.DISCONNECTED, + chatId: this.data.meta.chatId, + signalData: null, + env: this.env, + callType: this.callType, + callDetails: details, + } + ); + // destroy the peerInstance this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts new file mode 100644 index 000000000..cad064e07 --- /dev/null +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -0,0 +1,209 @@ +import { produce } from 'immer'; +import { chats } from '../chat'; +import { ENV } from '../constants'; +import { + isValidETHAddress, + pCAIP10ToWallet, + walletToPCAIP10, +} from '../helpers'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; +import { + VideoCallStatus, + VideoNotificationRules, + VideoPeerInfo, +} from '../types'; +import { Video as VideoV1 } from './Video'; +import { validatePeerInfo } from './helpers/validatePeerInfo'; + +/** + * VideoV2 class + */ +export class VideoV2 { + private account: string; + private decryptedPgpPvtKey: string; + private env: ENV; + + private videoInstance: VideoV1; + + /** + * VideoV2 constructor + * @param {object} params - The constructor parameters + * @param {VideoV1} params.videoV1Instance - The VideoV1 instance + * @param {string} params.account - The account + * @param {string} params.decryptedPgpPvtKey - The decrypted PGP private key + * @param {ENV} params.env - The environment + */ + constructor({ + videoV1Instance, + account, + decryptedPgpPvtKey, + env, + }: { + videoV1Instance: VideoV1; + account: string; + decryptedPgpPvtKey: string; + env: ENV; + }) { + this.videoInstance = videoV1Instance; + this.account = account; + this.decryptedPgpPvtKey = decryptedPgpPvtKey; + this.env = env; + } + + /** + * Request a video call + * @param {string[]} recipients - The recipients of the video call + * @param {object} options - The options for the video call + * @param {object} options.rules - The rules for the video call + * @param {object} options.rules.access - The access rules for the video call + * @param {string} options.rules.access.type - The type of the video call + * @param {object} options.rules.access.data - The data for the video call + * @param {string} options.rules.access.data.chatId - The chat ID for the video call + */ + async request( + recipients: string[], + options?: { + rules: VideoNotificationRules; + } + ) { + const { rules } = options || {}; + + for (const recipient of recipients) { + if (!isValidETHAddress(recipient)) { + throw new Error('Invalid recipient address found'); + } + } + + if (recipients.length === 0) { + throw new Error( + 'Alteast one recipient address is required for a video call' + ); + } + + if ( + recipients.length > 1 && + rules?.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + !rules.access.data.chatId + ) { + throw new Error( + 'For multiple recipient addresses, chatId is required for a video call' + ); + } + + // If chatId is not passed, find a w2w chat between the addresses and use the chatId from there + let retrievedChatId = ''; + if (!rules?.access.data.chatId) { + let page = 1; + const limit = 30; + while (!retrievedChatId) { + const response = await chats({ + account: this.account, + toDecrypt: true, + pgpPrivateKey: this.decryptedPgpPvtKey, + env: this.env, + page, + limit, + }); + + if (response.length === 0) break; + + response.forEach((chat) => { + if (chat.did === walletToPCAIP10(recipients[0]) && chat.chatId) { + retrievedChatId = chat.chatId; + } + }); + + page++; + } + + if (!retrievedChatId) { + throw new Error( + `ChatId not found between local user (${this.account}) and recipient (${recipients[0]}).` + ); + } + } + + this.videoInstance.setData((oldData) => { + return produce(oldData, (draft: any) => { + draft.local.address = this.account; + draft.incoming = recipients.map((recipient) => ({ + address: pCAIP10ToWallet(recipient), + status: VideoCallStatus.INITIALIZED, + })); + draft.meta.chatId = rules?.access.data.chatId ?? retrievedChatId; + }); + }); + + await this.videoInstance.request({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: recipients.map((recipient) => + pCAIP10ToWallet(recipient) + ), + rules: rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: retrievedChatId, + }, + }, + }, + }); + } + + /** + * Approve a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async approve(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { signal, address, meta } = peerInfo; + + await this.videoInstance.acceptRequest({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: pCAIP10ToWallet(address), + signalData: signal, + rules: meta.rules, + }); + } + + /** + * Deny a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async deny(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { address } = peerInfo; + + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Disconnect from a video call + * @param {string} address - The address to disconnect from + */ + async disconnect(address: string) { + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Enable or disable media (video, audio) + * @param {object} params - The parameters + * @param {boolean} params.video - The video state + * @param {boolean} params.audio - The audio state + */ + media({ video, audio }: { video?: boolean; audio?: boolean }) { + if (typeof video === 'boolean') { + this.videoInstance.enableVideo({ state: video }); + } + + if (typeof audio === 'boolean') { + this.videoInstance.enableAudio({ state: audio }); + } + } +} diff --git a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts index 4675e5be4..5408a8c6b 100644 --- a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts +++ b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts @@ -13,7 +13,7 @@ import { EnvOptionsType, SignerType, VideoCallStatus, - VideNotificationRules, + VideoNotificationRules, } from '../../types'; interface CallDetailsType { @@ -33,7 +33,7 @@ export interface VideoDataType { interface VideoCallInfoType extends VideoDataType, EnvOptionsType { callType?: VIDEO_CALL_TYPE; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; } interface UserInfoType { @@ -60,7 +60,7 @@ const sendVideoCallNotification = async ( const videoData: VideoDataType = { recipientAddress, senderAddress, - chatId: rules?.access.data ?? chatId, + chatId: rules?.access.data.chatId ?? chatId, signalData, status, callDetails, diff --git a/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts new file mode 100644 index 000000000..b72f20a4b --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts @@ -0,0 +1,22 @@ +import { isValidETHAddress } from '../../helpers'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; +import { VideoPeerInfo } from '../../types'; + +export const validatePeerInfo = (peerInfo: VideoPeerInfo) => { + const { signal, address, meta } = peerInfo; + + if (!signal) { + throw new Error('Invalid signal data received'); + } + + if (!isValidETHAddress(address)) { + throw new Error('Invalid address received'); + } + + if ( + meta.rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + !meta.rules.access.data.chatId + ) { + throw new Error('ChatId not found in meta.rules'); + } +}; diff --git a/packages/restapi/src/lib/video/helpers/validateVideoRules.ts b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts new file mode 100644 index 000000000..bb185fe8a --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts @@ -0,0 +1,13 @@ +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; +import { VideoNotificationRules } from '../../types'; + +export const validateVideoRules = (rules: VideoNotificationRules) => { + if ( + rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + (!rules.access.data.chatId || rules.access.data.chatId === '') + ) { + throw new Error( + 'Invalid rules object recieved. For access as Push Chat, chatId is required!' + ); + } +}; diff --git a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts index d45055b25..bbc8c73e3 100644 --- a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts +++ b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts @@ -113,7 +113,7 @@ describe('sendNotification functionality for video calls', () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: chatId, + data: { chatId }, }, }, notification: {