diff --git a/package.json b/package.json index 8b0e5242b..761534ff6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "prepare": "husky install", "test:restapi": "cd packages/restapi && 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", + "test:d-node-notif": "cd packages/d-node-notif && 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", "cleanbuild": "rimraf ./dist && rimraf ./tmp" }, "config": { diff --git a/packages/d-node-notif/.babelrc b/packages/d-node-notif/.babelrc new file mode 100644 index 000000000..e24a5465f --- /dev/null +++ b/packages/d-node-notif/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/packages/d-node-notif/.eslintrc.json b/packages/d-node-notif/.eslintrc.json new file mode 100644 index 000000000..9d9c0db55 --- /dev/null +++ b/packages/d-node-notif/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/d-node-notif/README.md b/packages/d-node-notif/README.md new file mode 100644 index 000000000..383d4fca0 --- /dev/null +++ b/packages/d-node-notif/README.md @@ -0,0 +1,7 @@ +# d-node-notif + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build d-node-notif` to build the library. diff --git a/packages/d-node-notif/package.json b/packages/d-node-notif/package.json new file mode 100644 index 000000000..e494077fa --- /dev/null +++ b/packages/d-node-notif/package.json @@ -0,0 +1,43 @@ +{ + "name": "@sdk/d-node-notif", + "version": "0.0.1", + "type": "commonjs", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "peerDependencies": { + "ethers": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "ethers": { + "optional": true + } + }, + "dependencies": { + "@metamask/eth-sig-util": "^5.0.2", + "buffer": "^6.0.3", + "crypto-js": "^4.1.1", + "immer": "^10.0.2", + "joi": "^17.9.2", + "lru-cache": "^10.1.0", + "openpgp": "^5.5.0", + "socket.io-client": "^4.7.2", + "viem": "^2.13.1" + }, + "scripts": { + "test": "TS_NODE_PROJECT='./tsconfig.mocha.json' NODE_OPTIONS='--loader ts-node/esm' DOTENV_CONFIG_PATH='./tests/.env' mocha -r ts-node/register -r dotenv/config 'tests/**/*.test.ts' --timeout 1200000 --require tests/root.ts --serial" + }, + "devDependencies": { + "@types/chai": "^4.3.4", + "@types/chai-as-promised": "^7.1.5", + "@types/crypto-js": "^4.1.1", + "@types/mocha": "^10.0.1", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "envfile": "^7.1.0", + "mocha": "^10.2.0", + "mocha-typescript": "^1.1.17", + "ts-node": "^10.9.1", + "typescript": "^5.0.2" + } +} diff --git a/packages/d-node-notif/project.json b/packages/d-node-notif/project.json new file mode 100644 index 000000000..8d645914c --- /dev/null +++ b/packages/d-node-notif/project.json @@ -0,0 +1,57 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/d-node-notif/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/d-node-notif", + "main": "packages/d-node-notif/src/index.ts", + "tsConfig": "packages/d-node-notif/tsconfig.lib.json", + "assets": ["packages/d-node-notif/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/d-node-notif/**/*.ts"] + } + } + }, + "test": { + "executor": "@nrwl/workspace:run-commands", + "outputs": ["coverage/packages/restapi"], + "options": { + "commands": ["yarn run test:d-node-notif"], + "passWithNoTests": true + } + }, + "ci-version": { + "executor": "@jscutlery/semver:version", + "options": { + "preset": "angular", + "commitMessageFormat": "ci(${projectName}): 🎉 cut release to ${projectName}-v${version}", + "postTargets": ["d-node_notif:build", "d-node_notif:ci-publish"] + } + }, + "ci-version-beta": { + "executor": "@jscutlery/semver:version", + "options": { + "preset": "angular", + "commitMessageFormat": "ci(${projectName}): 🎉 cut beta release to ${projectName}-v${version}", + "postTargets": ["d-node_notif:build", "d-node_notif:ci-publish"], + "version": "prerelease", + "preid": "alpha" + } + }, + "ci-publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "node tools/scripts/publish.mjs d-node_notif" + } + }, + "tags": [] +} diff --git a/packages/d-node-notif/src/index.ts b/packages/d-node-notif/src/index.ts new file mode 100644 index 000000000..f41a696fd --- /dev/null +++ b/packages/d-node-notif/src/index.ts @@ -0,0 +1 @@ +export * from './lib'; diff --git a/packages/d-node-notif/src/lib/abis/comm.ts b/packages/d-node-notif/src/lib/abis/comm.ts new file mode 100644 index 000000000..31b7c27d7 --- /dev/null +++ b/packages/d-node-notif/src/lib/abis/comm.ts @@ -0,0 +1,520 @@ +export const commABI = [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'delegate', + type: 'address', + }, + ], + name: 'AddDelegate', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'string', + name: '_chainName', + type: 'string', + }, + { + indexed: true, + internalType: 'uint256', + name: '_chainID', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: '_channelOwnerAddress', + type: 'address', + }, + { + indexed: false, + internalType: 'string', + name: '_ethereumChannelAddress', + type: 'string', + }, + ], + name: 'ChannelAlias', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: 'publickey', + type: 'bytes', + }, + ], + name: 'PublicKeyRegistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'delegate', + type: 'address', + }, + ], + name: 'RemoveDelegate', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: 'identity', + type: 'bytes', + }, + ], + name: 'SendNotification', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + ], + name: 'Subscribe', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + ], + name: 'Unsubscribe', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: '_channel', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: '_user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: '_notifID', + type: 'uint256', + }, + { + indexed: false, + internalType: 'string', + name: '_notifSettings', + type: 'string', + }, + ], + name: 'UserNotifcationSettingsAdded', + type: 'event', + }, + { + inputs: [], + name: 'DOMAIN_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'EPNSCoreAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'NAME_HASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'SEND_NOTIFICATION_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'SUBSCRIBE_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'UNSUBSCRIBE_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_delegate', type: 'address' }], + name: 'addDelegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: '_channelList', type: 'address[]' }, + ], + name: 'batchSubscribe', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: '_channelList', type: 'address[]' }, + ], + name: 'batchUnsubscribe', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: '_publicKey', type: 'bytes' }], + name: 'broadcastUserPublicKey', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'chainID', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'chainName', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'uint256', name: '_notifID', type: 'uint256' }, + { internalType: 'string', name: '_notifSettings', type: 'string' }, + ], + name: 'changeUserChannelSettings', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'completeMigration', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: '', type: 'address' }, + ], + name: 'delegatedNotificationSenders', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: '_publicKey', type: 'bytes' }], + name: 'getWalletFromPublicKey', + outputs: [{ internalType: 'address', name: 'wallet', type: 'address' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'governance', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_pushChannelAdmin', type: 'address' }, + { internalType: 'string', name: '_chainName', type: 'string' }, + ], + name: 'initialize', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'isMigrationComplete', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'address', name: '_user', type: 'address' }, + ], + name: 'isUserSubscribed', + outputs: [{ internalType: 'bool', name: 'isSubscriber', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'mapAddressUsers', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_startIndex', type: 'uint256' }, + { internalType: 'uint256', name: '_endIndex', type: 'uint256' }, + { internalType: 'address[]', name: '_channelList', type: 'address[]' }, + { internalType: 'address[]', name: '_usersList', type: 'address[]' }, + ], + name: 'migrateSubscribeData', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pushChannelAdmin', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_delegate', type: 'address' }], + name: 'removeDelegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'address', name: '_recipient', type: 'address' }, + { internalType: 'bytes', name: '_identity', type: 'bytes' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + { internalType: 'uint256', name: 'expiry', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'sendNotifBySig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'address', name: '_recipient', type: 'address' }, + { internalType: 'bytes', name: '_identity', type: 'bytes' }, + ], + name: 'sendNotification', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_coreAddress', type: 'address' }, + ], + name: 'setEPNSCoreAddress', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_governanceAddress', type: 'address' }, + ], + name: 'setGovernanceAddress', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_channel', type: 'address' }], + name: 'subscribe', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'channel', type: 'address' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + { internalType: 'uint256', name: 'expiry', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'subscribeBySig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'address', name: '_user', type: 'address' }, + ], + name: 'subscribeViaCore', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_newAdmin', type: 'address' }], + name: 'transferPushChannelAdminControl', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_channel', type: 'address' }], + name: 'unsubscribe', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'channel', type: 'address' }, + { internalType: 'uint256', name: 'nonce', type: 'uint256' }, + { internalType: 'uint256', name: 'expiry', type: 'uint256' }, + { internalType: 'uint8', name: 'v', type: 'uint8' }, + { internalType: 'bytes32', name: 'r', type: 'bytes32' }, + { internalType: 'bytes32', name: 's', type: 'bytes32' }, + ], + name: 'unsubscribeBySig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'address', name: '', type: 'address' }, + ], + name: 'userToChannelNotifs', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'users', + outputs: [ + { internalType: 'bool', name: 'userActivated', type: 'bool' }, + { internalType: 'bool', name: 'publicKeyRegistered', type: 'bool' }, + { internalType: 'uint256', name: 'userStartBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'subscribedCount', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'usersCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'string', name: '_channelAddress', type: 'string' }, + ], + name: 'verifyChannelAlias', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/packages/d-node-notif/src/lib/abis/core.ts b/packages/d-node-notif/src/lib/abis/core.ts new file mode 100644 index 000000000..6032a8b3d --- /dev/null +++ b/packages/d-node-notif/src/lib/abis/core.ts @@ -0,0 +1,957 @@ +export const coreABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'enum EPNSCoreStorageV1_5.ChannelType', + name: 'channelType', + type: 'uint8', + }, + { + indexed: false, + internalType: 'bytes', + name: 'identity', + type: 'bytes', + }, + ], + name: 'AddChannel', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: '_subGraphData', + type: 'bytes', + }, + ], + name: 'AddSubGraph', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + ], + name: 'ChannelBlocked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: '_channel', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'totalNotifOptions', + type: 'uint256', + }, + { + indexed: false, + internalType: 'string', + name: '_notifSettings', + type: 'string', + }, + { + indexed: false, + internalType: 'string', + name: '_notifDescription', + type: 'string', + }, + ], + name: 'ChannelNotifcationSettingsAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'ChannelOwnershipTransfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'revoker', + type: 'address', + }, + ], + name: 'ChannelVerificationRevoked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'verifier', + type: 'address', + }, + ], + name: 'ChannelVerified', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amountClaimed', + type: 'uint256', + }, + ], + name: 'ChatIncentiveClaimed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amountRefunded', + type: 'uint256', + }, + ], + name: 'DeactivateChannel', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'requestSender', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'requestReceiver', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amountForReqReceiver', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'feePoolAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'timestamp', + type: 'uint256', + }, + ], + name: 'IncentivizeChatReqReceived', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amountDeposited', + type: 'uint256', + }, + ], + name: 'ReactivateChannel', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'rewardAmount', + type: 'uint256', + }, + ], + name: 'RewardsClaimed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'rewardAmount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'fromEpoch', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'tillEpoch', + type: 'uint256', + }, + ], + name: 'RewardsHarvested', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amountStaked', + type: 'uint256', + }, + ], + name: 'Staked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amountRefunded', + type: 'uint256', + }, + ], + name: 'TimeBoundChannelDestroyed', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: true, + internalType: 'uint256', + name: 'amountUnstaked', + type: 'uint256', + }, + ], + name: 'Unstaked', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'channel', + type: 'address', + }, + { + indexed: false, + internalType: 'bytes', + name: 'identity', + type: 'bytes', + }, + { + indexed: true, + internalType: 'uint256', + name: 'amountDeposited', + type: 'uint256', + }, + ], + name: 'UpdateChannel', + type: 'event', + }, + { + inputs: [], + name: 'ADD_CHANNEL_MIN_FEES', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'CHANNEL_POOL_FUNDS', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'CREATE_CHANNEL_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DOMAIN_TYPEHASH', + outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'FEE_AMOUNT', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MIN_POOL_CONTRIBUTION', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PROTOCOL_POOL_FEES', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PUSH_TOKEN_ADDRESS', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'REFERRAL_CODE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'UNISWAP_V2_ROUTER', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'WETH_ADDRESS', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'aDaiAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_rewardAmount', type: 'uint256' }, + ], + name: 'addPoolFees', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: '_subGraphData', type: 'bytes' }], + name: 'addSubGraph', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_startIndex', type: 'uint256' }, + { internalType: 'uint256', name: '_endIndex', type: 'uint256' }, + { internalType: 'address[]', name: '_channelList', type: 'address[]' }, + ], + name: 'batchVerification', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channelAddress', type: 'address' }, + ], + name: 'blockChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_user', type: 'address' }, + { internalType: 'uint256', name: '_epochId', type: 'uint256' }, + ], + name: 'calculateEpochRewards', + outputs: [{ internalType: 'uint256', name: 'rewards', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'celebUserFunds', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'channelById', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'channelNotifSettings', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'channelUpdateCounter', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'channels', + outputs: [ + { + internalType: 'enum EPNSCoreStorageV1_5.ChannelType', + name: 'channelType', + type: 'uint8', + }, + { internalType: 'uint8', name: 'channelState', type: 'uint8' }, + { internalType: 'address', name: 'verifiedBy', type: 'address' }, + { internalType: 'uint256', name: 'poolContribution', type: 'uint256' }, + { internalType: 'uint256', name: 'channelHistoricalZ', type: 'uint256' }, + { + internalType: 'uint256', + name: 'channelFairShareCount', + type: 'uint256', + }, + { internalType: 'uint256', name: 'channelLastUpdate', type: 'uint256' }, + { internalType: 'uint256', name: 'channelStartBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'channelUpdateBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'channelWeight', type: 'uint256' }, + { internalType: 'uint256', name: 'expiryTime', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'channelsCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'claimChatIncentives', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_notifOptions', type: 'uint256' }, + { internalType: 'string', name: '_notifSettings', type: 'string' }, + { internalType: 'string', name: '_notifDescription', type: 'string' }, + { internalType: 'uint256', name: '_amountDeposited', type: 'uint256' }, + ], + name: 'createChannelSettings', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'enum EPNSCoreStorageV1_5.ChannelType', + name: '_channelType', + type: 'uint8', + }, + { internalType: 'bytes', name: '_identity', type: 'bytes' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint256', name: '_channelExpiryTime', type: 'uint256' }, + ], + name: 'createChannelWithPUSH', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'daiAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_tillEpoch', type: 'uint256' }], + name: 'daoHarvestPaginated', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'deactivateChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channelAddress', type: 'address' }, + ], + name: 'destroyTimeBoundChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'epnsCommunicator', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'epochDuration', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'epochRewards', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'epochToTotalStakedWeight', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'genesisEpoch', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_channel', type: 'address' }], + name: 'getChannelVerfication', + outputs: [ + { internalType: 'uint8', name: 'verificationStatus', type: 'uint8' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'governance', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'groupFairShareCount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'groupHistoricalZ', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'groupLastUpdate', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'groupNormalizedWeight', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'requestSender', type: 'address' }, + { internalType: 'address', name: 'requestReceiver', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'handleChatRequestData', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'harvestAll', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_tillEpoch', type: 'uint256' }], + name: 'harvestPaginated', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_pushChannelAdmin', type: 'address' }, + { internalType: 'address', name: '_pushTokenAddress', type: 'address' }, + { internalType: 'address', name: '_wethAddress', type: 'address' }, + { + internalType: 'address', + name: '_uniswapRouterAddress', + type: 'address', + }, + { + internalType: 'address', + name: '_lendingPoolProviderAddress', + type: 'address', + }, + { internalType: 'address', name: '_daiAddress', type: 'address' }, + { internalType: 'address', name: '_aDaiAddress', type: 'address' }, + { internalType: 'uint256', name: '_referralCode', type: 'uint256' }, + ], + name: 'initialize', + outputs: [{ internalType: 'bool', name: 'success', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'initializeStake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'isMigrationComplete', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_from', type: 'uint256' }, + { internalType: 'uint256', name: '_to', type: 'uint256' }, + ], + name: 'lastEpochRelative', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'lendingPoolProviderAddress', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'nonces', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pauseContract', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'previouslySetEpochRewards', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pushChannelAdmin', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'reactivateChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_commAddress', type: 'address' }, + ], + name: 'setEpnsCommunicatorAddress', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_newFees', type: 'uint256' }], + name: 'setFeeAmount', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_governanceAddress', type: 'address' }, + ], + name: 'setGovernanceAddress', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_newFees', type: 'uint256' }], + name: 'setMinChannelCreationFees', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_newAmount', type: 'uint256' }], + name: 'setMinPoolContribution', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'stake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'totalStakedAmount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_newAdmin', type: 'address' }], + name: 'transferPushChannelAdminControl', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'unPauseContract', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'unstake', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_channel', type: 'address' }], + name: 'unverifyChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_channel', type: 'address' }, + { internalType: 'bytes', name: '_newIdentity', type: 'bytes' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + ], + name: 'updateChannelMeta', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'userFeesInfo', + outputs: [ + { internalType: 'uint256', name: 'stakedAmount', type: 'uint256' }, + { internalType: 'uint256', name: 'stakedWeight', type: 'uint256' }, + { internalType: 'uint256', name: 'lastStakedBlock', type: 'uint256' }, + { internalType: 'uint256', name: 'lastClaimedBlock', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'usersRewardsClaimed', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_channel', type: 'address' }], + name: 'verifyChannel', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/packages/d-node-notif/src/lib/abis/token.ts b/packages/d-node-notif/src/lib/abis/token.ts new file mode 100644 index 000000000..a68265ba9 --- /dev/null +++ b/packages/d-node-notif/src/lib/abis/token.ts @@ -0,0 +1,709 @@ +export const tokenABI = [ + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'delegator', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'fromDelegate', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'toDelegate', + type: 'address', + }, + ], + name: 'DelegateChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'delegate', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'previousBalance', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'newBalance', + type: 'uint256', + }, + ], + name: 'DelegateVotesChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'holder', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'weight', + type: 'uint256', + }, + ], + name: 'HolderWeightChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'DELEGATION_TYPEHASH', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DOMAIN_TYPEHASH', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PERMIT_TYPEHASH', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'rawAmount', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'born', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'rawAmount', + type: 'uint256', + }, + ], + name: 'burn', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + ], + name: 'checkpoints', + outputs: [ + { + internalType: 'uint32', + name: 'fromBlock', + type: 'uint32', + }, + { + internalType: 'uint96', + name: 'votes', + type: 'uint96', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'decimals', + outputs: [ + { + internalType: 'uint8', + name: '', + type: 'uint8', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'delegatee', + type: 'address', + }, + ], + name: 'delegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'delegatee', + type: 'address', + }, + { + internalType: 'uint256', + name: 'nonce', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'expiry', + type: 'uint256', + }, + { + internalType: 'uint8', + name: 'v', + type: 'uint8', + }, + { + internalType: 'bytes32', + name: 'r', + type: 'bytes32', + }, + { + internalType: 'bytes32', + name: 's', + type: 'bytes32', + }, + ], + name: 'delegateBySig', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'delegates', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'getCurrentVotes', + outputs: [ + { + internalType: 'uint96', + name: '', + type: 'uint96', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'uint256', + name: 'blockNumber', + type: 'uint256', + }, + ], + name: 'getPriorVotes', + outputs: [ + { + internalType: 'uint96', + name: '', + type: 'uint96', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'holderDelegation', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'holderWeight', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'nonces', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + name: 'numCheckpoints', + outputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + internalType: 'address', + name: 'spender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'rawAmount', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'deadline', + type: 'uint256', + }, + { + internalType: 'uint8', + name: 'v', + type: 'uint8', + }, + { + internalType: 'bytes32', + name: 'r', + type: 'bytes32', + }, + { + internalType: 'bytes32', + name: 's', + type: 'bytes32', + }, + ], + name: 'permit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'holder', + type: 'address', + }, + ], + name: 'resetHolderWeight', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'address', + name: 'delegate', + type: 'address', + }, + ], + name: 'returnHolderDelegation', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'returnHolderRatio', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'delegate', + type: 'address', + }, + { + internalType: 'bool', + name: 'value', + type: 'bool', + }, + ], + name: 'setHolderDelegation', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [ + { + internalType: 'string', + name: '', + type: 'string', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'dst', + type: 'address', + }, + { + internalType: 'uint256', + name: 'rawAmount', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'src', + type: 'address', + }, + { + internalType: 'address', + name: 'dst', + type: 'address', + }, + { + internalType: 'uint256', + name: 'rawAmount', + type: 'uint256', + }, + ], + name: 'transferFrom', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const; diff --git a/packages/d-node-notif/src/lib/alias/getAliasInfo.ts b/packages/d-node-notif/src/lib/alias/getAliasInfo.ts new file mode 100644 index 000000000..a1636293e --- /dev/null +++ b/packages/d-node-notif/src/lib/alias/getAliasInfo.ts @@ -0,0 +1,37 @@ +import { getAPIBaseUrls, getCAIPWithChainId } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { ALIAS_CHAIN, ALIAS_CHAIN_ID } from '../config'; +import { axiosGet } from '../utils/axiosUtil'; + +/** + * GET /v1/alias/{aliasAddressinCAIP}/channel + */ + +export type GetAliasInfoOptionsType = { + /** alias address of the ethereum channel */ + alias: string; + /** name of the alias chain, can be Polygon or BSC or Optimism */ + aliasChain: ALIAS_CHAIN; + env?: ENV; +}; + +/** + * Returns the ethereum channel address of the provided alias address along with its verification status + */ + +export const getAliasInfo = async (options: GetAliasInfoOptionsType) => { + const { alias, aliasChain, env = Constants.ENV.PROD } = options || {}; + + const aliasChainId: number = ALIAS_CHAIN_ID[aliasChain][env]; + + const _alias = getCAIPWithChainId(alias, aliasChainId, 'Alias'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/alias`; + const requestUrl = `${apiEndpoint}/${_alias}/channel`; + + return await axiosGet(requestUrl) + .then((response) => response.data) + .catch((err) => { + console.error(`[EPNS-SDK] - API ${requestUrl}: `, err); + }); +}; diff --git a/packages/d-node-notif/src/lib/alias/index.ts b/packages/d-node-notif/src/lib/alias/index.ts new file mode 100644 index 000000000..c6e18c3d5 --- /dev/null +++ b/packages/d-node-notif/src/lib/alias/index.ts @@ -0,0 +1 @@ +export * from "./getAliasInfo"; \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/channels/_getSubscribers.ts b/packages/d-node-notif/src/lib/channels/_getSubscribers.ts new file mode 100644 index 000000000..60a05a764 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/_getSubscribers.ts @@ -0,0 +1,49 @@ +import { getCAIPAddress, getAPIBaseUrls, getCAIPDetails } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosPost } from '../utils/axiosUtil'; + +export type GetSubscribersOptionsType = { + channel: string; // plain ETH Format only + env?: ENV; +}; + +/** + * LEGACY SDK method, kept to support old functionality + * can be removed if not needed in future. + */ + +const deprecationWarning = ` + [Push SDK]: _getSubscribers() Deprecation Warning! + This method has been deprecated, please use the below alternatives + if you need to, + * to check if user is subscribed or not: user.getSubscriptions() + * get channels count: channels.getChannels() +`; + +export const _getSubscribers = async ( + options: GetSubscribersOptionsType +): Promise => { + console.warn(deprecationWarning); + + const { channel, env = Constants.ENV.PROD } = options || {}; + + const _channelAddress = await getCAIPAddress(env, channel, 'Channel'); + + const channelCAIPDetails = getCAIPDetails(_channelAddress); + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + const chainId = channelCAIPDetails.networkId; + + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/channels/_get_subscribers`; + const requestUrl = `${apiEndpoint}`; + + const body = { + channel: channelCAIPDetails.address, // deprecated API expects ETH address format + blockchain: chainId, + op: 'read', + }; + + const response = await axiosPost<{ subscribers: string[] }>(requestUrl, body); + return response.data.subscribers; +}; diff --git a/packages/d-node-notif/src/lib/channels/getChannel.ts b/packages/d-node-notif/src/lib/channels/getChannel.ts new file mode 100644 index 000000000..cd24d0011 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/getChannel.ts @@ -0,0 +1,33 @@ +import { getCAIPAddress, getAPIBaseUrls } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; +import { parseSettings } from '../utils/parseSettings'; + +/** + * GET /v1/channels/{addressinCAIP} + */ + +export type GetChannelOptionsType = { + channel: string; + env?: ENV; + raw?: boolean; +}; + +export const getChannel = async (options: GetChannelOptionsType) => { + const { channel, env = Constants.ENV.PROD, raw = true } = options || {}; + + const _channel = await getCAIPAddress(env, channel, 'Channel'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/channels`; + const requestUrl = `${apiEndpoint}/${_channel}`; + + return await axiosGet(requestUrl).then((response) => { + if (raw) return response.data; + else { + response.data.channel_settings = response.data.channel_settings + ? parseSettings(response.data.channel_settings) + : null; + return response.data; + } + }); +}; diff --git a/packages/d-node-notif/src/lib/channels/getChannelNotifications.ts b/packages/d-node-notif/src/lib/channels/getChannelNotifications.ts new file mode 100644 index 000000000..b934999d7 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/getChannelNotifications.ts @@ -0,0 +1,48 @@ +import { getCAIPAddress, getAPIBaseUrls, getQueryParams } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; +import { NotifictaionType } from '../types'; + +type GetChannelOptionsType = { + channel: string; + env?: ENV; + page?: number; + limit?: number; + filter?: NotifictaionType | null; + raw?: boolean; +}; + +export const getChannelNotifications = async ( + options: GetChannelOptionsType +) => { + const { + channel, + env = Constants.ENV.PROD, + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + filter = null, + raw = true, + } = options || {}; + + const _channel = await getCAIPAddress(env, channel, 'Channel'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v2/channels`; + const query = getQueryParams( + filter + ? { + page, + limit, + notificationType: filter, + raw, + } + : { + page, + limit, + raw, + } + ); + const requestUrl = `${apiEndpoint}/${_channel}/notifications?${query}`; + return await axiosGet(requestUrl).then((response) => { + return response.data; + }); +}; diff --git a/packages/d-node-notif/src/lib/channels/getChannels.ts b/packages/d-node-notif/src/lib/channels/getChannels.ts new file mode 100644 index 000000000..0b463a96b --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/getChannels.ts @@ -0,0 +1,42 @@ +import { ENV } from '../constants'; +import CONSTANTS from '../constants'; + +import { getAPIBaseUrls, getCAIPAddress } from '../helpers'; +import { axiosGet } from '../utils/axiosUtil'; +import { parseSettings } from '../utils/parseSettings'; + +/** + * GET /v1/channels/{addressinCAIP} + */ + +type getChannelsOptionsType = { + env?: ENV; + + page?: number; + limit?: number; + sort?: string; + order?: string; +}; + +export const getChannels = async (options: getChannelsOptionsType) => { + const { + env = CONSTANTS.ENV.PROD, + page = 1, + limit = 10, + sort = CONSTANTS.FILTER.CHANNEL_LIST.SORT.SUBSCRIBER, + order = CONSTANTS.FILTER.CHANNEL_LIST.ORDER.DESCENDING, + } = options || {}; + + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/channels`; + const requestUrl = `${apiEndpoint}?page=${page}&limit=${limit}&sort=${sort}&order=${order}`; + + return await axiosGet(requestUrl) + .then((response) => { + return response.data; + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + throw Error(`[Push SDK] - API - Error - API ${requestUrl} -: ${err}`); + }); +}; diff --git a/packages/d-node-notif/src/lib/channels/getDelegates.ts b/packages/d-node-notif/src/lib/channels/getDelegates.ts new file mode 100644 index 000000000..c2a81cbfa --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/getDelegates.ts @@ -0,0 +1,40 @@ +import { + getCAIPAddress, + getAPIBaseUrls +} from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; + +/** + * GET v1/channels/${channelAddressInCAIP}/delegates + */ + +export type GetDelegatesOptionsType = { + /** address of the channel */ + channel: string; + env?: ENV; +} + +/** + * Returns the list of addresses that the channel has delegated to + */ + +export const getDelegates = async ( + options : GetDelegatesOptionsType +) => { + const { + channel, + env = Constants.ENV.PROD, + } = options || {}; + + const _channel = await getCAIPAddress(env, channel, 'Channel'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/channels`; + const requestUrl = `${apiEndpoint}/${_channel}/delegates`; + + return await axiosGet(requestUrl) + .then((response) => response.data?.delegates) + .catch((err) => { + console.error(`[EPNS-SDK] - API ${requestUrl}: `, err); + }); +} diff --git a/packages/d-node-notif/src/lib/channels/getSubscribers.ts b/packages/d-node-notif/src/lib/channels/getSubscribers.ts new file mode 100644 index 000000000..ee9090f5c --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/getSubscribers.ts @@ -0,0 +1,77 @@ +import { getCAIPAddress, getAPIBaseUrls } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { Subscribers } from '../types'; +import { axiosGet } from '../utils/axiosUtil'; +import { parseSubscrbersApiResponse } from '../utils/parseSubscribersAPI'; + +/** + * GET /v1/channels/:channelId/:subscribers + */ + +export type GetChannelSubscribersOptionsType = { + channel: string; // plain ETH Format only + page?: number, + limit?: number, + category?: number, + setting?: boolean, + env?: ENV, + raw?: boolean +} + +export const getSubscribers = async ( + options: GetChannelSubscribersOptionsType +): Promise => { + + try { + + const { + channel, + page = 1, + limit = 10, + category = null, + setting = false, + env = Constants.ENV.PROD, + raw = true + } = options || {}; + + try { + if (channel == null || channel.length == 0) { + throw new Error(`channel cannot be null or empty`); + } + + if (page <= 0) { + throw new Error("page must be greater than 0"); + } + + if (limit <= 0) { + throw new Error("limit must be greater than 0"); + } + + if (limit > 30) { + throw new Error("limit must be lesser than or equal to 30"); + } + const _channel = await getCAIPAddress(env, channel, 'Channel'); + const API_BASE_URL = getAPIBaseUrls(env); + let apiEndpoint = `${API_BASE_URL}/v1/channels/${_channel}/subscribers?page=${page}&limit=${limit}&setting=${setting}`; + if(category){ + apiEndpoint = apiEndpoint+`&category=${category}` + } + return await axiosGet(apiEndpoint) + .then((response) => { + if(raw) + return response.data + else + return parseSubscrbersApiResponse(response.data) + }) + .catch((err) => { + console.error(`[Push SDK] - API ${apiEndpoint}: `, err); + }); + } catch (err) { + console.error(`[Push SDK] - API - Error - API send() -: `, err); + throw Error(`[Push SDK] - API - Error - API send() -: ${err}`); + } + } catch (err) { + console.error(`[Push SDK] - API - Error - API send() -: `, err); + throw Error(`[Push SDK] - API - Error - API send() -: ${err}`); + } +}; diff --git a/packages/d-node-notif/src/lib/channels/index.ts b/packages/d-node-notif/src/lib/channels/index.ts new file mode 100644 index 000000000..8728fc02a --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/index.ts @@ -0,0 +1,12 @@ +export * from './_getSubscribers'; +export * from './getChannel'; +export * from './getChannelNotifications'; +export * from './getChannels'; +export * from './getDelegates'; +export * from './getSubscribers'; +export * from './search'; +export * from './subscribe'; +export * from './subscribeV2'; +export * from './unsubscribe'; +export * from './unsubscribeV2'; + diff --git a/packages/d-node-notif/src/lib/channels/search.ts b/packages/d-node-notif/src/lib/channels/search.ts new file mode 100644 index 000000000..0abe5a628 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/search.ts @@ -0,0 +1,42 @@ +import { getAPIBaseUrls, getQueryParams, getLimit } from '../helpers'; +import Constants, {ENV} from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; + +/** + * GET /v1/channels/search/ + * optional params: page=(1)&limit=(20{min:1}{max:30})&query=(searchquery) + * + */ + +export type SearchChannelOptionsType = { + query: string; + env?: ENV; + page?: number; + limit?: number; +} + +export const search = async ( + options: SearchChannelOptionsType +) => { + const { + query, + env = Constants.ENV.PROD, + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + } = options || {}; + + if (!query) throw Error('"query" not provided!'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/channels/search/`; + const queryObj = { + page, + limit: getLimit(limit), + query: query + }; + const requestUrl = `${apiEndpoint}?${getQueryParams(queryObj)}`; + return axiosGet(requestUrl) + .then((response) => response.data.channels) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + }); +} diff --git a/packages/d-node-notif/src/lib/channels/signature.helpers.ts b/packages/d-node-notif/src/lib/channels/signature.helpers.ts new file mode 100644 index 000000000..fadb4e480 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/signature.helpers.ts @@ -0,0 +1,77 @@ +type channelActionType = 'Unsubscribe' | 'Subscribe'; + +export const getDomainInformation = ( + chainId: number, + verifyingContract: string +) => { + return { + name: 'EPNS COMM V1', + chainId, + verifyingContract, + }; +}; + +export const getSubscriptionMessage = ( + channel: string, + userAddress: string, + action: channelActionType +) => { + const actionTypeKey = + action === 'Unsubscribe' ? 'unsubscriber' : 'subscriber'; + + return { + channel, + [actionTypeKey]: userAddress, + action: action, + }; +}; + +export const getSubscriptionMessageV2 = ( + channel: string, + userAddress: string, + action: channelActionType, + userSetting?: string | null +) => { + const actionTypeKey = + action === 'Unsubscribe' ? 'unsubscriber' : 'subscriber'; + if (action == 'Subscribe') { + return JSON.stringify({ + channel, + [actionTypeKey]: userAddress, + action: action, + userSetting: userSetting?? '', + }, null, 4); + } else { + return JSON.stringify({ + channel, + [actionTypeKey]: userAddress, + action: action, + }, null, 4); + } +}; + +export const getTypeInformation = (action: string) => { + if (action === 'Subscribe') { + return { + Subscribe: [ + { name: 'channel', type: 'address' }, + { name: 'subscriber', type: 'address' }, + { name: 'action', type: 'string' }, + ], + }; + } + + return { + Unsubscribe: [ + { name: 'channel', type: 'address' }, + { name: 'unsubscriber', type: 'address' }, + { name: 'action', type: 'string' }, + ], + }; +}; + +export const getTypeInformationV2 = () => { + return { + Data: [{ name: 'data', type: 'string' }], + }; +}; diff --git a/packages/d-node-notif/src/lib/channels/subscribe.ts b/packages/d-node-notif/src/lib/channels/subscribe.ts new file mode 100644 index 000000000..00b6216f9 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/subscribe.ts @@ -0,0 +1,107 @@ +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; +import { + getTypeInformation, + getDomainInformation, + getSubscriptionMessage, +} from './signature.helpers'; +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; +import { axiosPost } from '../utils/axiosUtil'; +export type SubscribeOptionsType = { + signer: SignerType; + channelAddress: string; + userAddress: string; + verifyingContractAddress?: string; + origin?: string; + env?: ENV; + onSuccess?: () => void; + onError?: (err: Error) => void; +}; + +export const subscribe = async (options: SubscribeOptionsType) => { + const { + signer, + channelAddress, + userAddress, + verifyingContractAddress, + origin, + env = Constants.ENV.PROD, + onSuccess, + onError, + } = options || {}; + + try { + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); + + const channelCAIPDetails = getCAIPDetails(_channelAddress); + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + 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 requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/subscribe`; + + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress || EPNS_COMMUNICATOR_CONTRACT + ); + + // get type information + const typeInformation = getTypeInformation('Subscribe'); + + // get message + const messageInformation = getSubscriptionMessage( + channelCAIPDetails.address, + userCAIPDetails.address, + 'Subscribe' + ); + + // sign a message using EIP712 + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation as any, + messageInformation, + 'Subscribe' + ); + + const verificationProof = signature; // might change + + const body = { + verificationProof, + message: { + ...messageInformation, + channel: _channelAddress, + subscriber: _userAddress, + }, + origin: origin + }; + + await axiosPost(requestUrl, body); + + if (typeof onSuccess === 'function') onSuccess(); + + 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), + }; + } +}; diff --git a/packages/d-node-notif/src/lib/channels/subscribeV2.ts b/packages/d-node-notif/src/lib/channels/subscribeV2.ts new file mode 100644 index 000000000..c38fdc456 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/subscribeV2.ts @@ -0,0 +1,106 @@ +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; +import { + getDomainInformation, + getTypeInformationV2, + getSubscriptionMessageV2, +} from './signature.helpers'; +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; +import { axiosPost } from '../utils/axiosUtil'; + +export type SubscribeOptionsV2Type = { + signer: SignerType; + channelAddress: string; + userAddress: string; + settings?: string | null; + verifyingContractAddress?: string; + env?: ENV; + origin?: string; + onSuccess?: () => void; + onError?: (err: Error) => void; +}; + +export const subscribeV2 = async (options: SubscribeOptionsV2Type) => { + const { + signer, + channelAddress, + userAddress, + settings = undefined, + verifyingContractAddress, + env = Constants.ENV.PROD, + origin, + onSuccess, + onError, + } = options || {}; + try { + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); + + const channelCAIPDetails = getCAIPDetails(_channelAddress); + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + 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 requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/subscribe`; + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress || EPNS_COMMUNICATOR_CONTRACT + ); + + // get type information + const typeInformation = getTypeInformationV2(); + + // get message + const messageInformation = { + data: getSubscriptionMessageV2( + channelCAIPDetails.address, + userCAIPDetails.address, + 'Subscribe', + settings + ), + }; + // sign a message using EIP712 + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation, + messageInformation, + 'Data' + ); + + const verificationProof = signature; // might change + + const body = { + verificationProof: `eip712v2:${verificationProof}`, + message: messageInformation.data, + origin: origin + }; + + const res = await axiosPost(requestUrl, body); + + if (typeof onSuccess === 'function') onSuccess(); + + 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 ?? '', + message: err instanceof Error ? err.message : JSON.stringify(err), + }; + } +}; diff --git a/packages/d-node-notif/src/lib/channels/unsubscribe.ts b/packages/d-node-notif/src/lib/channels/unsubscribe.ts new file mode 100644 index 000000000..03c3265db --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/unsubscribe.ts @@ -0,0 +1,107 @@ +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; +import { + getTypeInformation, + getDomainInformation, + getSubscriptionMessage, +} from './signature.helpers'; +import Constants, {ENV} from '../constants'; +import { SignerType } from "../types"; +import { axiosPost } from "../utils/axiosUtil"; + + + +export type UnSubscribeOptionsType = { + signer: SignerType; + channelAddress: string; + userAddress: string; + verifyingContractAddress?: string; + env?: ENV; + onSuccess?: () => void; + onError?: (err: Error) => void; +}; + +export const unsubscribe = async (options: UnSubscribeOptionsType) => { + const { + signer, + channelAddress, + userAddress, + verifyingContractAddress, + env = Constants.ENV.PROD, + onSuccess, + onError, + } = options || {}; + + try { + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); + + const channelCAIPDetails = getCAIPDetails(_channelAddress); + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + 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 requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/unsubscribe`; + + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress || EPNS_COMMUNICATOR_CONTRACT + ); + + // get type information + const typeInformation = getTypeInformation('Unsubscribe'); + + // get message + const messageInformation = getSubscriptionMessage( + channelCAIPDetails.address, + userCAIPDetails.address, + 'Unsubscribe' + ); + + // sign a message using EIP712 + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation as any, + messageInformation, + 'Unsubscribe' + ); + + const verificationProof = signature; // might change + + const body = { + verificationProof, + message: { + ...messageInformation, + channel: _channelAddress, + unsubscriber: _userAddress, + }, + }; + + await axiosPost(requestUrl, body); + + if (typeof onSuccess === 'function') onSuccess(); + + 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), + }; + } +}; diff --git a/packages/d-node-notif/src/lib/channels/unsubscribeV2.ts b/packages/d-node-notif/src/lib/channels/unsubscribeV2.ts new file mode 100644 index 000000000..545273ca2 --- /dev/null +++ b/packages/d-node-notif/src/lib/channels/unsubscribeV2.ts @@ -0,0 +1,103 @@ +import { getCAIPAddress, getConfig, getCAIPDetails, Signer } from '../helpers'; +import { + getDomainInformation, + getTypeInformationV2, + getSubscriptionMessageV2, +} from './signature.helpers'; +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; +import { axiosPost } from '../utils/axiosUtil'; + +export type UnSubscribeOptionsV2Type = { + signer: SignerType; + channelAddress: string; + userAddress: string; + verifyingContractAddress?: string; + env?: ENV; + onSuccess?: () => void; + onError?: (err: Error) => void; +}; + +export const unsubscribeV2 = async (options: UnSubscribeOptionsV2Type) => { + const { + signer, + channelAddress, + userAddress, + verifyingContractAddress, + env = Constants.ENV.PROD, + onSuccess, + onError, + } = options || {}; + + try { + const _channelAddress = await getCAIPAddress( + env, + channelAddress, + 'Channel' + ); + + const channelCAIPDetails = getCAIPDetails(_channelAddress); + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + 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 requestUrl = `${API_BASE_URL}/v1/channels/${_channelAddress}/unsubscribe`; + + // get domain information + const domainInformation = getDomainInformation( + chainId, + verifyingContractAddress || EPNS_COMMUNICATOR_CONTRACT + ); + + // get type information + const typeInformation = getTypeInformationV2(); + + // get message + const messageInformation = { + data: getSubscriptionMessageV2( + channelCAIPDetails.address, + userCAIPDetails.address, + 'Unsubscribe' + ), + }; + + // sign a message using EIP712 + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domainInformation, + typeInformation, + messageInformation, + 'Data' + ); + + const verificationProof = signature; // might change + + const body = { + verificationProof: `eip712v2:${verificationProof}`, + message: messageInformation.data, + }; + + const res = await axiosPost(requestUrl, body); + + if (typeof onSuccess === 'function') onSuccess(); + + return { status: res.status, message: 'successfully opted out channel' }; + } catch (err: any) { + if (typeof onError === 'function') onError(err as Error); + + return { + status: err?.response?.status ?? '', + message: err instanceof Error ? err.message : JSON.stringify(err), + }; + } +}; diff --git a/packages/d-node-notif/src/lib/chat/helpers/aes.ts b/packages/d-node-notif/src/lib/chat/helpers/aes.ts new file mode 100644 index 000000000..dd90b9094 --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/aes.ts @@ -0,0 +1,21 @@ +import * as CryptoJS from "crypto-js" + +export const aesEncrypt = ({ plainText, secretKey }: { plainText: string; secretKey: string }): string => { + return CryptoJS.AES.encrypt(plainText, secretKey).toString() +} + +export const aesDecrypt = ({ cipherText, secretKey }: { cipherText: string; secretKey: string }): string => { + const bytes = CryptoJS.AES.decrypt(cipherText, secretKey) + return bytes.toString(CryptoJS.enc.Utf8) +} + +export const generateRandomSecret = (length: number): string => { + let result = '' + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + const charactersLength = characters.length + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)) + } + + return result +} diff --git a/packages/d-node-notif/src/lib/chat/helpers/crypto.ts b/packages/d-node-notif/src/lib/chat/helpers/crypto.ts new file mode 100644 index 000000000..ce6c3f3e5 --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/crypto.ts @@ -0,0 +1,197 @@ +import * as PGP from './pgp'; +import * as AES from './aes'; +import * as CryptoJS from 'crypto-js'; +import { walletType } from '../../types'; +import { get } from '../../user'; +import { + Signer, + decryptPGPKey, + decryptWithWalletRPCMethod, + isValidPushCAIP, +} from '../../helpers'; +import { get as getUser } from '../../user'; +import { createUserService } from './service'; +import Constants, { ENV } from '../../constants'; +import { getDomainInformation, getTypeInformation } from './signature'; +import { IPGPHelper } from './pgp'; + +const SIG_TYPE_V2 = 'eip712v2'; + +export const encryptAndSign = async ({ + plainText, + keys, + privateKeyArmored, + secretKey, +}: { + plainText: string; + keys: Array; + privateKeyArmored: string; + secretKey: string; +}): Promise<{ + cipherText: string; + encryptedSecret: string; + signature: string; + sigType: string; + encType: string; +}> => { + return await encryptAndSignCore({ + plainText, + keys, + privateKeyArmored, + secretKey, + pgpHelper: PGP.PGPHelper, + }); +}; + +export const encryptAndSignCore = async ({ + plainText, + keys, + privateKeyArmored, + secretKey, + pgpHelper, +}: { + plainText: string; + keys: Array; + privateKeyArmored: string; + secretKey: string; + pgpHelper: PGP.IPGPHelper; +}): Promise<{ + cipherText: string; + encryptedSecret: string; + signature: string; + sigType: string; + encType: string; +}> => { + const cipherText: string = AES.aesEncrypt({ plainText, secretKey }); + const encryptedSecret = await pgpHelper.pgpEncrypt({ + plainText: secretKey, + keys: keys, + }); + const signature: string = await pgpHelper.sign({ + message: cipherText, + signingKey: privateKeyArmored, + }); + return { + cipherText, + encryptedSecret, + signature, + sigType: 'pgp', + encType: 'pgp', + }; +}; + +export const signMessageWithPGP = async ({ + message, + privateKeyArmored, +}: { + message: string; + privateKeyArmored: string; +}): Promise<{ + signature: string; + sigType: string; +}> => { + return await signMessageWithPGPCore({ + message, + privateKeyArmored, + pgpHelper: PGP.PGPHelper, + }); +}; + +export const signMessageWithPGPCore = async ({ + message, + privateKeyArmored, + pgpHelper, +}: { + message: string; + privateKeyArmored: string; + pgpHelper: PGP.IPGPHelper; +}): Promise<{ + signature: string; + sigType: string; +}> => { + const signature: string = await pgpHelper.sign({ + message: message, + signingKey: privateKeyArmored, + }); + + return { + signature, + sigType: 'pgp', + }; +}; + +export const getEip191Signature = async ( + wallet: walletType, + message: string, + version: 'v1' | 'v2' = 'v1' +) => { + if (!wallet?.signer) { + console.warn('This method is deprecated. Provide signer in the function'); + // sending random signature for making it backward compatible + return { signature: 'xyz', sigType: 'a' }; + } + const _signer = wallet?.signer; + // EIP191 signature + + const pushSigner = new Signer(_signer); + const signature = await pushSigner.signMessage(message); + const sigType = version === 'v1' ? 'eip191' : 'eip191v2'; + return { verificationProof: `${sigType}:${signature}` }; +}; + +export const getEip712Signature = async ( + wallet: walletType, + hash: string, + isDomainEmpty: boolean +) => { + if (!wallet?.signer) { + console.warn('This method is deprecated. Provide signer in the function'); + // sending random signature for making it backward compatible + return { signature: 'xyz', sigType: 'a' }; + } + + const typeInformation = getTypeInformation(); + const _signer = wallet?.signer; + const pushSigner = new Signer(_signer); + let chainId: number; + try { + chainId = await pushSigner.getChainId(); + } catch (err) { + chainId = 1; + } + const domain = getDomainInformation(chainId); + + // sign a message using EIP712 + const signedMessage = await pushSigner.signTypedData( + isDomainEmpty ? {} : domain, + typeInformation, + { data: hash }, + 'Data' + ); + const verificationProof = isDomainEmpty + ? `${SIG_TYPE_V2}:${signedMessage}` + : `${SIG_TYPE_V2}:${chainId}:${signedMessage}`; + return { verificationProof }; +}; + +export async function getDecryptedPrivateKey( + wallet: walletType, + user: any, + address: string, + env: ENV +): Promise { + let decryptedPrivateKey; + if (wallet.signer) { + decryptedPrivateKey = await decryptPGPKey({ + signer: wallet.signer, + encryptedPGPPrivateKey: user.encryptedPrivateKey, + env, + }); + } else { + decryptedPrivateKey = await decryptWithWalletRPCMethod( + user.encryptedPrivateKey, + address + ); + } + return decryptedPrivateKey; +} diff --git a/packages/d-node-notif/src/lib/chat/helpers/index.ts b/packages/d-node-notif/src/lib/chat/helpers/index.ts new file mode 100644 index 000000000..f69d0ed3a --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/index.ts @@ -0,0 +1,6 @@ +export * from './aes'; +export * from './pgp'; +export * from './user'; +export * from './service'; +export * from './wallet'; +export * from './crypto'; diff --git a/packages/d-node-notif/src/lib/chat/helpers/pgp.ts b/packages/d-node-notif/src/lib/chat/helpers/pgp.ts new file mode 100644 index 000000000..6240dc60d --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/pgp.ts @@ -0,0 +1,176 @@ +import * as openpgp from 'openpgp'; + +interface IPGPHelper{ + generateKeyPair(): Promise<{ privateKeyArmored: string; publicKeyArmored: string }>; + sign ({ message, signingKey }: { message: string; signingKey: string }): Promise; + pgpEncrypt ({ plainText, keys }: { plainText: string; keys: Array }): Promise; + pgpDecrypt({cipherText,toPrivateKeyArmored}: { cipherText: any, toPrivateKeyArmored: string}): Promise; + verifySignature({ messageContent, signatureArmored, publicKeyArmored, }: {messageContent: string;signatureArmored: string; publicKeyArmored: string;}): Promise +} + +const PGPHelper:IPGPHelper = { + async generateKeyPair(){ + const keys = await openpgp.generateKey({ + type: 'rsa', + rsaBits: 2048, + userIDs: [{ name: '', email: '' }] + }) + return { + privateKeyArmored: keys.privateKey, + publicKeyArmored: keys.publicKey + } + }, + + async sign ({ message, signingKey }): Promise { + const messageObject: openpgp.Message = await openpgp.createMessage({ text: message }) + const privateKey: openpgp.PrivateKey = await openpgp.readPrivateKey({ armoredKey: signingKey }) + return await openpgp.sign({ message: messageObject, signingKeys: privateKey, detached: true }) + }, + + async pgpEncrypt ({ plainText, keys }): Promise { + const pgpKeys: openpgp.Key[] = []; + + for(let i = 0; i < keys.length; i++) { + pgpKeys.push(await openpgp.readKey({ armoredKey: keys[i] })); + } + const message: openpgp.Message = await openpgp.createMessage({ text: plainText }); + const encrypted: string = await openpgp.encrypt({ + message: message, + encryptionKeys: pgpKeys, + }); + return encrypted; + }, + + async pgpDecrypt({ + cipherText, + toPrivateKeyArmored + }: { + cipherText: any + toPrivateKeyArmored: string + }): Promise{ + + const message = await openpgp.readMessage({ armoredMessage: cipherText }) + const privateKey: openpgp.PrivateKey = await openpgp.readPrivateKey({ armoredKey: toPrivateKeyArmored }) + + const { data: decrypted } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey + }) + + return decrypted as string + }, + + async verifySignature({ + messageContent, + signatureArmored, + publicKeyArmored, + }: { + messageContent: string + signatureArmored: string + publicKeyArmored: string + }): Promise { + const message: openpgp.Message = await openpgp.createMessage({ text: messageContent }) + const signature: openpgp.Signature = await openpgp.readSignature({ + armoredSignature: signatureArmored + }) + const publicKey: openpgp.PublicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }) + const verificationResult = await openpgp.verify({ + message, + signature, + verificationKeys: publicKey + }) + const { verified } = verificationResult.signatures[0] + try { + await verified + } catch (e) { + throw new Error('Signature could not be verified: ' + e) + } + } + +} + +export {IPGPHelper, PGPHelper} + +export const generateKeyPair = async (): Promise<{ privateKeyArmored: string; publicKeyArmored: string }> => { + const keys = await openpgp.generateKey({ + type: 'rsa', + rsaBits: 2048, + userIDs: [{ name: '', email: '' }] + }) + return { + privateKeyArmored: keys.privateKey, + publicKeyArmored: keys.publicKey + } +} + +export const pgpEncrypt = async ({ + plainText, + keys, +}: { + plainText: string + keys: Array +}): Promise => { + + const pgpKeys: openpgp.Key[] = []; + + for(let i = 0; i < keys.length; i++) { + pgpKeys.push(await openpgp.readKey({ armoredKey: keys[i] })) + } + const message: openpgp.Message = await openpgp.createMessage({ text: plainText }) + const encrypted: string = await openpgp.encrypt({ + message: message, + encryptionKeys: pgpKeys + }) + return encrypted +} + +export const sign = async ({ message, signingKey }: { message: string; signingKey: string }): Promise => { + const messageObject: openpgp.Message = await openpgp.createMessage({ text: message }) + const privateKey: openpgp.PrivateKey = await openpgp.readPrivateKey({ armoredKey: signingKey }) + return await openpgp.sign({ message: messageObject, signingKeys: privateKey, detached: true }) +} + +export const verifySignature = async ({ + messageContent, + signatureArmored, + publicKeyArmored, +}: { + messageContent: string + signatureArmored: string + publicKeyArmored: string +}): Promise => { + const message: openpgp.Message = await openpgp.createMessage({ text: messageContent }) + const signature: openpgp.Signature = await openpgp.readSignature({ + armoredSignature: signatureArmored + }) + const publicKey: openpgp.PublicKey = await openpgp.readKey({ armoredKey: publicKeyArmored }) + const verificationResult = await openpgp.verify({ + message, + signature, + verificationKeys: publicKey + }) + const { verified } = verificationResult.signatures[0] + try { + await verified + } catch (e) { + throw new Error('Signature could not be verified: ' + e) + } +} + +export const pgpDecrypt = async ({ + cipherText, + toPrivateKeyArmored +}: { + cipherText: any + toPrivateKeyArmored: string +}): Promise => { + const message = await openpgp.readMessage({ armoredMessage: cipherText }) + const privateKey: openpgp.PrivateKey = await openpgp.readPrivateKey({ armoredKey: toPrivateKeyArmored }) + + const { data: decrypted } = await openpgp.decrypt({ + message, + decryptionKeys: privateKey + }) + + return decrypted as string +} diff --git a/packages/d-node-notif/src/lib/chat/helpers/service.ts b/packages/d-node-notif/src/lib/chat/helpers/service.ts new file mode 100644 index 000000000..2ba85e554 --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/service.ts @@ -0,0 +1,124 @@ +import Constants, { ENV } from '../../constants'; +import { + generateHash, + getAPIBaseUrls, + getQueryParams, + isValidNFTCAIP, + verifyProfileKeys, + walletToPCAIP10, +} from '../../helpers'; +import { + AccountEnvOptionsType, + ConversationHashOptionsType, + walletType, +} from '../../types'; +import { getEip191Signature } from './crypto'; +import { populateDeprecatedUser } from '../../utils/populateIUser'; +import { axiosGet, axiosPost, axiosPut } from '../../utils/axiosUtil'; + +type CreateUserOptionsType = { + user: string; + wallet?: walletType; + publicKey?: string; + encryptedPrivateKey?: string; + env?: ENV; + origin?: string | null; +}; + +export const createUserService = async (options: CreateUserOptionsType) => { + const { + wallet, + publicKey = '', + encryptedPrivateKey = '', + env = Constants.ENV.PROD, + origin, + } = options || {}; + let { user } = options || {}; + + const API_BASE_URL = getAPIBaseUrls(env); + + const requestUrl = `${API_BASE_URL}/v2/users/`; + + if (isValidNFTCAIP(user)) { + const epoch = Math.floor(Date.now() / 1000); + if (user.split(':').length !== 6) { + user = `${user}:${epoch}`; + } + } + const data = { + caip10: walletToPCAIP10(user), + did: walletToPCAIP10(user), + publicKey, + encryptedPrivateKey, + }; + + const hash = generateHash(data); + + const signatureObj = await getEip191Signature(wallet!, hash, 'v2'); + + const body = { + ...data, + origin: origin, + ...signatureObj, + }; + + return axiosPost(requestUrl, body) + .then(async (response) => { + if (response.data) + response.data.publicKey = await verifyProfileKeys( + response.data.encryptedPrivateKey, + response.data.publicKey, + response.data.did, + response.data.wallets, + response.data.verificationProof + ); + return populateDeprecatedUser(response.data); + }) + .catch((err) => { + throw Error(`[Push SDK] - API ${requestUrl}: ${err}`); + }); +}; + +export const authUpdateUserService = async (options: CreateUserOptionsType) => { + const { + user, + wallet, + publicKey = '', + encryptedPrivateKey = '', + env = Constants.ENV.PROD, + } = options || {}; + + const API_BASE_URL = getAPIBaseUrls(env); + + const requestUrl = `${API_BASE_URL}/v2/users/${walletToPCAIP10(user)}/auth`; + + const data = { + caip10: walletToPCAIP10(user), + did: walletToPCAIP10(user), + publicKey, + encryptedPrivateKey, + }; + + const hash = generateHash(data); + + const signatureObj = await getEip191Signature(wallet!, hash, 'v2'); + + // Exclude the "did" property from the "body" object + const { did, ...body } = { ...data, ...signatureObj }; + + return axiosPut(requestUrl, body) + .then(async (response) => { + if (response.data) + response.data.publicKey = await verifyProfileKeys( + response.data.encryptedPrivateKey, + response.data.publicKey, + response.data.did, + response.data.wallets, + response.data.verificationProof + ); + return populateDeprecatedUser(response.data); + }) + .catch((err) => { + throw Error(`[Push SDK] - API ${requestUrl}: ${err}`); + }); +}; diff --git a/packages/d-node-notif/src/lib/chat/helpers/signature.ts b/packages/d-node-notif/src/lib/chat/helpers/signature.ts new file mode 100644 index 000000000..96b22587c --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/signature.ts @@ -0,0 +1,150 @@ +import { + recoverTypedSignature, + SignTypedDataVersion, +} from '@metamask/eth-sig-util'; +import * as viem from 'viem'; + +/** + * + * @param chainId + * @returns + */ +export const getDomainInformation = (chainId: number) => { + const chatVerifyingContract = '0x0000000000000000000000000000000000000000'; + return { + name: 'PUSH CHAT ID', + chainId, + verifyingContract: chatVerifyingContract, + }; +}; + +/** + * + * @param action + * @returns + */ +export const getTypeInformation = () => { + return { + Data: [{ name: 'data', type: 'string' }], + }; +}; + +/** + * + * @param signedData + * @param chainId + * @param version + * @returns typedData for typedV4 EIP712 sig + */ +export const getTypedData = ( + signedData: string, + chainId: number, + version: 'V1' | 'V2' +) => { + const message = { data: signedData }; + const typeInformation = getTypeInformation(); + const domainInformation = getDomainInformation(chainId); + const primaryType = 'Data' as const; + let types: any; + let domain = {}; + + if (version === 'V1') { + types = { + EIP712Domain: [], + Data: typeInformation.Data, + }; + } else { + types = { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Data: typeInformation.Data, + }; + domain = domainInformation; + } + + return { + types, + primaryType, + domain, + message, + }; +}; + +/** + * + * @param signature + * @param signedData + * @param address + * @param chainId + * @returns + */ +export const verifyProfileSignature = async ( + verificationProof: string, + signedData: string, + address: string +): Promise => { + const SIG_TYPE_V2 = 'eip712v2'; + const SIG_TYPE_V3 = 'eip191'; + const SIG_TYPE_V4 = 'eip191v2'; + let chainId: number | null = null; + let signature: string; + const sigType = verificationProof.split(':')[0]; + if ( + (sigType !== SIG_TYPE_V2 && + sigType !== SIG_TYPE_V3 && + sigType !== SIG_TYPE_V4) || + verificationProof.split(':').length > 3 + ) { + return false; + } + if (verificationProof.split(':').length === 2) { + signature = verificationProof.split(':')[1]; + } else { + chainId = parseInt(verificationProof.split(':')[1]); + signature = verificationProof.split(':')[2]; + } + + if (sigType === SIG_TYPE_V2) { + try { + // EIP712 sig validation with empty domain + // V2 should be checked first rather than v1 otherwise validation will fail + const typedData = getTypedData(signedData, chainId as number, 'V2'); // For backward compatibility + const recoveredAddress = recoverTypedSignature({ + data: typedData, + signature: signature, + version: SignTypedDataVersion.V4, + }); + if (recoveredAddress.toLowerCase() === address.toLowerCase()) { + return true; + } else return false; + } catch (err) { + // EIP712 sig validation with domain details + const typedData = getTypedData(signedData, chainId as number, 'V1'); // For backward compatibility + const recoveredAddress = recoverTypedSignature({ + data: typedData, + signature: signature, + version: SignTypedDataVersion.V4, + }); + if (recoveredAddress.toLowerCase() === address.toLowerCase()) { + return true; + } else return false; + } + } else { + // EIP191 sig validation + try { + // EOA Wallet + 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) { + return false; + } + } +}; diff --git a/packages/d-node-notif/src/lib/chat/helpers/user.ts b/packages/d-node-notif/src/lib/chat/helpers/user.ts new file mode 100644 index 000000000..b9e672d15 --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/user.ts @@ -0,0 +1,63 @@ +import Constants, { ENV } from '../../constants'; +import { get, createUserCore } from '../../user'; +import { IConnectedUser, SignerType, walletType } from '../../types'; +import { getAccountAddress } from './wallet'; +import { IPGPHelper, PGPHelper, getDecryptedPrivateKey } from '.'; + +export const getConnectedUserV2 = async ( + wallet: walletType, + privateKey: string | null, + env: ENV +): Promise => { + return await getConnectedUserV2Core(wallet, privateKey, env, PGPHelper); +}; + +export const getConnectedUserV2Core = async ( + wallet: walletType, + privateKey: string | null, + env: ENV, + pgpHelper: IPGPHelper +): Promise => { + const address = await getAccountAddress(wallet); + const user = await get({ account: address, env: env || Constants.ENV.PROD }); + if (user?.encryptedPrivateKey) { + if (privateKey) { + return { ...user, privateKey }; + } else { + console.warn( + "Please note that if you don't pass the pgpPrivateKey parameter, a wallet popup will appear every time the approveRequest endpoint is called. We strongly recommend passing this parameter, and it will become mandatory in future versions of the API." + ); + const decryptedPrivateKey = await getDecryptedPrivateKey( + wallet, + user, + address, + env + ); + return { ...user, privateKey: decryptedPrivateKey }; + } + } else { + const createUserProps: { + account?: string; + signer?: SignerType; + env?: ENV; + } = {}; + if (wallet.account) { + createUserProps.account = wallet.account; + } + if (user && user.did) { + createUserProps.account = user.did; + } + if (wallet.signer) { + createUserProps.signer = wallet.signer; + } + createUserProps.env = env; + const newUser = await createUserCore(createUserProps, pgpHelper); + const decryptedPrivateKey = await getDecryptedPrivateKey( + wallet, + newUser, + address, + env + ); + return { ...newUser, privateKey: decryptedPrivateKey }; + } +}; diff --git a/packages/d-node-notif/src/lib/chat/helpers/wallet.ts b/packages/d-node-notif/src/lib/chat/helpers/wallet.ts new file mode 100644 index 000000000..1935f188b --- /dev/null +++ b/packages/d-node-notif/src/lib/chat/helpers/wallet.ts @@ -0,0 +1,25 @@ +import { Signer, pCAIP10ToWallet } from '../../helpers'; +import { SignerType, walletType } from '../../types'; + +export const getWallet = ( + options: walletType +): { + account: string | null; + signer: SignerType | null; +} => { + const { account, signer } = options || {}; + + return { + account: account ? pCAIP10ToWallet(account) : account, + signer, + }; +}; + +export const getAccountAddress = async ( + options: walletType +): Promise => { + const { account, signer } = options || {}; + + const pushSigner = new Signer(signer as SignerType); + return account || (await pushSigner.getAddress()) || ''; +}; diff --git a/packages/d-node-notif/src/lib/config.ts b/packages/d-node-notif/src/lib/config.ts new file mode 100644 index 000000000..2fe58b612 --- /dev/null +++ b/packages/d-node-notif/src/lib/config.ts @@ -0,0 +1,581 @@ +import { ENV } from './constants'; +import { coreABI } from './abis/core'; +import { commABI } from './abis/comm'; +import { tokenABI } from './abis/token'; +import { + mainnet, + polygon, + bsc, + bscTestnet, + optimism, + optimismSepolia, + polygonZkEvm, + sepolia, + arbitrum, + arbitrumSepolia, + fuse, + fuseSparknet, + berachainTestnet, + polygonAmoy, + polygonZkEvmCardona, + cyberTestnet, + cyber, +} from 'viem/chains'; + +// for methods not needing the entire config +export const API_BASE_URL = { + [ENV.PROD]: 'https://backend.epns.io/apis', + [ENV.STAGING]: 'https://backend-staging.epns.io/apis', + [ENV.DEV]: 'https://backend-dev.epns.io/apis', + /** + * **This is for local development only** + */ + [ENV.LOCAL]: 'http://localhost:4000/apis', +}; + +const BLOCKCHAIN_NETWORK = { + ETH_MAINNET: 'eip155:1', + ETH_SEPOLIA: 'eip155:11155111', + POLYGON_MAINNET: 'eip155:137', + POLYGON_AMOY: 'eip155:80002', + BSC_MAINNET: 'eip155:56', + BSC_TESTNET: 'eip155:97', + OPTIMISM_TESTNET: 'eip155:11155420', + OPTIMISM_MAINNET: 'eip155:10', + POLYGON_ZK_EVM_TESTNET: 'eip155:2442', + POLYGON_ZK_EVM_MAINNET: 'eip155:1101', + ARBITRUM_TESTNET: 'eip155:421614', + ARBITRUMONE_MAINNET: 'eip155:42161', + FUSE_TESTNET: 'eip155:123', + FUSE_MAINNET: 'eip155:122', + BERACHAIN_TESTNET: 'eip155:80085', + CYBER_CONNECT_TESTNET: 'eip155:111557560', + CYBER_CONNECT_MAINNET: 'eip155:7560', +}; + +export type ALIAS_CHAIN = + | 'POLYGON' + | 'BSC' + | 'OPTIMISM' + | 'POLYGONZKEVM' + | 'ARBITRUMONE' + | 'FUSE' + | 'BERACHAIN' + | 'CYBERCONNECT'; + +export const ETH_CHAIN_ID = { + [ENV.PROD]: 1, + [ENV.STAGING]: 11155111, + [ENV.DEV]: 11155111, + [ENV.LOCAL]: 11155111, +}; +export const ALIAS_CHAIN_ID: { + [key: string]: { [key in ENV]: number }; +} = { + POLYGON: { + [ENV.PROD]: 137, + [ENV.STAGING]: 80002, + [ENV.DEV]: 80002, + [ENV.LOCAL]: 80002, + }, + BSC: { + [ENV.PROD]: 56, + [ENV.STAGING]: 97, + [ENV.DEV]: 97, + [ENV.LOCAL]: 97, + }, + OPTIMISM: { + [ENV.PROD]: 10, + [ENV.STAGING]: 11155420, + [ENV.DEV]: 11155420, + [ENV.LOCAL]: 11155420, + }, + POLYGONZKEVM: { + [ENV.PROD]: 1101, + [ENV.STAGING]: 2442, + [ENV.DEV]: 2442, + [ENV.LOCAL]: 2442, + }, + ARBITRUMONE: { + [ENV.PROD]: 42161, + [ENV.STAGING]: 421614, + [ENV.DEV]: 421614, + [ENV.LOCAL]: 421614, + }, + FUSE: { + [ENV.PROD]: 122, + [ENV.STAGING]: 123, + [ENV.DEV]: 123, + [ENV.LOCAL]: 123, + }, + BERACHAIN: { + [ENV.PROD]: 0, // TODO: update this + [ENV.STAGING]: 80085, + [ENV.DEV]: 80085, + [ENV.LOCAL]: 80085, + }, + CYBERCONNECT: { + [ENV.PROD]: 7560, + [ENV.STAGING]: 111557560, + [ENV.DEV]: 111557560, + [ENV.LOCAL]: 111557560, + }, +}; + +export const CHAIN_ID = { + ETHEREUM: ETH_CHAIN_ID, + ...ALIAS_CHAIN_ID, +}; + +export const CHAIN_NAME: { [key: number]: string } = { + // eth + 1: 'ETHEREUM', + 11155111: 'ETHEREUM', + // polygon + 137: 'POLYGON', + 80002: 'POLYGON', + // bsc + 56: 'BSC', + 97: 'BSC', + // optimism + 10: 'OPTIMISM', + 11155420: 'OPTIMISM', + // plygonzkevm + 1101: 'POLYGONZKEVM', + 2442: 'POLYGONZKEVM', + // arbitrun + 421614: 'ARBITRUN', + 42161: 'ARBITRUM', + // fuse + 122: 'FUSE', + 123: 'FUSE', + // berachain + 80085: 'BERACHAIN', + // cyberconnect + 7560: 'CYBER_CONNECT_MAINNET', + 111557560: 'CYBER_CONNECT_TESTNET', +}; +export interface ConfigType { + API_BASE_URL: string; + EPNS_COMMUNICATOR_CONTRACT: string; +} + +export const VIEM_CORE_CONFIG = { + [ENV.PROD]: { + NETWORK: mainnet, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_CORE_CONTRACT: '0x66329Fdd4042928BfCAB60b179e1538D56eeeeeE', + }, + [ENV.STAGING]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_CORE_CONTRACT: '0x9d65129223451fbd58fc299c635cd919baf2564c', + }, + [ENV.DEV]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_CORE_CONTRACT: '0x5ab1520e2bd519bdab2e1347eee81c00a77f4946', + }, + [ENV.LOCAL]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_CORE_CONTRACT: '0x5ab1520e2bd519bdab2e1347eee81c00a77f4946', + }, +}; + +export const CORE_CONFIG = { + [ENV.PROD]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_CORE_CONTRACT: '0x66329Fdd4042928BfCAB60b179e1538D56eeeeeE', + }, + [ENV.STAGING]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_CORE_CONTRACT: '0x9d65129223451fbd58fc299c635cd919baf2564c', + }, + [ENV.DEV]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_CORE_CONTRACT: '0x5ab1520e2bd519bdab2e1347eee81c00a77f4946', + }, + [ENV.LOCAL]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_CORE_CONTRACT: '0x5ab1520e2bd519bdab2e1347eee81c00a77f4946', + }, +}; + +const CONFIG = { + [ENV.PROD]: { + [BLOCKCHAIN_NETWORK.ETH_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.POLYGON_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BSC_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.ARBITRUMONE_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.FUSE_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_MAINNET]: { + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xbf895df410b7fcbe093973600950ba392f7e1d8e', + }, + }, + [ENV.STAGING]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x0c34d54a09cfe75bccd878a469206ae77e0fe6e7', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9Dc25996ba72A2FD7E64e7a674232a683f406F1A', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x6e489b7af21ceb969f49a90e481274966ce9d74d', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9Dc25996ba72A2FD7E64e7a674232a683f406F1A', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x7b9C405e261ba671f008c20D0321f62d08C140EC', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x6e489B7af21cEb969f49A90E481274966ce9D74d', + }, + }, + [ENV.DEV]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9dDCD7ed7151afab43044E4D694FA064742C428c', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x4132061E3349ff36cFfCadA460E10Bd4f31F7ea8', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x7eBb54D86CF928115965DB596a3E600404dD8039', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0xA1DF3E68D085aa6918bcc2506b24e499830Db0eB', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550B5c92baA056Fc0F08132f49508145F', + }, + }, + [ENV.LOCAL]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x9dDCD7ed7151afab43044E4D694FA064742C428c', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x4132061E3349ff36cFfCadA460E10Bd4f31F7ea8', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x7eBb54D86CF928115965DB596a3E600404dD8039', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0xA1DF3E68D085aa6918bcc2506b24e499830Db0eB', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550B5c92baA056Fc0F08132f49508145F', + }, + }, +}; + +export default CONFIG; +export const TOKEN = { + [ENV.PROD]: '0xf418588522d5dd018b425E472991E52EBBeEEEEE', + [ENV.STAGING]: '0x37c779a1564DCc0e3914aB130e0e787d93e21804', + [ENV.DEV]: '0x37c779a1564DCc0e3914aB130e0e787d93e21804', + [ENV.LOCAL]: '0x37c779a1564DCc0e3914aB130e0e787d93e21804', +}; + +export const TOKEN_VIEM_NETWORK_MAP = { + [ENV.PROD]: mainnet, + [ENV.STAGING]: sepolia, + [ENV.DEV]: sepolia, + [ENV.LOCAL]: sepolia, +}; + +export const MIN_TOKEN_BALANCE = { + [ENV.PROD]: 50, + [ENV.STAGING]: 50, + [ENV.DEV]: 50, + [ENV.LOCAL]: 50, +}; +export const ABIS = { + CORE: coreABI, + COMM: commABI, + TOKEN: tokenABI, +}; + +export const CHANNEL_TYPE = { + TIMEBOUND: 4, + GENERAL: 2, +}; + +export const VIEM_CONFIG = { + [ENV.PROD]: { + [BLOCKCHAIN_NETWORK.ETH_MAINNET]: { + NETWORK: mainnet, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.POLYGON_MAINNET]: { + NETWORK: polygon, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BSC_MAINNET]: { + NETWORK: bsc, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_MAINNET]: { + NETWORK: optimism, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_MAINNET]: { + NETWORK: polygonZkEvm, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.ARBITRUMONE_MAINNET]: { + NETWORK: arbitrum, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.FUSE_MAINNET]: { + NETWORK: fuse, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_MAINNET]: { + NETWORK: cyber, + API_BASE_URL: API_BASE_URL[ENV.PROD], + EPNS_COMMUNICATOR_CONTRACT: '0xbf895df410b7fcbe093973600950ba392f7e1d8e', + }, + }, + [ENV.STAGING]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x0c34d54a09cfe75bccd878a469206ae77e0fe6e7', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + NETWORK: polygonAmoy, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + NETWORK: bscTestnet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + NETWORK: optimismSepolia, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9Dc25996ba72A2FD7E64e7a674232a683f406F1A', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + NETWORK: polygonZkEvmCardona, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x6e489b7af21ceb969f49a90e481274966ce9d74d', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + NETWORK: arbitrumSepolia, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9Dc25996ba72A2FD7E64e7a674232a683f406F1A', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + NETWORK: fuseSparknet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0xb3971BCef2D791bc4027BbfedFb47319A4AAaaAa', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + NETWORK: berachainTestnet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x7b9C405e261ba671f008c20D0321f62d08C140EC', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + NETWORK: cyberTestnet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x6e489B7af21cEb969f49A90E481274966ce9D74d', + }, + }, + [ENV.DEV]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9dDCD7ed7151afab43044E4D694FA064742C428c', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + NETWORK: polygonAmoy, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + NETWORK: bscTestnet, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x4132061E3349ff36cFfCadA460E10Bd4f31F7ea8', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + NETWORK: optimismSepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + NETWORK: polygonZkEvmCardona, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + NETWORK: arbitrumSepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + NETWORK: fuseSparknet, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x7eBb54D86CF928115965DB596a3E600404dD8039', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + NETWORK: berachainTestnet, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0xA1DF3E68D085aa6918bcc2506b24e499830Db0eB', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + NETWORK: cyberTestnet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550B5c92baA056Fc0F08132f49508145F', + }, + }, + [ENV.LOCAL]: { + [BLOCKCHAIN_NETWORK.ETH_SEPOLIA]: { + NETWORK: sepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9dDCD7ed7151afab43044E4D694FA064742C428c', + }, + [BLOCKCHAIN_NETWORK.POLYGON_AMOY]: { + NETWORK: polygonAmoy, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.BSC_TESTNET]: { + NETWORK: bscTestnet, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x4132061E3349ff36cFfCadA460E10Bd4f31F7ea8', + }, + [BLOCKCHAIN_NETWORK.OPTIMISM_TESTNET]: { + NETWORK: optimismSepolia, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.POLYGON_ZK_EVM_TESTNET]: { + NETWORK: polygonZkEvmCardona, + API_BASE_URL: API_BASE_URL[ENV.DEV], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550b5c92baa056fc0f08132f49508145f', + }, + [BLOCKCHAIN_NETWORK.ARBITRUM_TESTNET]: { + NETWORK: arbitrumSepolia, + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x754787358fac861ef904c92d54f7adb659779317', + }, + [BLOCKCHAIN_NETWORK.FUSE_TESTNET]: { + NETWORK: fuseSparknet, + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0x7eBb54D86CF928115965DB596a3E600404dD8039', + }, + [BLOCKCHAIN_NETWORK.BERACHAIN_TESTNET]: { + NETWORK: berachainTestnet, + API_BASE_URL: API_BASE_URL[ENV.LOCAL], + EPNS_COMMUNICATOR_CONTRACT: '0xA1DF3E68D085aa6918bcc2506b24e499830Db0eB', + }, + [BLOCKCHAIN_NETWORK.CYBER_CONNECT_TESTNET]: { + NETWORK: cyberTestnet, + API_BASE_URL: API_BASE_URL[ENV.STAGING], + EPNS_COMMUNICATOR_CONTRACT: '0x9cb3bd7550B5c92baA056Fc0F08132f49508145F', + }, + }, +}; + +export const ALPHA_FEATURE_CONFIG = { + STABLE: { + feature: [] as string[], + }, + ALPHA: { + feature: [] as string[], + }, +}; diff --git a/packages/d-node-notif/src/lib/constants.ts b/packages/d-node-notif/src/lib/constants.ts new file mode 100644 index 000000000..8441695ef --- /dev/null +++ b/packages/d-node-notif/src/lib/constants.ts @@ -0,0 +1,71 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require('../../package.json'); + +import { + ChannelListOrderType, + ChannelListSortType, + ChannelListType, +} from './pushNotification/PushNotificationTypes'; +import { STREAM } from './pushstream/pushStreamTypes'; +import { NotifictaionType } from './types'; + +/** + * SUPPORTED ENVIRONEMENTS + */ +export enum ENV { + PROD = 'prod', + STAGING = 'staging', + DEV = 'dev', + /** + * **This is for local development only** + */ + LOCAL = 'local', +} + +/** + * SUPPORTED ENCRYPTIONS FOR PUSH PROFILE + */ +export enum ENCRYPTION_TYPE { + PGP_V1 = 'x25519-xsalsa20-poly1305', + PGP_V2 = 'aes256GcmHkdfSha256', + PGP_V3 = 'eip191-aes256-gcm-hkdf-sha256', + NFTPGP_V1 = 'pgpv1:nft', +} + +export const ALPHA_FEATURES = {}; + +export const PACKAGE_BUILD = packageJson.version.includes('alpha') + ? 'ALPHA' + : 'STABLE'; + +const CONSTANTS = { + ENV: ENV, + STREAM: STREAM, + ALPHA_FEATURES: ALPHA_FEATURES, + USER: { ENCRYPTION_TYPE: ENCRYPTION_TYPE }, + NOTIFICATION: { + TYPE: NotifictaionType, + CHANNEL: { + LIST_TYPE: ChannelListType, + }, + }, + FILTER: { + CHANNEL_LIST: { + SORT: ChannelListSortType, + ORDER: ChannelListOrderType, + }, + NOTIFICATION_TYPE: NotifictaionType, + }, + PAGINATION: { + INITIAL_PAGE: 1, + LIMIT: 10, + LIMIT_MIN: 1, + LIMIT_MAX: 50, + }, + ENC_TYPE_V1: 'x25519-xsalsa20-poly1305', + ENC_TYPE_V2: 'aes256GcmHkdfSha256', + ENC_TYPE_V3: 'eip191-aes256-gcm-hkdf-sha256', + ENC_TYPE_V4: 'pgpv1:nft', +}; + +export default CONSTANTS; diff --git a/packages/d-node-notif/src/lib/errors/httpStatus.ts b/packages/d-node-notif/src/lib/errors/httpStatus.ts new file mode 100644 index 000000000..767542bd9 --- /dev/null +++ b/packages/d-node-notif/src/lib/errors/httpStatus.ts @@ -0,0 +1,6 @@ +export enum HttpStatus { + BadRequest = 400, + NotFound = 404, + Forbidden = 403, + InternalError=500 +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/errors/validationError.ts b/packages/d-node-notif/src/lib/errors/validationError.ts new file mode 100644 index 000000000..631707d30 --- /dev/null +++ b/packages/d-node-notif/src/lib/errors/validationError.ts @@ -0,0 +1,124 @@ +import { HttpStatus } from "./httpStatus"; + +export class ValidationError extends Error { + status: HttpStatus; + errorCode: string; + details: string; + + constructor( + status: HttpStatus, + errorCode: string, + message: string, + details: string + ) { + super(message); + this.name = 'ValidationError'; + this.status = status; + this.errorCode = errorCode; + this.details = details; + } + + format(): object { + return { + status: this.status, + errorCode: this.errorCode, + message: this.message, + details: this.details, + timestamp: new Date().toISOString(), + }; + } +} + +export function isErrorWithResponse(error: unknown): error is { + response: { + status: number; + data: { + status: number; + errorCode: string; + message: string; + details: string; + timestamp: string; + }; + }; +} { + return ( + typeof error === 'object' && + error !== null && + 'response' in error && + typeof (error as { response: any }).response === 'object' && + (error as { response: { status: number } }).response !== null && + 'status' in (error as { response: any }).response && + 'data' in (error as { response: any }).response && + typeof (error as { response: { data: any } }).response.data === 'object' && + 'status' in (error as { response: { data: any } }).response.data && + 'errorCode' in (error as { response: { data: any } }).response.data && + 'message' in (error as { response: { data: any } }).response.data && + 'details' in (error as { response: { data: any } }).response.data && + 'timestamp' in (error as { response: { data: any } }).response.data + ); +} + +export function isErrorWithResponseV2(error: unknown): error is { + response: { + status: number; + data: { + error: string; + message: string; + validation: string; + }; + }; +} { + return ( + typeof error === 'object' && + error !== null && + 'response' in error && + typeof (error as { response: any }).response === 'object' && + (error as { response: { status: number } }).response !== null && + 'status' in (error as { response: any }).response && + 'data' in (error as { response: any }).response && + typeof (error as { response: { data: any } }).response.data === 'object' && + 'error' in (error as { response: { data: any } }).response.data && + 'message' in (error as { response: { data: any } }).response.data && + 'validation' in (error as { response: { data: any } }).response.data + ); +} + +export function handleError(error: unknown, context: string): ValidationError { + let status = HttpStatus.InternalError; + let errorCode = '00000000000'; + let message = + 'An unexpected error occurred. Please contact support or try again later.'; + let details = ''; + + if (isErrorWithResponse(error)) { + status = error.response?.status || HttpStatus.InternalError; + if (error.response?.data) { + const errData = error.response.data; + errorCode = errData.errorCode || errorCode; + message = errData.message || message; + details = errData.details || JSON.stringify(errData); + } + } else if (isErrorWithResponseV2(error)) { + status = error.response?.status || HttpStatus.InternalError; + const errData = error.response.data; + message = errData.message || message; + details = errData.validation || JSON.stringify(errData); + } else if (error instanceof Error) { + message = error.message; + details = error.stack || ''; + } + + const validationError = new ValidationError( + status, + errorCode, + message, + details + ); + + const logPrefix = `[Error - API ${context}]`; + console.error( + `${logPrefix} ${JSON.stringify(validationError.format(), null, 2)}` + ); + + return validationError; +} diff --git a/packages/d-node-notif/src/lib/helpers/address.ts b/packages/d-node-notif/src/lib/helpers/address.ts new file mode 100644 index 000000000..f50796aa5 --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/address.ts @@ -0,0 +1,258 @@ +import * as viem from 'viem'; +import Constants, { ENV } from '../constants'; +import { get } from '../user'; + +export interface AddressValidatorsType { + [key: string]: ({ address }: { address: string }) => boolean; +} + +/** + * CHECK IF THE WALLET IS A VALID PUSH CAIP SCW DID + * @param wallet scw:eip155:chainId:address + * @returns boolean + */ +export const isValidSCWCAIP = (wallet: string) => { + try { + const walletComponent = wallet.split(':'); + return ( + walletComponent.length === 4 && + walletComponent[0] === 'scw' && + walletComponent[1] === 'eip155' && + !isNaN(Number(walletComponent[2])) && + Number(walletComponent[2]) > 0 && + viem.isAddress(walletComponent[3]) + ); + } catch (err) { + return false; + } +}; + +/** + * CHECK IF THE WALLET IS A VALID PUSH CAIP NFT DID + * @param wallet nft:eip155:nftChainId:nftContractAddress:nftTokenId + * @returns boolean + */ +export const isValidNFTCAIP = (wallet: string): boolean => { + try { + const walletComponent = wallet.split(':'); + return ( + (walletComponent.length === 5 || walletComponent.length === 6) && + walletComponent[0].toLowerCase() === 'nft' && + !isNaN(Number(walletComponent[4])) && + Number(walletComponent[4]) > 0 && + !isNaN(Number(walletComponent[2])) && + Number(walletComponent[2]) > 0 && + viem.isAddress(walletComponent[3]) && + walletComponent[1] === 'eip155' + ); + } catch (err) { + return false; + } +}; + +/** + * CHECK IF THE WALLET IS A VALID PUSH CAIP EOA DID + * @param wallet eip155:chainId:address | eip155:address + * @returns + */ +export const isValidEOACAIP = (wallet: string): boolean => { + try { + const walletComponent = wallet.split(':'); + if (walletComponent.length === 3) { + return ( + walletComponent[0] === 'eip155' && + !isNaN(Number(walletComponent[1])) && + Number(walletComponent[1]) > 0 && + viem.isAddress(walletComponent[2]) + ); + } + if (walletComponent.length === 2) { + return ( + walletComponent[0] === 'eip155' && viem.isAddress(walletComponent[1]) + ); + } + return false; + } catch (err) { + return false; + } +}; + +/** + * CHECK IF THE WALLET IS A VALID PUSH CAIP + * @param wallet + * @returns boolean + */ +export const isValidPushCAIP = (wallet: string): boolean => { + return ( + isValidEOACAIP(wallet) || + isValidSCWCAIP(wallet) || + isValidNFTCAIP(wallet) || + viem.isAddress(wallet) + ); +}; + +/** + * CONVERT A VALID PUSH CAIP TO A VALID PUSH DID + * @param wallet valid wallet CAIP + * @param env optional env + * @param chainId optional chainId + * @param provider optional provider + * @returns valid Push DID + */ +export const convertToValidDID = async ( + wallet: string, + env: ENV = ENV.STAGING, + chainId?: number, + provider?: any +) => { + /** @dev Why Not throw error? - Used by Group ChatID also */ + if (!isValidPushCAIP(wallet)) return wallet; + if ( + isValidEOACAIP(wallet) || + isValidSCWCAIP(wallet) || + (isValidNFTCAIP(wallet) && wallet.split(':').length === 6) + ) + return wallet; + + if (isValidNFTCAIP(wallet)) { + const user = await get({ account: wallet, env: env }); + if (user && user.did) return user.did; + const epoch = Math.floor(Date.now() / 1000); + return `${wallet}:${epoch}`; + } + + // TODO: Implement SCW DID CHECK + if (provider) { + try { + // check if onChain code exists + } catch (err) { + // Ignore if it fails + } + } + + return chainId ? `eip155:${chainId}:${wallet}` : `eip155:${wallet}`; +}; + +/** + * CHECK IF THE WALLET IS A VALID FULL CAIP10 + * @param wallet eip155:chainId:address + * @returns boolean + */ +export const isValidFullCAIP10 = (wallet: string) => { + const walletComponent = wallet.split(':'); + if (isNaN(Number(walletComponent[1]))) return false; + return ( + walletComponent[0] === 'eip155' && + !isNaN(Number(walletComponent[1])) && + Number(walletComponent[1]) > 0 && + viem.isAddress(walletComponent[2]) + ); +}; + +const AddressValidators: AddressValidatorsType = { + // Ethereum + eip155: ({ address }: { address: string }) => { + return isValidPushCAIP(address); + }, + // Add other chains here +}; + +export function validateCAIP(addressInCAIP: string) { + const [blockchain, networkId, address] = addressInCAIP.split(':'); + + if (!blockchain) return false; + if (!networkId) return false; + if (!address) return false; + + if (isValidNFTCAIP(addressInCAIP)) return true; + + const validatorFn = AddressValidators[blockchain]; + + return validatorFn({ address }); +} + +export type CAIPDetailsType = { + blockchain: string; + networkId: string; + address: string; +}; + +export function getCAIPDetails(addressInCAIP: string): CAIPDetailsType | null { + if (validateCAIP(addressInCAIP)) { + const [blockchain, networkId, address] = addressInCAIP.split(':'); + + return { + blockchain, + networkId, + address, + }; + } + + return null; +} + +export function getFallbackETHCAIPAddress(env: ENV, address: string) { + let chainId = 1; // by default PROD + + if ( + env === Constants.ENV.DEV || + env === Constants.ENV.STAGING || + env === Constants.ENV.LOCAL + ) { + chainId = 11155111; + } + + return `eip155:${chainId}:${address}`; +} + +/** + * This helper + * checks if a VALID CAIP + * return the CAIP + * else + * check if valid ETH + * return a CAIP representation of that address (EIP155 + env) + * else + * throw error! + */ +export async function getCAIPAddress(env: ENV, address: string, msg?: string) { + if (isValidNFTCAIP(address)) { + return await convertToValidDID(address, env); + } + if (validateCAIP(address)) { + return address; + } else { + if (isValidPushCAIP(address)) { + return getFallbackETHCAIPAddress(env, address); + } else { + throw Error(`Invalid Address! ${msg} \n Address: ${address}`); + } + } +} + +export const getCAIPWithChainId = ( + address: string, + chainId: number, + msg?: string +) => { + if (isValidPushCAIP(address)) { + if (!address.includes('eip155:')) return `eip155:${chainId}:${address}`; + else return address; + } else { + throw Error(`Invalid Address! ${msg} \n Address: ${address}`); + } +}; + +// P = Partial CAIP +export const walletToPCAIP10 = (account: string): string => { + if (isValidNFTCAIP(account) || account.includes('eip155:')) { + return account; + } + return 'eip155:' + account; +}; + +export const pCAIP10ToWallet = (wallet: string): string => { + if (isValidNFTCAIP(wallet)) return wallet; + wallet = wallet.replace('eip155:', ''); + return wallet; +}; diff --git a/packages/d-node-notif/src/lib/helpers/api.ts b/packages/d-node-notif/src/lib/helpers/api.ts new file mode 100644 index 000000000..54cfc1db6 --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/api.ts @@ -0,0 +1,19 @@ +import Constants from '../constants'; + +export function getQueryParams(obj: any) { + return Object.keys(obj) + .map(key => { + return `${key}=${encodeURIComponent(obj[key])}`; + }) + .join('&'); +} + +export function getLimit(passedLimit?: number) { + if (!passedLimit) return Constants.PAGINATION.LIMIT; + + // if (passedLimit > Constants.PAGINATION.LIMIT_MAX) { + // return Constants.PAGINATION.LIMIT_MAX; + // } + + return passedLimit; +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/helpers/cache.ts b/packages/d-node-notif/src/lib/helpers/cache.ts new file mode 100644 index 000000000..8f0b73c8c --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/cache.ts @@ -0,0 +1,13 @@ +import { LRUCache } from 'lru-cache'; + +export const cache = new LRUCache({ + max: 200, + maxSize: 500 * 1024, // 500KB + sizeCalculation: (value, key) => { + return typeof value === 'string' + ? value.length + : new TextEncoder().encode(JSON.stringify(value)).length; + }, + ttl: 1000 * 60 * 5, // 5 minutes + allowStale: false, +}); diff --git a/packages/d-node-notif/src/lib/helpers/config.ts b/packages/d-node-notif/src/lib/helpers/config.ts new file mode 100644 index 000000000..3a48744dc --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/config.ts @@ -0,0 +1,41 @@ +import CONFIG, { API_BASE_URL, ConfigType } from '../config'; +import {ENV} from '../constants'; + +/** + * This config helper returns the API url as well as the + * EPNS communicator contract method address + */ +export const getConfig = ( + env: ENV, + { + blockchain, + networkId + }: { + blockchain: string, + networkId: string + } +): ConfigType => { + + const blockchainSelector = `${blockchain}:${networkId}`; + const configuration = CONFIG[env][blockchainSelector]; + + if (!configuration) { + throw Error(` + [Push SDK] - cannot determine config for + env: ${env}, + blockchain: ${blockchain}, + networkId: ${networkId} + `) + } + + return configuration; +}; + + +/** + * This config helper returns only the API urls + */ +export function getAPIBaseUrls(env: ENV) { + if (!env) throw Error('ENV not provided!'); + return API_BASE_URL[env]; +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/helpers/crypto.ts b/packages/d-node-notif/src/lib/helpers/crypto.ts new file mode 100644 index 000000000..f71c071db --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/crypto.ts @@ -0,0 +1,585 @@ +import * as metamaskSigUtil from '@metamask/eth-sig-util'; +import { + decrypt as metamaskDecrypt, + getEncryptionPublicKey, +} from '@metamask/eth-sig-util'; +import * as CryptoJS from 'crypto-js'; +import { + getAccountAddress, + getWallet, + getEip712Signature, + getEip191Signature, +} from '../chat/helpers'; +import Constants, { ENV } from '../constants'; +import { + SignerType, + walletType, + encryptedPrivateKeyType, + encryptedPrivateKeyTypeV2, + ProgressHookType, + ProgressHookTypeFunction, +} from '../types'; +import { isValidNFTCAIP, isValidPushCAIP, pCAIP10ToWallet } from './address'; +import { verifyProfileSignature } from '../chat/helpers/signature'; +import { upgrade } from '../user/upgradeUser'; +import PROGRESSHOOK from '../progressHook'; +import { Signer } from './signer'; +import * as viem from 'viem'; +import { mainnet } from 'viem/chains'; + +const KDFSaltSize = 32; // bytes +const AESGCMNonceSize = 12; // property iv + +let crypto: Crypto; +if (typeof window !== 'undefined' && window.crypto) { + crypto = window.crypto; +} else if (typeof require !== 'undefined') { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + crypto = require('crypto').webcrypto; + } catch (e) { + throw new Error('Unable To load crypto'); + } +} + +/** + * @deprecated + */ +export const getPublicKey = async (options: walletType): Promise => { + const { account, signer } = options || {}; + 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 + */ +export const encryptV1 = ( + text: string, + encryptionPublicKey: string, + version: string +) => { + const encryptedSecret = metamaskSigUtil.encrypt({ + publicKey: encryptionPublicKey, + data: text, + version: version, + }); + + return encryptedSecret; +}; + +/** @deprecated */ +export const decryptWithWalletRPCMethod = async ( + encryptedPGPPrivateKey: string, + account: string +) => { + console.warn( + 'decryptWithWalletRPCMethod method is DEPRECATED. Use decryptPGPKey method with signer!' + ); + return await decryptPGPKey({ + encryptedPGPPrivateKey, + account, + }); +}; + +type decryptPgpKeyProps = { + encryptedPGPPrivateKey: string; + account?: string; + signer?: SignerType | null; + env?: ENV; + toUpgrade?: boolean; + additionalMeta?: { + NFTPGP_V1?: { + password: string; + }; + }; + progressHook?: (progress: ProgressHookType) => void; +}; + +export const decryptPGPKey = async (options: decryptPgpKeyProps) => { + const { + encryptedPGPPrivateKey, + account = null, + signer = null, + env = Constants.ENV.PROD, + toUpgrade = true, + additionalMeta = null, + progressHook, + } = options || {}; + try { + if (account == null && signer == null) { + throw new Error(`At least one from account or signer is necessary!`); + } + + const wallet = getWallet({ account, signer }); + const address = await getAccountAddress(wallet); + + if (!isValidPushCAIP(address)) { + throw new Error(`Invalid address!`); + } + + const { version: encryptionType } = JSON.parse(encryptedPGPPrivateKey); + let privateKey; + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-DECRYPT-01'] as ProgressHookType); + + switch (encryptionType) { + case Constants.ENC_TYPE_V1: { + if (wallet?.signer?.privateKey) { + privateKey = metamaskDecrypt({ + encryptedData: JSON.parse(encryptedPGPPrivateKey), + privateKey: wallet?.signer?.privateKey.substring(2), + }); + } else { + const metamaskProvider = viem.createWalletClient({ + chain: mainnet, + transport: viem.custom((window as any).ethereum), + }); + const web3Provider = signer?.provider?.provider || metamaskProvider; + privateKey = await web3Provider.request({ + method: 'eth_decrypt', + params: [encryptedPGPPrivateKey, address], + }); + } + break; + } + case Constants.ENC_TYPE_V2: { + if (!wallet?.signer) { + throw new Error( + 'Cannot Decrypt this encryption version without signer!' + ); + } + const { preKey: input } = JSON.parse(encryptedPGPPrivateKey); + const enableProfileMessage = 'Enable Push Chat Profile \n' + input; + let encodedPrivateKey: Uint8Array; + try { + const { verificationProof: secret } = await getEip712Signature( + wallet, + enableProfileMessage, + true + ); + encodedPrivateKey = await decryptV2( + JSON.parse(encryptedPGPPrivateKey), + hexToBytes(secret || '') + ); + } catch (err) { + const { verificationProof: secret } = await getEip712Signature( + wallet, + enableProfileMessage, + false + ); + encodedPrivateKey = await decryptV2( + JSON.parse(encryptedPGPPrivateKey), + hexToBytes(secret || '') + ); + } + const dec = new TextDecoder(); + privateKey = dec.decode(encodedPrivateKey); + break; + } + case Constants.ENC_TYPE_V3: { + if (!wallet?.signer) { + throw new Error( + 'Cannot Decrypt this encryption version without signer!' + ); + } + const { preKey: input } = JSON.parse(encryptedPGPPrivateKey); + const enableProfileMessage = 'Enable Push Profile \n' + input; + const { verificationProof: secret } = await getEip191Signature( + wallet, + enableProfileMessage + ); + const encodedPrivateKey = await decryptV2( + JSON.parse(encryptedPGPPrivateKey), + hexToBytes(secret || '') + ); + const dec = new TextDecoder(); + privateKey = dec.decode(encodedPrivateKey); + break; + } + case Constants.ENC_TYPE_V4: { + let password: string | null = null; + if (additionalMeta?.NFTPGP_V1) { + password = additionalMeta.NFTPGP_V1.password; + } else { + if (!wallet?.signer) { + throw new Error( + 'Cannot Decrypt this encryption version without signer!' + ); + } + const { encryptedPassword } = JSON.parse(encryptedPGPPrivateKey); + password = await decryptPGPKey({ + encryptedPGPPrivateKey: JSON.stringify(encryptedPassword), + signer, + env, + }); + } + const encodedPrivateKey = await decryptV2( + JSON.parse(encryptedPGPPrivateKey), + hexToBytes(stringToHex(password as string)) + ); + const dec = new TextDecoder(); + privateKey = dec.decode(encodedPrivateKey); + break; + } + default: + throw new Error('Invalid Encryption Type'); + } + + // try key upgradation + if (signer && toUpgrade && encryptionType !== Constants.ENC_TYPE_V4) { + try { + await upgrade({ env, account: address, signer, progressHook }); + } catch (err) { + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-01' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(err)); + } + } + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-DECRYPT-02'] as ProgressHookType); + return privateKey; + } catch (err) { + // TODO: Remove Later + console.log(err); + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-00' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(decryptPGPKey.name, err)); + throw Error( + `[Push SDK] - API - Error - API ${decryptPGPKey.name} -: ${JSON.stringify( + err + )}` + ); + } +}; + +export const generateHash = (message: any): string => { + const hash = CryptoJS.SHA256(JSON.stringify(message)).toString( + CryptoJS.enc.Hex + ); + return hash; +}; + +const getRandomValues = async (array: Uint8Array) => { + return crypto.getRandomValues(array); +}; + +const bytesToHex = (bytes: Uint8Array): string => { + return bytes.reduce( + (str, byte) => str + byte.toString(16).padStart(2, '0'), + '' + ); +}; + +export const hexToBytes = (hex: string): Uint8Array => { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16); + } + return bytes; +}; + +export const stringToHex = (str: string): string => { + let hex = ''; + for (let i = 0; i < str.length; i++) { + hex += str.charCodeAt(i).toString(16).padStart(2, '0'); + } + return hex; +}; + +// Derive AES-256-GCM key from a shared secret and salt +const hkdf = async ( + secret: Uint8Array, + salt: Uint8Array +): Promise => { + const key = await crypto.subtle.importKey('raw', secret, 'HKDF', false, [ + 'deriveKey', + ]); + return crypto.subtle.deriveKey( + { name: 'HKDF', hash: 'SHA-256', salt, info: new ArrayBuffer(0) }, + key, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); +}; + +/** AES-GCM Encryption */ +export const encryptV2 = async ( + data: Uint8Array, + secret: Uint8Array, + additionalData?: Uint8Array +): Promise => { + const salt = crypto.getRandomValues(new Uint8Array(KDFSaltSize)); + const nonce = crypto.getRandomValues(new Uint8Array(AESGCMNonceSize)); + const key = await hkdf(secret, salt); + + const aesGcmParams: AesGcmParams = { + name: 'AES-GCM', + iv: nonce, + }; + if (additionalData) { + aesGcmParams.additionalData = additionalData; + } + const encrypted: ArrayBuffer = await crypto.subtle.encrypt( + aesGcmParams, + key, + data + ); + return { + ciphertext: bytesToHex(new Uint8Array(encrypted)), + salt: bytesToHex(salt), + nonce: bytesToHex(nonce), + }; +}; + +/** AES-GCM Decryption */ +export const decryptV2 = async ( + encryptedData: encryptedPrivateKeyTypeV2, + secret: Uint8Array, + additionalData?: Uint8Array +): Promise => { + const key = await hkdf(secret, hexToBytes(encryptedData.salt as string)); + const aesGcmParams: AesGcmParams = { + name: 'AES-GCM', + iv: hexToBytes(encryptedData.nonce), + }; + if (additionalData) { + aesGcmParams.additionalData = additionalData; + } + const decrypted: ArrayBuffer = await crypto.subtle.decrypt( + aesGcmParams, + key, + hexToBytes(encryptedData.ciphertext) + ); + return new Uint8Array(decrypted); +}; + +export const encryptPGPKey = async ( + encryptionType: string, + privateKey: string, + wallet: walletType, + additionalMeta?: { + NFTPGP_V1?: { + password: string; + }; + SCWPGP_V1?: { + password: string; + }; + }, + env: ENV = ENV.STAGING +): Promise => { + let encryptedPrivateKey: encryptedPrivateKeyType; + switch (encryptionType) { + case Constants.ENC_TYPE_V1: { + let walletPublicKey: string; + if (wallet?.signer?.privateKey) { + // get metamask specific encryption public key + walletPublicKey = getEncryptionPublicKey( + wallet?.signer?.privateKey.substring(2) + ); + } else { + // wallet popup will happen to get encryption public key + walletPublicKey = await getPublicKey(wallet); + } + encryptedPrivateKey = encryptV1( + privateKey, + walletPublicKey, + encryptionType + ); + break; + } + case Constants.ENC_TYPE_V2: { + const input = bytesToHex(await getRandomValues(new Uint8Array(32))); + const enableProfileMessage = 'Enable Push Chat Profile \n' + input; + const { verificationProof: secret } = await getEip712Signature( + wallet, + enableProfileMessage, + true + ); + const enc = new TextEncoder(); + const encodedPrivateKey = enc.encode(privateKey); + encryptedPrivateKey = await encryptV2( + encodedPrivateKey, + hexToBytes(secret || '') + ); + encryptedPrivateKey.version = Constants.ENC_TYPE_V2; + encryptedPrivateKey.preKey = input; + break; + } + case Constants.ENC_TYPE_V3: { + const input = bytesToHex(await getRandomValues(new Uint8Array(32))); + const enableProfileMessage = 'Enable Push Profile \n' + input; + const { verificationProof: secret } = await getEip191Signature( + wallet, + enableProfileMessage + ); + const enc = new TextEncoder(); + const encodedPrivateKey = enc.encode(privateKey); + encryptedPrivateKey = await encryptV2( + encodedPrivateKey, + hexToBytes(secret || '') + ); + encryptedPrivateKey.version = Constants.ENC_TYPE_V3; + encryptedPrivateKey.preKey = input; + break; + } + case Constants.ENC_TYPE_V4: { + if (!additionalMeta?.NFTPGP_V1?.password) { + throw new Error('Password is required!'); + } + const enc = new TextEncoder(); + const encodedPrivateKey = enc.encode(privateKey); + encryptedPrivateKey = await encryptV2( + encodedPrivateKey, + hexToBytes(stringToHex(additionalMeta.NFTPGP_V1.password)) + ); + encryptedPrivateKey.version = Constants.ENC_TYPE_V4; + encryptedPrivateKey.preKey = ''; + encryptedPrivateKey.encryptedPassword = await encryptPGPKey( + Constants.ENC_TYPE_V3, + additionalMeta.NFTPGP_V1.password, + wallet + ); + break; + } + default: + throw new Error('Invalid Encryption Type'); + } + return encryptedPrivateKey; +}; + +export const preparePGPPublicKey = async ( + encryptionType: string, + publicKey: string, + wallet: walletType +): Promise => { + let chatPublicKey: string; + switch (encryptionType) { + case Constants.ENC_TYPE_V1: { + chatPublicKey = publicKey; + break; + } + case Constants.ENC_TYPE_V2: + case Constants.ENC_TYPE_V3: + case Constants.ENC_TYPE_V4: { + const verificationProof = 'DEPRECATED'; + // TODO - Change JSON Structure to string ie equivalent to ENC_TYPE_V1 ( would be done after PUSH Node changes ) + chatPublicKey = JSON.stringify({ + key: publicKey, + signature: verificationProof, + }); + break; + } + default: + throw new Error('Invalid Encryption Type'); + } + return chatPublicKey; +}; + +/** + * Checks the Push Profile keys using verificationProof + * @param encryptedPrivateKey + * @param publicKey + * @param did + * @param caip10 + * @param verificationProof + * @returns PGP Public Key + */ +export const verifyProfileKeys = async ( + encryptedPrivateKey: string, + publicKey: string, + did: string, + caip10: string, + verificationProof: string +): Promise => { + let parsedPublicKey: string; + try { + parsedPublicKey = JSON.parse(publicKey).key; + if (parsedPublicKey === undefined) { + throw new Error('Invalid Public Key'); + } + } catch (err) { + parsedPublicKey = publicKey; + } + + try { + if ( + publicKey && + publicKey.length > 0 && + verificationProof && + // Allow pgp sig validation after eip191v2 only + verificationProof.split(':')[0] === 'eip191v2' + ) { + const data = { + caip10, + did, + publicKey, + encryptedPrivateKey, + }; + + if (isValidNFTCAIP(did)) { + const keyToRemove = 'owner'; + const parsedEncryptedPrivateKey = JSON.parse(encryptedPrivateKey); + if (keyToRemove in parsedEncryptedPrivateKey) { + delete parsedEncryptedPrivateKey[keyToRemove]; + } + data.encryptedPrivateKey = JSON.stringify(parsedEncryptedPrivateKey); + } + + const signedData = generateHash(data); + + const isValidSig: boolean = await verifyProfileSignature( + verificationProof, + signedData, + isValidNFTCAIP(did) + ? pCAIP10ToWallet(JSON.parse(encryptedPrivateKey).owner) + : pCAIP10ToWallet(did) + ); + if (isValidSig) { + return parsedPublicKey; + } else { + throw new Error('Invalid Signature'); + } + } + return parsedPublicKey; + } catch (err) { + console.warn(`Cannot Verify keys for DID : ${did} !!!`); + return parsedPublicKey; + } +}; + +export const validatePssword = (password: string) => { + if (password.length < 8) { + throw new Error('Password must be at least 8 characters long!'); + } + if (!/[A-Z]/.test(password)) { + throw new Error('Password must contain at least one uppercase letter!'); + } + if (!/[a-z]/.test(password)) { + throw new Error('Password must contain at least one lowercase letter!'); + } + if (!/\d/.test(password)) { + throw new Error('Password must contain at least one digit!'); + } + if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) { + throw new Error('Password must contain at least one special character!'); + } +}; diff --git a/packages/d-node-notif/src/lib/helpers/index.ts b/packages/d-node-notif/src/lib/helpers/index.ts new file mode 100644 index 000000000..182450d34 --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/index.ts @@ -0,0 +1,5 @@ +export * from './config'; +export * from './address'; +export * from './api'; +export * from './crypto'; +export * from './signer'; diff --git a/packages/d-node-notif/src/lib/helpers/signer.ts b/packages/d-node-notif/src/lib/helpers/signer.ts new file mode 100644 index 000000000..e75f6b201 --- /dev/null +++ b/packages/d-node-notif/src/lib/helpers/signer.ts @@ -0,0 +1,89 @@ +import { SignerType, viemSignerType } from '../types'; +import { TypedDataField, TypedDataDomain } from '../types'; + +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 { + 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, + }); + } 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'); + } + } + + async getAddress(): Promise { + if (this.isViemSigner(this.signer)) { + return this.signer.account['address'] ?? ''; + } else { + return await this.signer.getAddress(); + } + } + + 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/d-node-notif/src/lib/index.ts b/packages/d-node-notif/src/lib/index.ts new file mode 100644 index 000000000..5deef1403 --- /dev/null +++ b/packages/d-node-notif/src/lib/index.ts @@ -0,0 +1,4 @@ +import CONSTANTS from './constants'; + +export { CONSTANTS }; +export { PushAPI } from './pushAPI/PushAPI'; diff --git a/packages/d-node-notif/src/lib/payloads/constants.ts b/packages/d-node-notif/src/lib/payloads/constants.ts new file mode 100644 index 000000000..1656878ed --- /dev/null +++ b/packages/d-node-notif/src/lib/payloads/constants.ts @@ -0,0 +1,112 @@ +export interface ChainIdToSourceType { + [key: number]: string; +} + +export const CHAIN_ID_TO_SOURCE: ChainIdToSourceType = { + 1: 'ETH_MAINNET', + 11155111: 'ETH_TEST_SEPOLIA', + 137: 'POLYGON_MAINNET', + 80002: 'POLYGON_TEST_AMOY', + 56: 'BSC_MAINNET', + 97: 'BSC_TESTNET', + 10: 'OPTIMISM_MAINNET', + 11155420: 'OPTIMISM_TESTNET', + 2442: 'POLYGON_ZK_EVM_TESTNET', + 1101: 'POLYGON_ZK_EVM_MAINNET', + 421614: 'ARBITRUM_TESTNET', + 42161: 'ARBITRUMONE_MAINNET', + 122: 'FUSE_MAINNET', + 123: 'FUSE_TESTNET', + 80085: 'BERACHAIN_TESTNET', + 7560: 'CYBER_CONNECT_MAINNET', + 111557560: 'CYBER_CONNECT_TESTNET', +}; + +export const SOURCE_TYPES = { + ETH_MAINNET: 'ETH_MAINNET', + ETH_TEST_SEPOLIA: 'ETH_TEST_SEPOLIA', + POLYGON_MAINNET: 'POLYGON_MAINNET', + POLYGON_TEST_AMOY: 'POLYGON_TEST_AMOY', + BSC_MAINNET: 'BSC_MAINNET', + BSC_TESTNET: 'BSC_TESTNET', + OPTIMISM_MAINNET: 'OPTIMISM_MAINNET', + OPTIMISM_TESTNET: 'OPTIMISM_TESTNET', + POLYGON_ZK_EVM_TESTNET: 'POLYGON_ZK_EVM_TESTNET', + POLYGON_ZK_EVM_MAINNET: 'POLYGON_ZK_EVM_MAINNET', + ARBITRUM_TESTNET: 'ARBITRUM_TESTNET', + ARBITRUMONE_MAINNET: 'ARBITRUMONE_MAINNET', + FUSE_TESTNET: 'FUSE_TESTNET', + FUSE_MAINNET: 'FUSE_MAINNET', + BERACHAIN_TESTNET: 'BERACHAIN_TESTNET', + THE_GRAPH: 'THE_GRAPH', + PUSH_VIDEO: 'PUSH_VIDEO', + SIMULATE: 'SIMULATE', + CYBER_CONNECT_TESTNET: 'CYBER_CONNECT_TESTNET', + CYBER_CONNECT_MAINNET: 'CYBER_CONNECT_MAINNET', +}; + +export const SUPPORTED_CHAINS = [ + 1, 11155111, 42, 137, 80002, 56, 97, 10, 11155420, 2442, 1101, 421614, 42161, + 122, 123, 80085, 111557560, 7560, +]; + +export enum IDENTITY_TYPE { + MINIMAL = 0, + IPFS = 1, + DIRECT_PAYLOAD = 2, + SUBGRAPH = 3, +} + +export enum NOTIFICATION_TYPE { + BROADCAST = 1, + TARGETTED = 3, + SUBSET = 4, +} + +export enum ADDITIONAL_META_TYPE { + CUSTOM = 0, + PUSH_VIDEO = 1, + PUSH_SPACE = 2, +} + +// Subset of ADDITIONAL_META_TYPE, to be used exclusively for Push Video, Spaces +export enum VIDEO_CALL_TYPE { + PUSH_VIDEO = 1, + PUSH_SPACE = 2, +} + +export enum SPACE_REQUEST_TYPE { + JOIN_SPEAKER, // space has started, join as a speaker + ESTABLISH_MESH, // request to establish mesh connection + INVITE_TO_PROMOTE, // host invites someone to be promoted as the speaker + REQUEST_TO_PROMOTE, // someone requests the host to be promoted to a spaeker +} + +export enum SPACE_ACCEPT_REQUEST_TYPE { + ACCEPT_JOIN_SPEAKER, + ACCEPT_INVITE, + ACCEPT_PROMOTION, +} + +export enum SPACE_DISCONNECT_TYPE { + STOP, // space is stopped/ended + LEAVE, // speaker leaves a space +} + +export enum SPACE_INVITE_ROLES { + CO_HOST, + SPEAKER, +} + +export enum SPACE_ROLES { + HOST, + CO_HOST, + SPEAKER, + LISTENER, +} + +export const DEFAULT_DOMAIN = 'push.org'; + +export enum VIDEO_NOTIFICATION_ACCESS_TYPE { + PUSH_CHAT = 'PUSH_CHAT', +} diff --git a/packages/d-node-notif/src/lib/payloads/helpers.ts b/packages/d-node-notif/src/lib/payloads/helpers.ts new file mode 100644 index 000000000..6b58987bd --- /dev/null +++ b/packages/d-node-notif/src/lib/payloads/helpers.ts @@ -0,0 +1,337 @@ +import { v4 as uuidv4 } from 'uuid'; +import { ENV } from '../constants'; +import { Signer, getCAIPAddress } from '../helpers'; +import * as CryptoJS from 'crypto-js'; + +import { + ISendNotificationInputOptions, + INotificationPayload, + walletType, + VideoNotificationRules, +} from '../types'; +import { + IDENTITY_TYPE, + NOTIFICATION_TYPE, + CHAIN_ID_TO_SOURCE, + SOURCE_TYPES, + SUPPORTED_CHAINS, +} from './constants'; +import { sign } from '../chat/helpers'; + +export function getUUID() { + return uuidv4(); +} + +/** + * This function will map the Input options passed to the SDK to the "payload" structure + * needed by the API input + * + * We need notificationPayload only for identityType + * - DIRECT_PAYLOAD + * - MINIMAL + */ +export function getPayloadForAPIInput( + inputOptions: ISendNotificationInputOptions, + recipients: any +): INotificationPayload | null { + if (inputOptions?.notification && inputOptions?.payload) { + return { + notification: { + title: inputOptions?.notification?.title, + body: inputOptions?.notification?.body, + }, + data: { + acta: inputOptions?.payload?.cta || '', + aimg: inputOptions?.payload?.img || '', + amsg: inputOptions?.payload?.body || '', + asub: inputOptions?.payload?.title || '', + type: inputOptions?.type?.toString() || '', + //deprecated + ...(inputOptions?.expiry && { etime: inputOptions?.expiry }), + ...(inputOptions?.payload?.etime && { + etime: inputOptions?.payload?.etime, + }), + //deprecated + ...(inputOptions?.hidden && { hidden: inputOptions?.hidden }), + ...(inputOptions?.payload?.hidden && { + hidden: inputOptions?.payload?.hidden, + }), + ...(inputOptions?.payload?.silent && { + silent: inputOptions?.payload?.silent, + }), + ...(inputOptions?.payload?.sectype && { + sectype: inputOptions?.payload?.sectype, + }), + //deprecated + ...(inputOptions?.payload?.metadata && { + metadata: inputOptions?.payload?.metadata, + }), + ...(inputOptions?.payload?.additionalMeta && { + additionalMeta: inputOptions?.payload?.additionalMeta, + }), + ...(inputOptions?.payload?.index && { + index: inputOptions?.payload?.index, + }), + }, + recipients: recipients, + }; + } + + return null; +} + +/** + * This function returns the recipient format accepted by the API for different notification types + */ +export async function getRecipients({ + env, + notificationType, + channel, + recipients, + secretType, +}: { + env: ENV; + notificationType: NOTIFICATION_TYPE; + channel: string; + recipients?: string | string[]; + secretType?: string; +}) { + let addressInCAIP = ''; + + if (secretType) { + let secret = ''; + // return ''; + /** + * Currently SECRET FLOW is yet to be finalized on the backend, so will revisit this later. + * But in secret flow we basically generate secret for the address + * and send it in { 0xtarget: secret_generated_for_0xtarget } format for all + */ + if (notificationType === NOTIFICATION_TYPE.TARGETTED) { + if (typeof recipients === 'string') { + addressInCAIP = await getCAIPAddress(env, recipients, 'Recipient'); + secret = ''; // do secret stuff // TODO + + return { + [addressInCAIP]: secret, + }; + } + } else if (notificationType === NOTIFICATION_TYPE.SUBSET) { + if (Array.isArray(recipients)) { + const recipientObject = recipients.reduce( + async (_recipients, _rAddress) => { + addressInCAIP = await getCAIPAddress(env, _rAddress, 'Recipient'); + secret = ''; // do secret stuff // TODO + + return { + ..._recipients, + [addressInCAIP]: secret, + }; + }, + {} + ); + + return recipientObject; + } + } + } else { + /** + * NON-SECRET FLOW + */ + + if (notificationType === NOTIFICATION_TYPE.BROADCAST) { + return await getCAIPAddress(env, channel, 'Recipient'); + } else if (notificationType === NOTIFICATION_TYPE.TARGETTED) { + if (typeof recipients === 'string') { + return await getCAIPAddress(env, recipients, 'Recipient'); + } + } else if (notificationType === NOTIFICATION_TYPE.SUBSET) { + if (Array.isArray(recipients)) { + if (Array.isArray(recipients)) { + const recipientObject: any = {}; + recipients.map(async (_rAddress: string) => { + addressInCAIP = await getCAIPAddress(env, _rAddress, 'Recipient'); + recipientObject[addressInCAIP] = null; + }); + return recipientObject; + } + } + } + } + return recipients; +} + +export async function getRecipientFieldForAPIPayload({ + env, + notificationType, + recipients, + channel, +}: { + env: ENV; + notificationType: NOTIFICATION_TYPE; + recipients: string | string[]; + channel: string; +}) { + if ( + notificationType === NOTIFICATION_TYPE.TARGETTED && + typeof recipients === 'string' + ) { + return await getCAIPAddress(env, recipients, 'Recipient'); + } + + return await getCAIPAddress(env, channel, 'Recipient'); +} + +export async function getVerificationProof({ + senderType, + signer, + chainId, + notificationType, + identityType, + verifyingContract, + payload, + ipfsHash, + graph = {}, + uuid, + chatId, + wallet, + pgpPrivateKey, + env, + rules, +}: { + senderType: 0 | 1; + signer: any; + chainId: number; + notificationType: NOTIFICATION_TYPE; + identityType: IDENTITY_TYPE; + verifyingContract: string; + payload: any; + ipfsHash?: string; + graph?: any; + uuid: string; + // for notifications which have additionalMeta in payload + chatId?: string; + wallet?: walletType; + pgpPrivateKey?: string; + env?: ENV; + rules?: VideoNotificationRules; +}) { + let message = null; + let verificationProof = null; + + switch (identityType) { + case IDENTITY_TYPE.MINIMAL: { + message = { + data: `${identityType}+${notificationType}+${payload.notification.title}+${payload.notification.body}`, + }; + break; + } + case IDENTITY_TYPE.IPFS: { + message = { + data: `1+${ipfsHash}`, + }; + break; + } + case IDENTITY_TYPE.DIRECT_PAYLOAD: { + const payloadJSON = JSON.stringify(payload); + message = { + data: `2+${payloadJSON}`, + }; + break; + } + case IDENTITY_TYPE.SUBGRAPH: { + message = { + data: `3+graph:${graph?.id}+${graph?.counter}`, + }; + break; + } + default: { + throw new Error('Invalid IdentityType'); + } + } + + switch (senderType) { + case 0: { + const type = { + Data: [{ name: 'data', type: 'string' }], + }; + const domain = { + name: 'EPNS COMM V1', + chainId: chainId, + verifyingContract: verifyingContract, + }; + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domain, + type, + message, + 'Data' + ); + verificationProof = `eip712v2:${signature}::uid::${uuid}`; + break; + } + case 1: { + const hash = CryptoJS.SHA256(JSON.stringify(message)).toString(); + const signature = await sign({ + message: hash, + signingKey: pgpPrivateKey!, + }); + verificationProof = `pgpv2:${signature}:meta:${chatId}::uid::${uuid}`; + break; + } + default: { + throw new Error('Invalid SenderType'); + } + } + return verificationProof; +} + +export function getPayloadIdentity({ + identityType, + payload, + notificationType, + ipfsHash, + graph = {}, +}: { + identityType: IDENTITY_TYPE; + payload: any; + notificationType?: NOTIFICATION_TYPE; + ipfsHash?: string; + graph?: any; +}) { + if (identityType === IDENTITY_TYPE.MINIMAL) { + return `0+${notificationType}+${payload.notification.title}+${payload.notification.body}`; + } else if (identityType === IDENTITY_TYPE.IPFS) { + return `1+${ipfsHash}`; + } else if (identityType === IDENTITY_TYPE.DIRECT_PAYLOAD) { + const payloadJSON = JSON.stringify(payload); + return `2+${payloadJSON}`; + } else if (identityType === IDENTITY_TYPE.SUBGRAPH) { + return `3+graph:${graph?.id}+${graph?.counter}`; + } + + return null; +} + +export function getSource( + chainId: number, + identityType: IDENTITY_TYPE, + senderType: 0 | 1 +) { + if (senderType === 1) { + return SOURCE_TYPES.PUSH_VIDEO; + } + if (identityType === IDENTITY_TYPE.SUBGRAPH) { + return SOURCE_TYPES.THE_GRAPH; + } + return CHAIN_ID_TO_SOURCE[chainId]; +} + +export function getCAIPFormat(chainId: number, address: string) { + // EVM based chains + if (SUPPORTED_CHAINS.includes(chainId)) { + return `eip155:${chainId}:${address}`; + } + + return address; + // TODO: add support for other non-EVM based chains +} diff --git a/packages/d-node-notif/src/lib/payloads/index.ts b/packages/d-node-notif/src/lib/payloads/index.ts new file mode 100644 index 000000000..22436ff9a --- /dev/null +++ b/packages/d-node-notif/src/lib/payloads/index.ts @@ -0,0 +1,9 @@ +export * from './sendNotifications'; +export { + NOTIFICATION_TYPE, + IDENTITY_TYPE, + ADDITIONAL_META_TYPE, + SPACE_REQUEST_TYPE, + SPACE_ACCEPT_REQUEST_TYPE, + SPACE_DISCONNECT_TYPE +} from './constants'; diff --git a/packages/d-node-notif/src/lib/payloads/sendNotifications.ts b/packages/d-node-notif/src/lib/payloads/sendNotifications.ts new file mode 100644 index 000000000..ea02b3c12 --- /dev/null +++ b/packages/d-node-notif/src/lib/payloads/sendNotifications.ts @@ -0,0 +1,257 @@ +import { ISendNotificationInputOptions } from '../types'; +import { + getPayloadForAPIInput, + getPayloadIdentity, + getRecipients, + getRecipientFieldForAPIPayload, + getVerificationProof, + getSource, + getUUID, +} from './helpers'; +import { + getAPIBaseUrls, + getCAIPAddress, + getCAIPDetails, + getConfig, + isValidNFTCAIP, + isValidPushCAIP, +} from '../helpers'; +import { + IDENTITY_TYPE, + DEFAULT_DOMAIN, + NOTIFICATION_TYPE, + SOURCE_TYPES, + VIDEO_CALL_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, +} from './constants'; +import { ENV } from '../constants'; +import { axiosPost } from '../utils/axiosUtil'; +/** + * Validate options for some scenarios + */ +function validateOptions(options: ISendNotificationInputOptions) { + if (!options?.channel) { + throw '[Push SDK] - Error - sendNotification() - "channel" is mandatory!'; + } + if (!isValidPushCAIP(options.channel)) { + throw '[Push SDK] - Error - sendNotification() - "channel" is invalid!'; + } + if (options.senderType === 0 && options.signer === undefined) { + throw '[Push SDK] - Error - sendNotification() - "signer" is mandatory!'; + } + if (options.senderType === 1 && options.pgpPrivateKey === undefined) { + throw '[Push SDK] - Error - sendNotification() - "pgpPrivateKey" is mandatory!'; + } + + /** + * Apart from IPFS, GRAPH use cases "notification", "payload" is mandatory + */ + if ( + options?.identityType === IDENTITY_TYPE.DIRECT_PAYLOAD || + options?.identityType === IDENTITY_TYPE.MINIMAL + ) { + if (!options.notification) { + throw '[Push SDK] - Error - sendNotification() - "notification" mandatory for Identity Type: Direct Payload, Minimal!'; + } + if (!options.payload) { + throw '[Push SDK] - Error - sendNotification() - "payload" mandatory for Identity Type: Direct Payload, Minimal!'; + } + } + + const isAdditionalMetaPayload = options.payload?.additionalMeta; + + const isVideoOrSpaceType = + typeof options.payload?.additionalMeta === 'object' && + (options.payload.additionalMeta.type === + `${VIDEO_CALL_TYPE.PUSH_VIDEO}+1` || + options.payload.additionalMeta.type === + `${VIDEO_CALL_TYPE.PUSH_SPACE}+1`); + + if ( + isAdditionalMetaPayload && + isVideoOrSpaceType && + !options.chatId && + !options.rules + ) { + throw new Error( + '[Push SDK] - Error - sendNotification() - Either chatId or rules object is required to send a additional meta notification for video or spaces' + ); + } +} + +/** + * + * @param payloadOptions channel, recipient and type tp verify whether it is a simulate type + * @returns boolean + */ +async function checkSimulateNotification(payloadOptions: { + channelFound: boolean; + channelorAlias: string; + recipient: string | string[] | undefined; + type: NOTIFICATION_TYPE; + env: ENV | undefined; + senderType: 0 | 1; +}): Promise { + try { + const { channelFound, channelorAlias, recipient, type, env, senderType } = + payloadOptions || {}; + + // Video call notifications are not simulated + // If channel is found, then it is not a simulate type + if (senderType === 1 || channelFound) return false; + + // if no channel info found, check if channel address = recipient and notification type is targeted + const convertedRecipient = + typeof recipient == 'string' && recipient?.split(':').length == 3 + ? recipient.split(':')[2] + : recipient; + return ( + channelorAlias == convertedRecipient && + type == NOTIFICATION_TYPE.TARGETTED + ); + } catch (e) { + return true; + } +} + +export async function sendNotification(options: ISendNotificationInputOptions) { + try { + const { + /* + senderType = 0 for channel notification (default) + senderType = 1 for chat notification + */ + senderType = 0, + signer, + type, + identityType, + payload, + recipients, + channel, + graph, + ipfsHash, + env = ENV.PROD, + chatId, + rules, + pgpPrivateKey, + channelFound = true, + } = options || {}; + + validateOptions(options); + + if ( + payload && + payload.additionalMeta && + typeof payload.additionalMeta === 'object' && + !payload.additionalMeta.domain + ) { + payload.additionalMeta.domain = DEFAULT_DOMAIN; + } + const _channelAddress = await getCAIPAddress(env, channel, 'Channel'); + const channelCAIPDetails = getCAIPDetails(_channelAddress); + + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + const uuid = getUUID(); + const chainId = parseInt(channelCAIPDetails.networkId, 10); + + const API_BASE_URL = getAPIBaseUrls(env); + let COMMUNICATOR_CONTRACT = ''; + if (senderType === 0) { + const { EPNS_COMMUNICATOR_CONTRACT } = getConfig(env, channelCAIPDetails); + COMMUNICATOR_CONTRACT = EPNS_COMMUNICATOR_CONTRACT; + } + + const _recipients = await getRecipients({ + env, + notificationType: type, + channel: _channelAddress, + recipients, + secretType: payload?.sectype, + }); + + const notificationPayload = getPayloadForAPIInput(options, _recipients); + + const verificationProof = await getVerificationProof({ + senderType, + signer, + chainId, + identityType, + notificationType: type, + verifyingContract: COMMUNICATOR_CONTRACT, + payload: notificationPayload, + graph, + ipfsHash, + uuid, + // for the pgpv2 verfication proof + chatId: + rules?.access.data.chatId ?? // for backwards compatibilty with 'chatId' param + chatId, + pgpPrivateKey, + }); + + const identity = getPayloadIdentity({ + identityType, + payload: notificationPayload, + notificationType: type, + graph, + ipfsHash, + }); + + const source = (await checkSimulateNotification({ + channelFound: channelFound, + channelorAlias: options.channel, + recipient: options.recipients, + type: options.type, + env: options.env, + senderType: options.senderType as 0 | 1, + })) + ? SOURCE_TYPES.SIMULATE + : getSource(chainId, identityType, senderType); + + const apiPayload = { + verificationProof, + identity, + sender: + senderType === 1 && !isValidNFTCAIP(_channelAddress) + ? `${channelCAIPDetails?.blockchain}:${channelCAIPDetails?.address}` + : _channelAddress, + source, + /** note this recipient key has a different expectation from the BE API, see the funciton for more */ + recipient: await getRecipientFieldForAPIPayload({ + env, + notificationType: type, + recipients: recipients || '', + channel: _channelAddress, + }), + /* + - If 'rules' is not provided, check if 'chatId' is available. + - If 'chatId' is available, create a new 'rules' object for backwards compatibility. + - If neither 'rules' nor 'chatId' is available, do not include 'rules' in the payload. + */ + ...(rules || chatId + ? { + rules: rules ?? { + access: { + data: { chatId }, + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + }, + }, + } + : {}), + }; + + const requestURL = `${API_BASE_URL}/v1/payloads/`; + return await axiosPost(requestURL, apiPayload, { + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (err) { + console.error( + '[Push SDK] - Error - sendNotification() - ', + JSON.stringify(err) + ); + throw err; + } +} diff --git a/packages/d-node-notif/src/lib/progressHook.ts b/packages/d-node-notif/src/lib/progressHook.ts new file mode 100644 index 000000000..675106177 --- /dev/null +++ b/packages/d-node-notif/src/lib/progressHook.ts @@ -0,0 +1,264 @@ +import { ProgressHookType } from './types'; +type ProgressHookTypeFunction = (...args: any[]) => ProgressHookType; + +const PROGRESSHOOK: Record< + string, + ProgressHookType | ProgressHookTypeFunction +> = { + /** + * PUSH-CREATE PROGRESSHOOKS + */ + 'PUSH-CREATE-01': { + progressId: 'PUSH-CREATE-01', + progressTitle: 'Generating Secure Profile Signature', + progressInfo: + 'This step is only done for first time users and might take a few seconds. PGP keys are getting generated to provide you with secure yet seamless web3 communication', + level: 'INFO', + }, + /** + * @deprecated + */ + 'PUSH-CREATE-02': { + progressId: 'PUSH-CREATE-02', + progressTitle: 'Signing Generated Profile', + progressInfo: + 'This step is only done for first time users. Please sign the message to continue.', + level: 'INFO', + }, + 'PUSH-CREATE-03': { + progressId: 'PUSH-CREATE-03', + progressTitle: 'Encrypting Generated Profile', + progressInfo: 'Encrypting your keys. Please sign the message to continue.', + level: 'INFO', + }, + 'PUSH-CREATE-04': { + progressId: 'PUSH-CREATE-04', + progressTitle: 'Syncing Generated Profile', + progressInfo: + 'Please sign the message to continue. Great job! You are almost fully onboarded to Push Protocol!', + level: 'INFO', + }, + 'PUSH-CREATE-05': { + progressId: 'PUSH-CREATE-05', + progressTitle: 'Setup Complete', + progressInfo: '', + level: 'SUCCESS', + }, + /** + * PUSH-UPGRADE PROGRESSHOOKS + */ + /** + * @deprecated + */ + 'PUSH-UPGRADE-01': { + progressId: 'PUSH-UPGRADE-01', + progressTitle: 'Generating New Profile Signature', + progressInfo: + 'Trying to Upgrade Push Keys to latest version. Please sign the message to continue.', + level: 'INFO', + }, + 'PUSH-UPGRADE-02': { + progressId: 'PUSH-UPGRADE-02', + progressTitle: 'Decrypting Old Profile', + progressInfo: + 'Trying to Upgrade Push Keys to latest version. Please sign the message to continue.', + level: 'INFO', + }, + /** + * @deprecated + */ + 'PUSH-UPGRADE-03': { + progressId: 'PUSH-UPGRADE-03', + progressTitle: 'Generating Encrypted New Profile', + progressInfo: + 'Trying to Upgrade Push Keys to latest version. Encrypting Push Keys with latest version. Please sign the message to continue.', + level: 'INFO', + }, + /** + * @deprecated + */ + 'PUSH-UPGRADE-04': { + progressId: 'PUSH-UPGRADE-04', + progressTitle: 'Syncing New Profile', + progressInfo: + 'Please sign the message to continue. Upgraded keys are almost ready!', + level: 'INFO', + }, + 'PUSH-UPGRADE-05': { + progressId: 'PUSH-UPGRADE-05', + progressTitle: 'Upgrade Completed, Welcome to Push Protocol', + progressInfo: '', + level: 'SUCCESS', + }, + /** + * PUSH-DECRYPT PROGRESSHOOKS + */ + 'PUSH-DECRYPT-01': { + progressId: 'PUSH-DECRYPT-01', + progressTitle: 'Decrypting Profile', + progressInfo: 'Please sign the transaction to decrypt profile', + level: 'INFO', + }, + 'PUSH-DECRYPT-02': { + progressId: 'PUSH-DECRYPT-02', + progressTitle: 'Push Profile Unlocked', + progressInfo: 'Unlocking push profile', + level: 'SUCCESS', + }, + /** + * PUSH-AUTH-UPDATE PROGRESSHOOKS + */ + 'PUSH-AUTH-UPDATE-01': (pgpEncryptionVersion: string) => { + return { + progressId: 'PUSH-AUTH-UPDATE-01', + progressTitle: 'Generating New Profile Signature', + progressInfo: `Trying to Update Push Keys to ${pgpEncryptionVersion} version. Please sign the message to continue.`, + level: 'INFO', + }; + }, + 'PUSH-AUTH-UPDATE-02': (pgpEncryptionVersion: string) => { + return { + progressId: 'PUSH-AUTH-UPDATE-02', + progressTitle: 'Generating New Encrypted Profile', + progressInfo: `Encrypting Push Keys with ${pgpEncryptionVersion} version. Please sign the message to continue.`, + level: 'INFO', + }; + }, + 'PUSH-AUTH-UPDATE-03': { + progressId: 'PUSH-AUTH-UPDATE-03', + progressTitle: 'Syncing Updated Profile', + progressInfo: + 'Please sign the message to continue. Updated keys are almost ready!', + level: 'INFO', + }, + 'PUSH-AUTH-UPDATE-04': { + progressId: 'PUSH-AUTH-UPDATE-04', + progressTitle: 'Update Completed, Welcome to Push Protocol', + progressInfo: '', + level: 'SUCCESS', + }, + 'PUSH-AUTH-UPDATE-05': { + progressId: 'PUSH-AUTH-UPDATE-05', + progressTitle: 'Generating New Profile Signature', + progressInfo: `Trying to Update Push Profile creds. Please sign the message to continue.`, + level: 'INFO', + }, + 'PUSH-AUTH-UPDATE-06': { + progressId: 'PUSH-AUTH-UPDATE-06', + progressTitle: 'Generating New Profile Signature', + progressInfo: `Encrypting Push Keys with new creds. Please sign the message to continue.`, + level: 'INFO', + }, + /** + * PUSH-DECRYPT-AUTH PROGRESSHOOKS + */ + 'PUSH-DECRYPT-AUTH-01': { + progressId: 'PUSH-DECRYPT-AUTH-01', + progressTitle: 'Decrypting Profile Creds', + progressInfo: 'Please sign the transaction to decrypt profile creds', + level: 'INFO', + }, + 'PUSH-DECRYPT-AUTH-02': { + progressId: 'PUSH-DECRYPT-AUTH-02', + progressTitle: 'Push Profile Creds Unlocked', + progressInfo: 'Unlocking push profile creds', + level: 'SUCCESS', + }, + /** + * PUSH-PROFILE-UPDATE PROGRESSHOOKS + */ + 'PUSH-PROFILE-UPDATE-01': { + progressId: 'PUSH-PROFILE-UPDATE-01', + progressTitle: 'Syncing Updated Profile', + progressInfo: 'Steady lads, your profile is getting a new look!', + level: 'INFO', + }, + 'PUSH-PROFILE-UPDATE-02': { + progressId: 'PUSH-PROFILE-UPDATE-02', + progressTitle: 'Profile Update Completed, Welcome to Push Protocol', + progressInfo: '', + level: 'SUCCESS', + }, + /** + * PUSH_CHANNEL_CREATE PROGRESSHOOKS + */ + 'PUSH-CHANNEL-CREATE-01': { + progressId: 'PUSH-CHANNEL-CREATE-01', + progressTitle: 'Uploading data to IPFS', + progressInfo: 'The channel’s data is getting uploaded to IPFS', + level: 'INFO', + }, + 'PUSH-CHANNEL-CREATE-02': { + progressId: 'PUSH-CHANNEL-CREATE-02', + progressTitle: 'Approving PUSH tokens', + progressInfo: 'Gives approval to Push Core contract to spend 50 DAI', + level: 'INFO', + }, + 'PUSH-CHANNEL-CREATE-03': { + progressId: 'PUSH-CHANNEL-CREATE-03', + progressTitle: 'Channel is getting created', + progressInfo: 'Calls Push Core contract to create your channel', + level: 'INFO', + }, + 'PUSH-CHANNEL-CREATE-04': { + progressId: 'PUSH-CHANNEL-CREATE-04', + progressTitle: 'Channel creation is done, Welcome to Push Ecosystem', + progressInfo: 'Channel creation is completed', + level: 'SUCCESS', + }, + /** + * PUSH_CHANNEL_UPDATE PROGRESSHOOKS + */ + 'PUSH-CHANNEL-UPDATE-01': { + progressId: 'PUSH-CHANNEL-UPDATE-01', + progressTitle: 'Uploading new data to IPFS', + progressInfo: 'The channel’s new data is getting uploaded to IPFS', + level: 'INFO', + }, + 'PUSH-CHANNEL-UPDATE-02': { + progressId: 'PUSH-CHANNEL-UPDATE-02', + progressTitle: 'Approving PUSH tokens', + progressInfo: 'Gives approval to Push Core contract to spend 50 DAI', + level: 'INFO', + }, + 'PUSH-CHANNEL-UPDATE-03': { + progressId: 'PUSH-CHANNEL-UPDATE-03', + progressTitle: 'Channel is getting updated', + progressInfo: 'Calls Push Core contract to update your channel details', + level: 'INFO', + }, + 'PUSH-CHANNEL-UPDATE-04': { + progressId: 'PUSH-CHANNEL-UPDATE-04', + progressTitle: 'Channel is updated with new data', + progressInfo: 'Channel is successfully updated', + level: 'SUCCESS', + }, + /** + * PUSH-ERROR PROGRESSHOOKS + */ + 'PUSH-ERROR-00': (functionName: string, err: string) => { + return { + progressId: 'PUSH-ERROR-00', + progressTitle: 'Non Specific Error', + progressInfo: `[Push SDK] - API - Error - API ${functionName}() -: ${err}`, + level: 'ERROR', + }; + }, + 'PUSH-ERROR-01': (err: string) => { + return { + progressId: 'PUSH-ERROR-01', + progressTitle: 'Upgrade Profile Failed', + progressInfo: `[Push SDK] - API - Error - API decryptPgpKey() -: ${err}`, + level: 'WARN', + }; + }, + 'PUSH-ERROR-02': (name: string, err: string) => { + return { + progressId: 'PUSH-ERROR-02', + progressTitle: 'Transaction failed', + progressInfo: `[Push SDK] - Contract - Error - ${name} -: ${err}`, + level: 'ERROR', + }; + }, +}; +export default PROGRESSHOOK; diff --git a/packages/d-node-notif/src/lib/pushAPI/PushAPI.ts b/packages/d-node-notif/src/lib/pushAPI/PushAPI.ts new file mode 100644 index 000000000..42520478f --- /dev/null +++ b/packages/d-node-notif/src/lib/pushAPI/PushAPI.ts @@ -0,0 +1,281 @@ +import Constants, { ENV, PACKAGE_BUILD } from '../constants'; +import { SignerType, ProgressHookType } from '../types'; +import { InfoOptions, PushAPIInitializeProps } from './pushAPITypes'; +import * as PUSH_USER from '../user'; +import { getAccountAddress, getWallet } from '../chat/helpers'; +import { PushStream, StreamType } from '../pushstream/PushStream'; +import { Channel } from '../pushNotification/channel'; +import { Notification } from '../pushNotification/notification'; +import { + PushStreamInitializeProps, + STREAM, +} from '../pushstream/pushStreamTypes'; +import { ALPHA_FEATURE_CONFIG } from '../config'; +import { decryptPGPKey, isValidNFTCAIP, walletToPCAIP10 } from '../helpers'; +import { LRUCache } from 'lru-cache'; +import { cache } from '../helpers/cache'; +import { v4 as uuidv4 } from 'uuid'; + +export class PushAPI { + public signer?: SignerType; + private readMode: boolean; + private alpha: { feature: string[] }; + public account: string; + public chainWiseAccount: string; + public decryptedPgpPvtKey?: string; + public pgpPublicKey?: string; + public env: ENV; + private progressHook?: (progress: ProgressHookType) => void; + private cache: LRUCache; + + public stream!: PushStream; + // Notification + public channel!: Channel; + public notification!: Notification; + public uid: string; + // error object to maintain errors and warnings + public errors: { type: 'WARN' | 'ERROR'; message: string }[]; + + private constructor( + env: ENV, + account: string, + readMode: boolean, + alpha: { feature: string[] }, + decryptedPgpPvtKey?: string, + pgpPublicKey?: string, + signer?: SignerType, + progressHook?: (progress: ProgressHookType) => void, + initializationErrors?: { type: 'WARN' | 'ERROR'; message: string }[] + ) { + this.signer = signer; + this.readMode = readMode; + this.alpha = alpha; + this.env = env; + this.account = account; + this.chainWiseAccount = walletToPCAIP10(account); + this.decryptedPgpPvtKey = decryptedPgpPvtKey; + this.pgpPublicKey = pgpPublicKey; + this.progressHook = progressHook; + // Instantiate the notification classes + this.channel = new Channel(this.signer, this.env, this.account); + this.notification = new Notification(this.signer, this.env, this.account); + this.uid = uuidv4(); + this.cache = cache; + + this.errors = initializationErrors || []; + } + // Overloaded initialize method signatures + static async initialize( + signer?: SignerType | null, + options?: PushAPIInitializeProps + ): Promise; + static async initialize(options?: PushAPIInitializeProps): Promise; + + static async initialize(...args: any[]): Promise { + try { + let signer: SignerType | undefined; + let options: PushAPIInitializeProps | undefined; + let decryptedPGPPrivateKey: string | undefined; + + if (args.length === 1 && typeof args[0] === 'object') { + // This branch handles both the single options object and the single signer scenario. + if ('account' in args[0] && typeof args[0].account === 'string') { + // Single options object provided. + options = args[0]; + } else { + // Only signer provided. + [signer] = args; + } + } else if (args.length === 2) { + // Separate signer and options arguments provided. + [signer, options] = args; + } else { + // Handle other cases or throw an error. + throw new Error('Invalid arguments provided to initialize method.'); + } + + // Check for decryptedPGPPrivateKey in options, regardless of how options was assigned. + if ( + options && + 'decryptedPGPPrivateKey' in options && + typeof options.decryptedPGPPrivateKey === 'string' + ) { + decryptedPGPPrivateKey = options.decryptedPGPPrivateKey; + } + + if (!signer && !options?.account) { + throw new Error("Either 'signer' or 'account' must be provided."); + } + + // Determine readMode based on the presence of signer and decryptedPGPPrivateKey + let readMode = !signer && !decryptedPGPPrivateKey; + + // Default options + const defaultOptions: PushAPIInitializeProps = { + env: ENV.STAGING, + version: Constants.ENC_TYPE_V3, + autoUpgrade: true, + account: null, + }; + + // Settings object + // Default options are overwritten by the options passed in the initialize method + const settings = { + ...defaultOptions, + ...options, + version: options?.version || defaultOptions.version, + versionMeta: options?.versionMeta || defaultOptions.versionMeta, + autoUpgrade: + options?.autoUpgrade !== undefined + ? options?.autoUpgrade + : defaultOptions.autoUpgrade, + alpha: + options?.alpha && options.alpha.feature + ? options.alpha + : ALPHA_FEATURE_CONFIG[PACKAGE_BUILD], + }; + + const initializationErrors: { + type: 'WARN' | 'ERROR'; + message: string; + }[] = []; + + // Get account + // Derives account from signer if not provided + + let derivedAccount; + if (signer) { + derivedAccount = await getAccountAddress( + getWallet({ + account: settings.account as string | null, + signer: signer, + }) + ); + } else { + derivedAccount = options?.account; + } + + if (!derivedAccount) { + throw new Error('Account could not be derived.'); + } + + let pgpPublicKey: string | undefined; + + /** + * Decrypt PGP private key + * If user exists, decrypts the PGP private key + * If user does not exist, creates a new user and returns the decrypted PGP private key + */ + const user = await PUSH_USER.get({ + account: derivedAccount, + env: settings.env, + }); + + if (user && user.publicKey) { + pgpPublicKey = user.publicKey; + } + + if (!readMode) { + try { + if (user && user.encryptedPrivateKey) { + if (!decryptedPGPPrivateKey) { + decryptedPGPPrivateKey = await decryptPGPKey({ + encryptedPGPPrivateKey: user.encryptedPrivateKey, + signer: signer, + toUpgrade: settings.autoUpgrade, + additionalMeta: settings.versionMeta, + progressHook: settings.progressHook, + env: settings.env, + }); + } + } else { + const newUser = await PUSH_USER.create({ + env: settings.env, + account: derivedAccount, + signer, + version: settings.version, + additionalMeta: settings.versionMeta, + origin: settings.origin, + progressHook: settings.progressHook, + }); + decryptedPGPPrivateKey = newUser.decryptedPrivateKey as string; + pgpPublicKey = newUser.publicKey; + } + } catch (error) { + const decryptionError = + 'Error decrypting PGP private key ...swiching to Guest mode'; + initializationErrors.push({ + type: 'ERROR', + message: decryptionError, + }); + console.error(decryptionError); + if (isValidNFTCAIP(derivedAccount)) { + const nftDecryptionError = + 'NFT Account Detected. If this NFT was recently transferred to you, please ensure you have received the correct password from the previous owner. Alternatively, you can reinitialize for a fresh start. Please be aware that reinitialization will result in the loss of all previous account data.'; + + initializationErrors.push({ + type: 'WARN', + message: nftDecryptionError, + }); + console.warn(nftDecryptionError); + } + readMode = true; + } + } + // Initialize PushAPI instance + const api = new PushAPI( + settings.env as ENV, + derivedAccount, + readMode, + settings.alpha, + decryptedPGPPrivateKey, + pgpPublicKey, + signer, + settings.progressHook, + initializationErrors + ); + + return api; + } catch (error) { + console.error('Error initializing PushAPI:', error); + throw error; // or handle it more gracefully if desired + } + } + + async initStream( + listen: StreamType[], + options?: PushStreamInitializeProps + ): Promise { + if (this.stream) { + throw new Error('Stream is already initialized.'); + } + + this.stream = await PushStream.initialize( + this.account, + listen, + this.env, + this.decryptedPgpPvtKey, + this.progressHook, + this.signer, + options + ); + + return this.stream; + } + + async info(options?: InfoOptions) { + const accountToUse = options?.overrideAccount || this.account; + return await PUSH_USER.get({ + account: accountToUse, + env: this.env, + }); + } + + readmode(): boolean { + return this.readMode; + } + + static ensureSignerMessage(): string { + return 'Operation not allowed in read-only mode. Signer is required.'; + } +} diff --git a/packages/d-node-notif/src/lib/pushAPI/pushAPITypes.ts b/packages/d-node-notif/src/lib/pushAPI/pushAPITypes.ts new file mode 100644 index 000000000..b0df550b1 --- /dev/null +++ b/packages/d-node-notif/src/lib/pushAPI/pushAPITypes.ts @@ -0,0 +1,20 @@ +import Constants, { ENV } from '../constants'; +import { ProgressHookType } from '../types'; + +export interface PushAPIInitializeProps { + env?: ENV; + progressHook?: (progress: ProgressHookType) => void; + account?: string | null; + version?: typeof Constants.ENC_TYPE_V1 | typeof Constants.ENC_TYPE_V3; + versionMeta?: { NFTPGP_V1?: { password: string } }; + autoUpgrade?: boolean; + origin?: string; + alpha?: { + feature: string[]; + }; + decryptedPGPPrivateKey?: string | null; +} + +export interface InfoOptions { + overrideAccount?: string; +} diff --git a/packages/d-node-notif/src/lib/pushNotification/PushNotificationTypes.ts b/packages/d-node-notif/src/lib/pushNotification/PushNotificationTypes.ts new file mode 100644 index 000000000..dc3c6c20f --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/PushNotificationTypes.ts @@ -0,0 +1,163 @@ +import { ADDITIONAL_META_TYPE } from '../../lib/payloads/constants'; +import { GetAliasInfoOptionsType } from '../alias'; +import { NotifictaionType, ProgressHookType } from '../types'; + +export type SubscriptionOptions = { + account?: string; + page?: number; + limit?: number; + channel?: string; + raw?: boolean; +}; +export type ChannelInfoOptions = { + channel?: string; + page?: number; + limit?: number; + category?: number; + setting?: boolean; + raw?: boolean; +}; + +export type SubscribeUnsubscribeOptions = { + onSuccess?: () => void; + onError?: (err: Error) => void; + settings?: UserSetting[]; +}; + +export type UserSetting = { + enabled: boolean; + value?: number | { lower: number; upper: number }; +}; + +export type AliasOptions = Omit; + +export type AliasInfoOptions = { + raw?: boolean; + version?: number; +} + +export enum FeedType { + INBOX = 'INBOX', + SPAM = 'SPAM', +} + +export type FeedsOptions = { + account?: string; + //TODO: change it to string[] once we start supporting multiple channel + channels?: string[]; + page?: number; + limit?: number; + raw?: boolean; +}; + +export type ChannelSearchOptions = { + page?: number; + limit?: number; +}; + +// Types related to notification +export type INotification = { + title: string; + body: string; +}; + +export type IPayload = { + title?: string; + body?: string; + cta?: string; + embed?: string; + category?: number; + meta?: { + domain?: string; + type: `${ADDITIONAL_META_TYPE}+${number}`; + data: string; + }; +}; + +export type IConfig = { + expiry?: number; + silent?: boolean; + hidden?: boolean; +}; + +export type IAdvance = { + graph?: { + id: string; + counter: number; + }; + ipfs?: string; + minimal?: string; + chatid?: string; + pgpPrivateKey?: string; +}; + +export type NotificationOptions = { + notification: INotification; + payload?: IPayload; + config?: IConfig; + advanced?: IAdvance; + channel?: string; +}; + +export type CreateChannelOptions = { + name: string; + description: string; + icon: string; + url: string; + alias?: string; + progressHook?: (progress: ProgressHookType) => void; +}; + +export type NotificationSetting = { + type: number; + default: number | { upper: number; lower: number }; + description: string; + data?: { + upper: number; + lower: number; + enabled?: boolean; + ticker?: number; + }; +}; + +export type NotificationSettings = NotificationSetting[]; + +export type ChannelFeedsOptions = { + page?: number; + limit?: number; + raw?: boolean; + filter?: NotifictaionType; +}; +export type ChannelOptions = { + raw: boolean; +}; + + + +export enum ChannelListType { + ALL = 'all', + VERIFIED = 'verified', + UNVERIFIED = 'unverified', +} + +export enum ChannelListSortType { + SUBSCRIBER = 'subscribers', +} + + + +export type ChannelListOptions = { + page?: number; + limit?: number; + sort?: ChannelListSortType; + order?: ChannelListOrderType; +}; + + + + + +export enum ChannelListOrderType { + ASCENDING = 'asc', + DESCENDING = 'desc', +}; \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/pushNotification/alias.ts b/packages/d-node-notif/src/lib/pushNotification/alias.ts new file mode 100644 index 000000000..11df7472c --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/alias.ts @@ -0,0 +1,104 @@ +import { ENV } from '../constants'; +import { AliasInfoOptions, AliasOptions } from './PushNotificationTypes'; +import { SignerType } from '../types'; +import { validateCAIP } from '../helpers'; +import CONFIG, * as config from '../config'; +import * as PUSH_ALIAS from '../alias'; +import { PushNotificationBaseClass } from './pushNotificationBase'; + + +export class Alias extends PushNotificationBaseClass { + constructor(signer?: SignerType, env?: ENV, account?: string) { + super(signer, env, account); + } + + /** + * @description - fetches alias information + * @param {AliasOptions} options - options related to alias + * @returns Alias details + */ + info = async (options: AliasOptions) => { + try { + return await PUSH_ALIAS.getAliasInfo({ ...options, env: this.env }); + } catch (error) { + throw new Error(`Push SDK Error: API : alias::info : ${error}`); + } + }; + + /** + * @description adds an alias to the channel + * @param {string} alias - alias address in caip to be added + * @param {AliasInfoOptions} options - options related to alias + * @returns the transaction hash if the transaction is successfull + */ + initiate = async (alias: string, options?: AliasInfoOptions): Promise => { + try { + this.checkSignerObjectExists(); + 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'); + } + const commAddress = CONFIG[this.env!][caip].EPNS_COMMUNICATOR_CONTRACT; + + const commContract = this.createContractInstance( + commAddress, + config.ABIS.COMM, + config.VIEM_CONFIG[this.env!][caip].NETWORK + ); + + const addAliasRes = await this.initiateAddAlias(commContract, alias); + let resp: { [key: string]: any } = { tx: addAliasRes } + if (options?.raw) { + resp = { + ...resp, + "raw": { + "initiateVerificationProof": addAliasRes + } + } + } + return resp; + } catch (error) { + throw new Error(`Push SDK Error: Contract : alias::add : ${error}`); + } + }; + + /** + * @description verifies an alias address of a channel + * @param {string} channelAddress - channelAddress to be verified + * @param {AliasInfoOptions} options - options related to alias + * @returns the transaction hash if the transaction is successfull + */ + verify = async (channelAddress: string, options?: AliasInfoOptions) => { + try { + this.checkSignerObjectExists(); + 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'); + } + const commAddress = CONFIG[this.env!][caip].EPNS_COMMUNICATOR_CONTRACT; + + const commContract = this.createContractInstance( + commAddress, + config.ABIS.COMM, + config.VIEM_CONFIG[this.env!][caip].NETWORK + ); + const { verifyAliasRes, channelInfo } = await this.verifyAlias(commContract, channelAddress); + let resp: { [key: string]: any } = { tx: verifyAliasRes } + if (options?.raw) { + resp = { + ...resp, + "raw": { + "initiateVerificationProof": channelInfo.initiate_verification_proof, + "verifyVerificationProof": verifyAliasRes + } + } + } + return resp; + } catch (error) { + throw new Error(`Push SDK Error: Contract : alias::verify : ${error}`); + } + }; +} diff --git a/packages/d-node-notif/src/lib/pushNotification/channel.ts b/packages/d-node-notif/src/lib/pushNotification/channel.ts new file mode 100644 index 000000000..cc2a48f8f --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/channel.ts @@ -0,0 +1,460 @@ +import Constants, { ENV } from '../constants'; + +import * as viem from 'viem'; +import * as PUSH_CHANNEL from '../channels'; +import * as config from '../config'; +import { + getCAIPDetails, + getFallbackETHCAIPAddress, + validateCAIP, +} from '../helpers'; +import * as PUSH_PAYLOAD from '../payloads'; +import PROGRESSHOOK from '../progressHook'; +import { + ProgressHookType, + ProgressHookTypeFunction, + SignerType, +} from '../types'; +import { + ChannelFeedsOptions, + ChannelInfoOptions, + ChannelListOptions, + ChannelListOrderType, + ChannelListSortType, + ChannelOptions, + ChannelSearchOptions, + CreateChannelOptions, + NotificationOptions, + NotificationSettings, +} from './PushNotificationTypes'; + +import { Alias } from './alias'; +import { Delegate } from './delegate'; +import { PushNotificationBaseClass } from './pushNotificationBase'; + +export class Channel extends PushNotificationBaseClass { + public delegate!: Delegate; + public alias!: Alias; + constructor(signer?: SignerType, env?: ENV, account?: string) { + super(signer, env, account); + this.delegate = new Delegate(signer, env, account); + this.alias = new Alias(signer, env, account); + } + + /** + * @description - returns information about a channel + * @param {string} [options.channel] - channel address in caip, defaults to eth caip address + * @returns information about the channel if it exists + */ + info = async (channel?: string, options?: ChannelOptions) => { + try { + const { raw = true } = options || {}; + this.checkUserAddressExists(channel); + channel = channel ?? getFallbackETHCAIPAddress(this.env!, this.account!); + return await PUSH_CHANNEL.getChannel({ + channel: channel as string, + env: this.env, + raw: raw, + }); + } catch (error) { + throw new Error(`Push SDK Error: API : channel::info : ${error}`); + } + }; + + /** + * @description - returns relevant information as per the query that was passed + * @param {string} query - search query + * @param {number} [options.page] - page number. default is set to Constants.PAGINATION.INITIAL_PAGE + * @param {number} [options.limit] - number of feeds per page. default is set to Constants.PAGINATION.LIMIT + * @returns Array of results relevant to the serach query + */ + search = async (query: string, options?: ChannelSearchOptions) => { + try { + const { + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + } = options || {}; + return await PUSH_CHANNEL.search({ + query: query, + page: page, + limit: limit, + env: this.env, + }); + } catch (error) { + throw new Error(`Push SDK Error: API : channel::search : ${error}`); + } + }; + /** + * @description - Get subscribers of a channell + * @param {string} [options.channel] - channel in caip. defaults to account from signer with eth caip + * @returns array of subscribers + */ + subscribers = async (options?: ChannelInfoOptions) => { + try { + let channel = options?.channel + ? options.channel + : this.account + ? getFallbackETHCAIPAddress(this.env!, this.account!) + : null; + this.checkUserAddressExists(channel!); + channel = validateCAIP(channel!) + ? channel + : getFallbackETHCAIPAddress(this.env!, channel!); + if (options && options.page) { + return await PUSH_CHANNEL.getSubscribers({ + channel: channel!, + env: this.env, + page: options.page, + limit: options.limit ?? 10, + setting: options.setting ?? false, + category: options.category, + raw: options.raw, + }); + } else { + /** @dev - Fallback to deprecated method when page is not provided ( to ensure backward compatibility ) */ + /** @notice - This will be removed in V2 Publish */ + return await PUSH_CHANNEL._getSubscribers({ + channel: channel!, + env: this.env, + }); + } + } catch (error) { + throw new Error(`Push SDK Error: API : channel::subscribers : ${error}`); + } + }; + /** + * + * @param {string[]} recipients - Array of recipients. `['0x1'] -> TARGET`, `['0x1, 0x2'] -> SUBSET`, `['*'] -> BROADCAST` + * @param {object} options - Notification options + * @returns + */ + send = async (recipients: string[], options: NotificationOptions) => { + try { + this.checkSignerObjectExists(); + const channelInfo = await this.getChannelOrAliasInfo( + options.channel! ?? this.account + ); + + const lowLevelPayload = this.generateNotificationLowLevelPayload({ + signer: this.signer!, + env: this.env!, + recipients: recipients, + options: options, + channel: options.channel ?? this.account, + channelInfo: channelInfo, + }); + return await PUSH_PAYLOAD.sendNotification(lowLevelPayload); + } catch (error) { + throw new Error(`Push SDK Error: API : channel::send : ${error}`); + } + }; + + create = async (options: CreateChannelOptions) => { + const { + name, + description, + url, + icon, + alias = null, + progressHook, + } = options || {}; + try { + // create push token instance + let aliasInfo; + // validate all the parameters and length + this.validateChannelParameters(options); + // check for PUSH balance + const pushTokenContract = await this.createContractInstance( + config.TOKEN[this.env!], + config.ABIS.TOKEN, + config.TOKEN_VIEM_NETWORK_MAP[this.env!] + ); + const balance = await this.fetchBalance(pushTokenContract, this.account!); + const fees = viem.parseUnits( + config.MIN_TOKEN_BALANCE[this.env!].toString(), + 18 + ); + if (fees > balance) { + throw new Error('Insufficient PUSH balance'); + } + // if alias is passed, check for the caip + if (alias) { + if (!validateCAIP(alias)) { + throw new Error('Invalid alias CAIP'); + } + const aliasDetails = getCAIPDetails(alias); + aliasInfo = { + [`${aliasDetails?.blockchain}:${aliasDetails?.networkId}`]: + aliasDetails?.address, + }; + } + // construct channel identity + progressHook?.(PROGRESSHOOK['PUSH-CREATE-01'] as ProgressHookType); + const input = { + name: name, + info: description, + url: url, + icon: icon, + aliasDetails: aliasInfo ?? {}, + }; + const cid = await this.uploadToIPFSViaPushNode(JSON.stringify(input)); + const allowanceAmount = await this.fetchAllownace( + pushTokenContract, + this.account!, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT + ); + if (!(allowanceAmount >= fees)) { + progressHook?.(PROGRESSHOOK['PUSH-CREATE-02'] as ProgressHookType); + const approvalRes = await this.approveToken( + pushTokenContract, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT, + fees + ); + if (!approvalRes) { + throw new Error('Something went wrong while approving the token'); + } + } + // generate the contract parameters + const channelType = config.CHANNEL_TYPE['GENERAL']; + const identity = '1+' + cid; + const identityBytes = viem.stringToBytes(identity); + // call contract + progressHook?.(PROGRESSHOOK['PUSH-CREATE-03'] as ProgressHookType); + const createChannelRes = await this.createChannel( + this.coreContract, + channelType, + identityBytes, + fees + ); + progressHook?.(PROGRESSHOOK['PUSH-CREATE-04'] as ProgressHookType); + return { transactionHash: createChannelRes }; + } catch (error) { + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-02' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook('Create Channel', error)); + throw new Error( + `Push SDK Error: Contract : createChannelWithPUSH : ${error}` + ); + } + }; + + update = async (options: CreateChannelOptions) => { + const { + name, + description, + url, + icon, + alias = null, + progressHook, + } = options || {}; + try { + // create push token instance + let aliasInfo; + // validate all the parameters and length + this.validateChannelParameters(options); + // check for PUSH balance + const pushTokenContract = await this.createContractInstance( + config.TOKEN[this.env!], + config.ABIS.TOKEN, + config.TOKEN_VIEM_NETWORK_MAP[this.env!] + ); + const balance = await this.fetchBalance(pushTokenContract, this.account!); + // get counter + const counter = await this.fetchUpdateCounter( + this.coreContract, + this.account! + ); + const fees = viem.parseUnits( + config.MIN_TOKEN_BALANCE[this.env!].toString(), + 18 + ); + const totalFees = fees * counter; + if (totalFees > balance) { + throw new Error('Insufficient PUSH balance'); + } + // if alias is passed, check for the caip + if (alias) { + if (!validateCAIP(alias)) { + throw new Error('Invalid alias CAIP'); + } + const aliasDetails = getCAIPDetails(alias); + aliasInfo = { + [`${aliasDetails?.blockchain}:${aliasDetails?.networkId}`]: + aliasDetails?.address, + }; + } + // construct channel identity + progressHook?.(PROGRESSHOOK['PUSH-UPDATE-01'] as ProgressHookType); + const input = { + name: name, + info: description, + url: url, + icon: icon, + aliasDetails: aliasInfo ?? {}, + }; + const cid = await this.uploadToIPFSViaPushNode(JSON.stringify(input)); + // approve the tokens to core contract + const allowanceAmount = await this.fetchAllownace( + pushTokenContract, + this.account!, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT + ); + // if allowance is not greater than the fees, dont call approval again + if (!(allowanceAmount >= totalFees)) { + progressHook?.(PROGRESSHOOK['PUSH-UPDATE-02'] as ProgressHookType); + const approvalRes = await this.approveToken( + pushTokenContract, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT, + totalFees + ); + if (!approvalRes) { + throw new Error('Something went wrong while approving the token'); + } + } + // generate the contract parameters + const identity = '1+' + cid; + const identityBytes = viem.stringToBytes(identity); + // call contract + progressHook?.(PROGRESSHOOK['PUSH-UPDATE-03'] as ProgressHookType); + const updateChannelRes = await this.updateChannel( + this.coreContract, + this.account!, + identityBytes, + totalFees + ); + progressHook?.(PROGRESSHOOK['PUSH-UPDATE-04'] as ProgressHookType); + return { transactionHash: updateChannelRes }; + } catch (error) { + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-02' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook('Update Channel', error)); + throw new Error(`Push SDK Error: Contract channel::update : ${error}`); + } + }; + /** + * @description verifies a channel + * @param {string} channelToBeVerified - address of the channel to be verified + * @returns the transaction hash if the transaction is successful + */ + verify = async (channelToBeVerified: string) => { + try { + this.checkSignerObjectExists(); + if (validateCAIP(channelToBeVerified)) { + channelToBeVerified = channelToBeVerified.split(':')[2]; + } + // checks if it is a valid address + if (!viem.isAddress(channelToBeVerified)) { + throw new Error('Invalid channel address'); + } + const channelDetails = await this.info(this.account); + if (channelDetails?.verified_status == 0) { + throw new Error('Only verified channel can verify other channel'); + } + // if valid, continue with it + const res = await this.verifyChannel( + this.coreContract, + channelToBeVerified + ); + if (!res) { + throw new Error('Something went wrong while verifying the channel'); + } + return { transactionHash: res }; + } catch (error) { + throw new Error(`Push SDK Error: Contract channel::verify : ${error}`); + } + }; + + setting = async (configuration: NotificationSettings) => { + try { + this.checkSignerObjectExists(); + // check for PUSH balance + const pushTokenContract = await this.createContractInstance( + config.TOKEN[this.env!], + config.ABIS.TOKEN, + config.TOKEN_VIEM_NETWORK_MAP[this.env!] + ); + const balance = await this.fetchBalance(pushTokenContract, this.account!); + const fees = viem.parseUnits( + config.MIN_TOKEN_BALANCE[this.env!].toString(), + 18 + ); + // get counter + const counter = await this.fetchUpdateCounter( + this.coreContract, + this.account! + ); + const totalFees = fees * counter; + if (totalFees > balance) { + throw new Error('Insufficient PUSH balance'); + } + const allowanceAmount = await this.fetchAllownace( + pushTokenContract, + this.account!, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT + ); + // if allowance is not greater than the fees, dont call approval again + if (!(allowanceAmount >= totalFees)) { + const approveRes = await this.approveToken( + pushTokenContract, + config.CORE_CONFIG[this.env!].EPNS_CORE_CONTRACT, + totalFees + ); + if (!approveRes) { + throw new Error('Something went wrong while approving your token'); + } + } + const { setting, description } = this.getMinimalSetting(configuration); + const createSettingsRes = await this.createChanelSettings( + this.coreContract, + configuration.length, + setting, + description, + fees + ); + return { transactionHash: createSettingsRes }; + } catch (error) { + throw new Error(`Push SDK Error: Contract : channel::setting : ${error}`); + } + }; + + notifications = async (account: string, options?: ChannelFeedsOptions) => { + try { + const { page, limit, filter = null, raw = true } = options || {}; + return await PUSH_CHANNEL.getChannelNotifications({ + channel: account as string, + env: this.env, + filter, + raw, + page, + limit, + }); + } catch (error) { + throw new Error( + `Push SDK Error: Contract : channel::notifications : ${error}` + ); + } + }; + + list = async (options?: ChannelListOptions) => { + try { + const { + page, + limit, + sort = ChannelListSortType.SUBSCRIBER, + order = ChannelListOrderType.DESCENDING, + } = options || {}; + + return await PUSH_CHANNEL.getChannels({ + env: this.env, + page, + limit, + sort, + order, + }); + } catch (error) { + throw new Error(`Push SDK Error: Contract : channel::list : ${error}`); + } + }; +} diff --git a/packages/d-node-notif/src/lib/pushNotification/delegate.ts b/packages/d-node-notif/src/lib/pushNotification/delegate.ts new file mode 100644 index 000000000..95fbb3abd --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/delegate.ts @@ -0,0 +1,106 @@ +import { ENV } from '../constants'; +import { SignerType } from '../types'; +import { ChannelInfoOptions } from './PushNotificationTypes'; +import CONFIG, * as config from '../config'; +import * as PUSH_CHANNEL from '../channels'; +import { validateCAIP, getFallbackETHCAIPAddress } from '../helpers'; +import { PushNotificationBaseClass } from './pushNotificationBase'; + +export class Delegate extends PushNotificationBaseClass { + constructor(signer?: SignerType, env?: ENV, account?: string) { + super(signer, env, account); + } + + /** + * @description - Get delegates of a channell + * @param {string} [options.channel] - channel in caip. defaults to account from signer with eth caip + * @returns array of delegates + */ + get = async (options?: ChannelInfoOptions) => { + try { + // const { + // channel = this.account + // ? getFallbackETHCAIPAddress(this.env!, this.account!) + // : null, + // } = options || {}; + let channel = options?.channel + ? options.channel + : this.account + ? getFallbackETHCAIPAddress(this.env!, this.account!) + : null; + this.checkUserAddressExists(channel!); + channel = validateCAIP(channel!) + ? channel + : getFallbackETHCAIPAddress(this.env!, channel!); + this.checkUserAddressExists(channel!); + return await PUSH_CHANNEL.getDelegates({ + channel: channel!, + env: this.env, + }); + } catch (error) { + throw new Error(`Push SDK Error: API : delegate::get : ${error}`); + } + }; + + /** + * @description adds a delegate + * @param {string} delegate - delegate address in caip to be added + * @returns the transaction hash if the transaction is successfull + */ + add = async (delegate: string) => { + try { + this.checkSignerObjectExists(); + if (validateCAIP(delegate)) { + delegate = this.getAddressFromCaip(delegate); + } + 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'); + } + const commAddress = CONFIG[this.env!][caip].EPNS_COMMUNICATOR_CONTRACT; + + const commContract = this.createContractInstance( + commAddress, + config.ABIS.COMM, + config.VIEM_CONFIG[this.env!][caip].NETWORK + ); + const addDelegateRes = await this.addDelegator(commContract, delegate); + return { transactionHash: addDelegateRes }; + } catch (error) { + throw new Error(`Push SDK Error: Contract : delegate::add : ${error}`); + } + }; + + /** + * @description removes a delegate + * @param {string} delegate - caip address of the delegate to be removed + * @returns the transaction hash if the transaction is successfull + */ + remove = async (delegate: string) => { + try { + this.checkSignerObjectExists(); + if (validateCAIP(delegate)) { + delegate = this.getAddressFromCaip(delegate); + } + 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'); + } + const commAddress = CONFIG[this.env!][caip].EPNS_COMMUNICATOR_CONTRACT; + const commContract = this.createContractInstance( + commAddress, + config.ABIS.COMM, + config.VIEM_CONFIG[this.env!][caip].NETWORK + ); + const removeDelegateRes = await this.removeDelegator( + commContract, + delegate + ); + return { transactionHash: removeDelegateRes }; + } catch (error) { + throw new Error(`Push SDK Error: Contract : delegate::remove : ${error}`); + } + }; +} diff --git a/packages/d-node-notif/src/lib/pushNotification/notification.ts b/packages/d-node-notif/src/lib/pushNotification/notification.ts new file mode 100644 index 000000000..896ba3ee7 --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/notification.ts @@ -0,0 +1,235 @@ +import Constants, { ENV } from '../constants'; +import { SignerType } from '../types'; +import { + SubscribeUnsubscribeOptions, + SubscriptionOptions, + FeedType, + FeedsOptions, +} from './PushNotificationTypes'; +import * as PUSH_USER from '../user'; +import * as PUSH_CHANNEL from '../channels'; + +import { + getCAIPDetails, + getCAIPWithChainId, + validateCAIP, + getFallbackETHCAIPAddress, + pCAIP10ToWallet, +} from '../helpers'; + +import { PushNotificationBaseClass } from './pushNotificationBase'; +// ERROR CONSTANTS +const ERROR_CHANNEL_NEEDED = 'Channel is needed'; +const ERROR_INVALID_CAIP = 'Invalid CAIP format'; + +export const FEED_MAP = { + INBOX: false, + SPAM: true, +}; +export class Notification extends PushNotificationBaseClass { + constructor(signer?: SignerType, env?: ENV, account?: string) { + super(signer, env, account); + } + + /** + * @description - Fetches feeds and spam feeds for a specific user + * @param {enums} spam - indicates if its a spam or not. `INBOX` for non-spam and `SPAM` for spam. default `INBOX` + * @param {string} [options.user] - user address, defaults to address from signer + * @param {number} [options.page] - page number. default is set to Constants.PAGINATION.INITIAL_PAGE + * @param {number} [options.limit] - number of feeds per page. default is set to Constants.PAGINATION.LIMIT + * @param {boolean} [options.raw] - indicates if the response should be raw or formatted. defaults is set to false + * @returns feeds for a specific address + */ + list = async ( + spam: `${FeedType}` = FeedType.INBOX, + options?: FeedsOptions + ) => { + const { + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + channels = [], + raw = false, + } = options || {}; + try { + let account: string | null; + if (options?.account) { + if (this.isValidPCaip(options.account)) { + account = pCAIP10ToWallet(options.account); + } else { + account = options.account; + } + } else if (this.account) { + account = getFallbackETHCAIPAddress(this.env!, this.account!); + } + // guest mode and valid address check + this.checkUserAddressExists(account!); + const nonCaipAccount = this.getAddressFromCaip(account!); + if (channels.length == 0) { + // else return the response + return await PUSH_USER.getFeeds({ + user: nonCaipAccount!, + page: page, + limit: limit, + spam: FEED_MAP[spam], + raw: raw, + env: this.env, + }); + } else { + const promises = channels.map(async (channel) => { + return await PUSH_USER.getFeedsPerChannel({ + user: nonCaipAccount!, + page: page, + limit: limit, + spam: FEED_MAP[spam], + raw: raw, + env: this.env, + channels: [channel], + }); + }); + + const results = await Promise.all(promises); + const feedRes = results.flat(); + return feedRes; + } + } catch (error) { + throw new Error(`Push SDK Error: API : notifcaiton::list : ${error}`); + } + }; + + subscriptions = async (options?: SubscriptionOptions) => { + try { + const { + // TODO: to be used once pagination is implemeted at API level + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + channel = null, + raw, + } = options || {}; + let account: string | null; + if (options?.account) { + if (this.isValidPCaip(options.account)) { + account = pCAIP10ToWallet(options.account); + } else { + account = options.account; + } + } else if (this.account) { + account = getFallbackETHCAIPAddress(this.env!, this.account!); + } + this.checkUserAddressExists(account!); + return await PUSH_USER.getSubscriptions({ + user: account!, + env: this.env, + channel: channel, + raw, + }); + } catch (error) { + throw new Error( + `Push SDK Error: API : notifcaiton::subscriptions : ${error}` + ); + } + }; + /** + * Subscribes a user to a channel + * @param {string} channel - channel address in caip format + * @param {function} [options.onSuccess] - callback function when a user successfully subscribes to a channel + * @param {function} [options.onError] - callback function incase a user was not able to subscribe to a channel + * @returns Subscribe status object + */ + subscribe = async ( + channel: string, + options?: SubscribeUnsubscribeOptions + ) => { + try { + const { onSuccess, onError, settings } = options || {}; + // Vaidatiions + // validates if signer object is present + this.checkSignerObjectExists(); + // validates if the user address exists + this.checkUserAddressExists(); + // validates if channel exists + if (!channel && channel != '') { + throw new Error(ERROR_CHANNEL_NEEDED); + } + // convert normal partial caip to wallet + if (this.isValidPCaip(channel)) { + channel = pCAIP10ToWallet(channel); + } + // validates if caip is correct + if (!validateCAIP(channel)) { + channel = getFallbackETHCAIPAddress(this.env!, channel); + } + // get channel caip + const caipDetail = getCAIPDetails(channel); + // based on the caip, construct the user caip + const userAddressInCaip = getCAIPWithChainId( + this.account!, + parseInt(caipDetail?.networkId as string) + ); + // convert the setting to minimal version + const minimalSetting = this.getMinimalUserSetting(settings!); + return await PUSH_CHANNEL.subscribeV2({ + signer: this.signer!, + channelAddress: channel, + userAddress: userAddressInCaip, + env: this.env, + settings: minimalSetting ?? '', + onSuccess: onSuccess, + onError: onError, + }); + } catch (error) { + throw new Error( + `Push SDK Error: API : notifcaiton::subscribe : ${error}` + ); + } + }; + + /** + * Unsubscribes a user to a channel + * @param {string} channel - channel address in caip format + * @param {function} [options.onSuccess] - callback function when a user successfully unsubscribes to a channel + * @param {function} [options.onError] - callback function incase a user was not able to unsubscribe to a channel + * @returns Unsubscribe status object + */ + unsubscribe = async ( + channel: string, + options?: SubscribeUnsubscribeOptions + ) => { + try { + const { onSuccess, onError } = options || {}; + // Vaidatiions + // validates if the user address exists + this.checkUserAddressExists(); + // validates if signer object is present + this.checkSignerObjectExists(); + // validates if channel exists + if (!channel && channel != '') { + return new Error(ERROR_CHANNEL_NEEDED); + } + // covert partial caip to normal wallet + if (this.isValidPCaip(channel)) { + channel = pCAIP10ToWallet(channel); + } + // validates if caip is correct + if (!validateCAIP(channel)) { + channel = getFallbackETHCAIPAddress(this.env!, channel); + } + const caipDetail = getCAIPDetails(channel); + const userAddressInCaip = getCAIPWithChainId( + this.account!, + parseInt(caipDetail?.networkId as string) + ); + return await PUSH_CHANNEL.unsubscribeV2({ + signer: this.signer!, + channelAddress: channel, + userAddress: userAddressInCaip, + env: this.env, + onSuccess: onSuccess, + onError: onError, + }); + } catch (error) { + throw new Error( + `Push SDK Error: API : notifcaiton::unsubscribe : ${error}` + ); + } + }; +} diff --git a/packages/d-node-notif/src/lib/pushNotification/pushNotificationBase.ts b/packages/d-node-notif/src/lib/pushNotification/pushNotificationBase.ts new file mode 100644 index 000000000..9953abbde --- /dev/null +++ b/packages/d-node-notif/src/lib/pushNotification/pushNotificationBase.ts @@ -0,0 +1,944 @@ +import { ENV } from '../constants'; +import { SignerType, ISendNotificationInputOptions } from '../types'; +import { + NotificationOptions, + CreateChannelOptions, + NotificationSettings, + UserSetting, + AliasInfoOptions, +} from './PushNotificationTypes'; +import * as config from '../config'; +import { getAccountAddress } from '../chat/helpers'; +import { IDENTITY_TYPE, NOTIFICATION_TYPE } from '../payloads/constants'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { ethers, Signer as EthersSigner } from 'ethers'; +import { + createPublicClient, + http, + getContract, + WalletClient, + Chain, + toHex, +} from 'viem'; +import * as PUSH_CHANNEL from '../channels'; +import { + CAIPDetailsType, + Signer, + getAPIBaseUrls, + getCAIPDetails, + getFallbackETHCAIPAddress, + validateCAIP, +} from '../helpers'; +import { axiosGet, axiosPost } from '../utils/axiosUtil'; +import { PushAPI } from '../pushAPI/PushAPI'; +import { channel } from 'diagnostics_channel'; +import * as viem from 'viem'; + +// ERROR CONSTANTS +const ERROR_ACCOUNT_NEEDED = 'Account is required'; +const ERROR_SIGNER_NEEDED = 'Signer object is required'; + +const BROADCAST_TYPE = '*'; +const LENGTH_UPPER_LIMIT = 125; +const LENGTH_LOWER_LIMTI = 1; +const SETTING_DELIMITER = '-'; +const SETTING_SEPARATOR = '+'; +const RANGE_TYPE = 3; +const SLIDER_TYPE = 2; +const BOOLEAN_TYPE = 1; +const DEFAULT_ENABLE_VALUE = '1'; +const DEFAULT_TICKER_VALUE = '1'; + +export const FEED_MAP = { + INBOX: false, + SPAM: true, +}; +export class PushNotificationBaseClass { + protected signer: SignerType | undefined; + protected account: string | undefined; + protected env: ENV | undefined; + protected guestMode: boolean; + protected coreContract: any; + + constructor(signer?: SignerType, env?: ENV, account?: string) { + this.signer = signer; + this.env = env; + this.guestMode = !!(account && signer); + this.account = account; + this.initializeCoreContract({ signer: this.signer, env: this.env }); + } + + private async initializeCoreContract(options?: { + signer?: SignerType; + env?: ENV; + }) { + const { env = ENV.STAGING, signer = null } = options || {}; + // Derives account from signer if not provided + let derivedAccount; + let coreContract; + if (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(), + }); + coreContract = getContract({ + abi: config.ABIS.CORE, + address: config.CORE_CONFIG[env].EPNS_CORE_CONTRACT as `0x${string}`, + client: { + public: client, + wallet: 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 + ); + } + } + + // Initialize PushNotifications instance + this.coreContract = coreContract; + } + + // check if addresses is supplied either by user or derived from signer object or if its guest mode + protected checkUserAddressExists(user?: string) { + if (!user && !this.account && !this.guestMode) + throw new Error(ERROR_ACCOUNT_NEEDED); + return true; + } + + // checks if the signer object is supplied + protected checkSignerObjectExists() { + if (!this.signer) throw new Error(PushAPI.ensureSignerMessage()); + return true; + } + + // get type of notification from recipient + protected getNotificationType( + recipient: string[], + channel: string + ): { recipient: string[] | string; type: number } { + if (recipient.length == 1) { + if (recipient[0] == BROADCAST_TYPE) { + return { recipient: channel, type: NOTIFICATION_TYPE['BROADCAST'] }; + } else { + return { + recipient: recipient[0], + type: NOTIFICATION_TYPE['TARGETTED'], + }; + } + } + return { recipient, type: NOTIFICATION_TYPE['SUBSET'] }; + } + + // get identity type for lowlevel call + protected generateNotificationLowLevelPayload({ + signer, + env, + recipients, + options, + channel, + channelInfo, + }: { + signer: SignerType; + env: ENV; + recipients: string[]; + options: NotificationOptions; + channel?: string; + channelInfo: any | null; + }): ISendNotificationInputOptions { + if (!channel) { + channel = `${this.account}`; + } + const notificationType = this.getNotificationType(recipients, channel); + const identityType = IDENTITY_TYPE.DIRECT_PAYLOAD; + // fetch the minimal version based on conifg that was passed + let index = ''; + + const settings = + channelInfo && channelInfo.channel_settings + ? JSON.parse(channelInfo.channel_settings) + : null; + + const channelFound = channelInfo ? true : false; + + if (options.payload?.category && settings) { + if (settings[options.payload.category - 1].type == SLIDER_TYPE) { + index = + options.payload.category + + SETTING_DELIMITER + + SLIDER_TYPE + + SETTING_DELIMITER + + settings[options.payload.category - 1].default; + } + if (settings[options.payload.category - 1].type == BOOLEAN_TYPE) { + index = options.payload.category + SETTING_DELIMITER + BOOLEAN_TYPE; + } + if (settings[options.payload.category - 1].type == RANGE_TYPE) { + index = + options.payload.category + + SETTING_DELIMITER + + RANGE_TYPE + + SETTING_DELIMITER + + settings[options.payload.category - 1].default.lower; + } + } + const notificationPayload: ISendNotificationInputOptions = { + signer: signer, + channel: channel, + type: notificationType.type, + identityType: identityType, + notification: options.notification, + payload: { + title: options.payload?.title ?? options.notification.title, + body: options.payload?.body ?? options.notification.body, + cta: options.payload?.cta ?? '', + img: options.payload?.embed ?? '', + hidden: options.config?.hidden, + etime: options.config?.expiry, + silent: options.config?.silent, + additionalMeta: options.payload?.meta, + index: options.payload?.category ? index : '', + }, + recipients: notificationType.recipient, + graph: options.advanced?.graph, + ipfsHash: options.advanced?.ipfs, + env: env, + chatId: options.advanced?.chatid, + pgpPrivateKey: options.advanced?.pgpPrivateKey, + channelFound: channelFound, + }; + + return notificationPayload; + } + + // check if the fields are empty + protected isEmpty(field: string) { + if (field.trim().length == 0) { + return true; + } + + return false; + } + + // check if the length is valid + protected isValidLength( + data: string, + upperLen: number = LENGTH_UPPER_LIMIT, + lowerLen: number = LENGTH_LOWER_LIMTI + ): boolean { + return data.length >= lowerLen && data.length <= upperLen!; + } + + // check if url is valid + protected isValidUrl(urlString: string): boolean { + const urlPattern = new RegExp( + '^((?:https|http):\\/\\/)' + // validate protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path + '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string + '(\\#[-a-z\\d_]*)?$', + 'i' + ); // validate fragment locator + return !!urlPattern.test(urlString); + } + + // check all the fields of channel + protected verifyEmptyChannelParameters( + options: CreateChannelOptions + ): boolean { + if (this.isEmpty(options.name)) { + throw new Error('Channel name cannot be empty'); + } else if (this.isEmpty(options.description)) { + throw new Error('Channel description cannot be empty'); + } else if (this.isEmpty(options.icon)) { + throw new Error('Channel icon cannot be empty'); + } else if (this.isEmpty(options.url)) { + throw new Error('Channel url cannot ne empty'); + } else { + return true; + } + } + + // check for valid length and url + protected validateParameterLength(options: CreateChannelOptions): boolean { + if (!this.isValidLength(options.name)) { + throw new Error( + `Channel name should not exceed ${LENGTH_UPPER_LIMIT} characters` + ); + } else if (!this.isValidLength(options.description)) { + throw new Error( + `Channel description should not exceed ${LENGTH_UPPER_LIMIT} characters` + ); + } else if ( + !this.isValidLength(options.url) || + !this.isValidUrl(options.url) + ) { + throw new Error( + `Channel url either excees ${LENGTH_UPPER_LIMIT} characters or is not a valid url` + ); + } else { + return true; + } + } + + protected validateChannelParameters(options: CreateChannelOptions): boolean { + return ( + this.verifyEmptyChannelParameters(options) && + this.validateParameterLength(options) + ); + } + + // create contract instance + protected createContractInstance( + contractAddress: string | `0x${string}`, + contractABI: any, + network: Chain + ) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + let contract: any; + const pushSigner = this.signer ? new Signer(this.signer) : null; + if (pushSigner?.isViemSigner(this.signer)) { + const client = createPublicClient({ + chain: network, + transport: http(), + }); + contract = getContract({ + abi: contractABI, + address: contractAddress as `0x${string}`, + client: { + public: client, + wallet: this.signer as unknown as WalletClient, + }, + }); + } else { + contract = new ethers.Contract( + contractAddress, + contractABI, + this.signer as unknown as EthersSigner + ); + } + return contract; + } + + protected async fetchBalance(contract: any, userAddress: string) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + let balance: bigint; + const pushSigner = new Signer(this.signer); + try { + if (pushSigner.isViemSigner(this.signer)) { + balance = BigInt( + await contract.read.balanceOf({ + args: [userAddress], + }) + ); + } else { + balance = BigInt(await contract.balanceOf(userAddress)); + } + return balance; + } catch (err) { + throw new Error(JSON.stringify(err)); + } + } + + protected async fetchAllownace( + contract: any, + userAddress: string, + spenderAddress: string + ) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + + const pushSigner = new Signer(this.signer); + let allowance: bigint; + try { + if (!pushSigner.isViemSigner(this.signer)) { + allowance = BigInt( + await contract!['allowance'](userAddress, spenderAddress) + ); + } else { + allowance = BigInt( + await contract.read.allowance({ + args: [userAddress, spenderAddress], + }) + ); + } + return allowance; + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + protected async fetchUpdateCounter(contract: any, userAddress: string) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + let count: bigint; + const pushSigner = new Signer(this.signer); + try { + if (!pushSigner.isViemSigner(this.signer)) { + count = BigInt(await contract!['channelUpdateCounter'](userAddress)); + } else { + count = BigInt( + await contract.read.channelUpdateCounter({ + args: [userAddress], + }) + ); + } + // add one and return the count + return count + BigInt(1); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + protected async approveToken( + contract: any, + spenderAddress: string, + amount: string | bigint + ) { + try { + 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 (!contract.write) { + throw new Error('viem signer is not provided'); + } + const approvalTrxPromise = contract.write.approve({ + args: [spenderAddress, amount], + }); + const approvalTrxRes = await approvalTrxPromise; + } + return true; + } catch (error) { + console.error(error); + return false; + } + } + + protected async createChannel( + contract: any, + channelType: number, + identityBytes: Uint8Array, + fees: bigint + ) { + let createChannelRes; + try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + if (!pushSigner.isViemSigner(this.signer)) { + const createChannelPromise = contract!['createChannelWithPUSH']( + channelType, + identityBytes, + fees, + this.getTimeBound(), + { + gasLimit: 1000000, + } + ); + const createChannelTrx = await createChannelPromise; + const createChannelTrxStatus = + await this.signer?.provider?.waitForTransaction( + createChannelTrx.hash + ); + if (createChannelTrxStatus?.status == 0) { + throw new Error('Something Went wrong while creating your channel'); + } + createChannelRes = createChannelTrx.hash; + } else { + if (!contract.write) { + throw new Error('viem signer is not provided'); + } + const createChannelPromise = contract.write.createChannelWithPUSH({ + args: [ + channelType, + toHex(new Uint8Array(identityBytes)), + fees, + this.getTimeBound(), + ], + }); + createChannelRes = await createChannelPromise; + } + return createChannelRes; + } catch (error: any) { + throw new Error(error?.message); + } + } + + protected async updateChannel( + contract: any, + account: string, + identityBytes: Uint8Array, + fees: bigint + ) { + let updateChannelRes; + try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + if (!pushSigner.isViemSigner(this.signer)) { + const updateChannelPromise = contract!['updateChannelMeta']( + account, + identityBytes, + fees, + { + gasLimit: 1000000, + } + ); + const updateChannelTrx = await updateChannelPromise; + const updateChannelTrxStatus = + await this.signer?.provider?.waitForTransaction( + updateChannelTrx.hash + ); + if (updateChannelTrxStatus?.status == 0) { + throw new Error('Something Went wrong while updating your channel'); + } + updateChannelRes = updateChannelTrx.hash; + } else { + if (!contract.write) { + throw new Error('viem signer is not provided'); + } + const updateChannelPromise = contract.write.updateChannelMeta({ + args: [account, toHex(new Uint8Array(identityBytes)), fees], + }); + updateChannelRes = await updateChannelPromise; + } + + return updateChannelRes; + } catch (error: any) { + throw new Error(error?.message); + } + } + + 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 (!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 (!contract.write) { + throw new Error('viem signer is not provided'); + } + const verifyTrxPromise = contract.write.verify({ + args: [channelToBeVerified], + }); + verifyTrxRes = await verifyTrxPromise; + } + return verifyTrxRes; + } catch (error: any) { + throw new Error(error.message); + } + } + + protected async createChanelSettings( + contract: any, + numberOfSettings: number, + settings: string, + description: string, + fees: bigint + ) { + try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + let createSettingsRes; + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); + } + const createSettingsPromise = contract!['createChannelSettings']( + numberOfSettings, + settings, + description, + fees + ); + const createSettings = await createSettingsPromise; + await this.signer?.provider?.waitForTransaction(createSettings.hash); + createSettingsRes = createSettings.hash; + } else { + if (!contract.write) { + throw new Error('viem signer is not provided'); + } + const createSettingsTrxPromise = contract.write.createChannelSettings({ + args: [numberOfSettings, settings, description, fees], + }); + createSettingsRes = await createSettingsTrxPromise; + } + return createSettingsRes; + } catch (error: any) { + throw new Error(error.message); + } + } + + 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 (!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 (!contract.write) { + throw new Error('viem signer is not provided'); + } + const addDelegateTrxPromise = contract.write.addDelegate({ + args: [delegatee], + }); + addDelegateRes = await addDelegateTrxPromise; + } + return addDelegateRes; + } catch (error: any) { + throw new Error(error.message); + } + } + + 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 (!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 (!contract.write) { + throw new Error('viem signer is not provided'); + } + const removeDelegateTrxPromise = contract.write.removeDelegate({ + args: [delegatee], + }); + removeDelegateRes = await removeDelegateTrxPromise; + } + return removeDelegateRes; + } catch (error: any) { + throw new Error(error.message); + } + } + + protected async getChainId(signer: SignerType) { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + return pushSigner.getChainId(); + } + + protected async uploadToIPFSViaPushNode(data: string): Promise { + try { + const response = await axiosPost( + `${config.CORE_CONFIG[this.env!].API_BASE_URL}/v1/ipfs/upload`, + { data } + ); + return response.data.cid as string; + } catch (error) { + throw new Error('Something went wrong while uploading data to IPFS'); + } + } + + protected getTimeBound(timeStamp?: number) { + // for now returns 0 for non-time bound. Later it can be modified to handle time bound channels + return 0; + } + + protected getMinimalSetting(configuration: NotificationSettings): { + setting: string; + description: string; + } { + let notificationSetting = ''; + let notificationSettingDescription = ''; + for (let i = 0; i < configuration.length; i++) { + const ele = configuration[i]; + if (ele.type == BOOLEAN_TYPE) { + notificationSetting = + notificationSetting + + SETTING_SEPARATOR + + BOOLEAN_TYPE + + SETTING_DELIMITER + + ele.default; + } + if (ele.type == SLIDER_TYPE) { + if (ele.data) { + const enabled = + ele.data && ele.data.enabled != undefined + ? Number(ele.data.enabled).toString() + : DEFAULT_ENABLE_VALUE; + const ticker = ele.data.ticker ?? DEFAULT_TICKER_VALUE; + notificationSetting = + notificationSetting + + SETTING_SEPARATOR + + SLIDER_TYPE + + SETTING_DELIMITER + + enabled + + SETTING_DELIMITER + + ele.default + + SETTING_DELIMITER + + ele.data.lower + + SETTING_DELIMITER + + ele.data.upper + + SETTING_DELIMITER + + ticker; + } + } + if (ele.type == RANGE_TYPE) { + if (ele.default && typeof ele.default == 'object' && ele.data) { + const enabled = + ele.data && ele.data.enabled != undefined + ? Number(ele.data.enabled).toString() + : DEFAULT_ENABLE_VALUE; + const ticker = ele.data.ticker ?? DEFAULT_TICKER_VALUE; + notificationSetting = + notificationSetting + + SETTING_SEPARATOR + + RANGE_TYPE + + SETTING_DELIMITER + + enabled + + SETTING_DELIMITER + + ele.default.lower + + SETTING_DELIMITER + + ele.default.upper + + SETTING_DELIMITER + + ele.data.lower + + SETTING_DELIMITER + + ele.data.upper + + SETTING_DELIMITER + + ticker; + } + } + + notificationSettingDescription = + notificationSettingDescription + SETTING_SEPARATOR + ele.description; + } + return { + setting: notificationSetting.replace(/^\+/, ''), + description: notificationSettingDescription.replace(/^\+/, ''), + }; + } + + protected getMinimalUserSetting(setting: UserSetting[]) { + if (!setting) { + return null; + } + let userSetting = ''; + let numberOfSettings = 0; + for (let i = 0; i < setting.length; i++) { + const ele = setting[i]; + const enabled = ele.enabled ? 1 : 0; + if (ele.enabled) numberOfSettings++; + + if (Object.keys(ele).includes('value')) { + // slider type + if (typeof ele.value == 'number') + userSetting = + userSetting + + SLIDER_TYPE + + SETTING_DELIMITER + + enabled + + SETTING_DELIMITER + + ele.value; + else { + userSetting = + userSetting + + RANGE_TYPE + + SETTING_DELIMITER + + enabled + + SETTING_DELIMITER + + ele.value?.lower + + SETTING_DELIMITER + + ele.value?.upper; + } + } else { + // boolean type + userSetting = userSetting + BOOLEAN_TYPE + SETTING_DELIMITER + enabled; + } + if (i != setting.length - 1) + userSetting = userSetting + SETTING_SEPARATOR; + } + return numberOfSettings + SETTING_SEPARATOR + userSetting; + } + + /** + * @param address Address of the channel or alias + * @returns Channel info for the address + */ + protected async getChannelOrAliasInfo(address: string) { + try { + const channelOrAliasCaip = validateCAIP(address) + ? address + : getFallbackETHCAIPAddress(this.env!, this.account!); + + const { networkId } = getCAIPDetails( + channelOrAliasCaip + ) as CAIPDetailsType; + + let channelInCaip = channelOrAliasCaip; + if (networkId !== '1' && networkId !== '11155111') { + // Alias + const aliasInfo = await this.getAliasInfo(address); + channelInCaip = aliasInfo?.channel || channelInCaip; + } + + const channelInfo = await PUSH_CHANNEL.getChannel({ + channel: channelInCaip, + env: this.env, + }); + + return channelInfo || null; + } catch (error) { + return null; + } + } + + protected async initiateAddAlias(contract: any, alias: string) { + try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + let addAliasRes; + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); + } + + const addAliasTrxPromise = contract!['verifyChannelAlias'](alias); + const addAliasTrx = await addAliasTrxPromise; + await this.signer?.provider?.waitForTransaction(addAliasTrx.hash); + addAliasRes = addAliasTrx.hash; + } else { + if (!contract.write) { + throw new Error('viem signer is not provided'); + } + + const addAliasTrxPromise = contract.write.verifyChannelAlias({ + args: [alias], + }); + addAliasRes = await addAliasTrxPromise; + } + return addAliasRes; + } catch (error: any) { + throw new Error(error.message); + } + } + + protected async verifyAlias(contract: any, channelAddress: string) { + try { + if (!this.signer) { + throw new Error('Signer is not provided'); + } + const pushSigner = new Signer(this.signer); + let verifyAliasRes; + if (!pushSigner.isViemSigner(this.signer)) { + if (!this.signer.provider) { + throw new Error('ethers provider is not provided'); + } + const addAliasTrxPromise = + contract!['verifyChannelAlias'](channelAddress); + const addAliasTrx = await addAliasTrxPromise; + await this.signer?.provider?.waitForTransaction(addAliasTrx.hash); + verifyAliasRes = addAliasTrx.hash; + } else { + if (!contract.write) { + throw new Error('viem signer is not provided'); + } + const addAliasTrxPromise = contract.write.verifyChannelAlias({ + args: [channelAddress], + }); + verifyAliasRes = await addAliasTrxPromise; + } + + const networkDetails = await pushSigner.getChainId(); + const aliasAddress = await pushSigner.getAddress(); + + const aliasIncaip = `eip155:${networkDetails}:${aliasAddress}`; + const channelInfo = await this.getChannelOrAliasInfo(aliasIncaip); + + return { verifyAliasRes, channelInfo }; + } catch (error: any) { + throw new Error(error.message); + } + } + + /** + * @param aliasInCaip Alias address in CAIP format + * @returns Channel info for the alias + */ + private async getAliasInfo(aliasInCaip: string) { + const API_BASE_URL = getAPIBaseUrls(this.env!); + const apiEndpoint = `${API_BASE_URL}/v1/alias`; + const requestUrl = `${apiEndpoint}/${aliasInCaip}/channel`; + + try { + const response = await axiosGet(requestUrl); + return response.data; + } catch (error) { + return null; + } + } + + protected getAddressFromCaip(caipAddress: string): string { + return caipAddress?.split(':')[caipAddress?.split(':').length - 1]; + } + + protected isValidPCaip(address: string): boolean { + const addressComponents = address.split(':'); + return ( + addressComponents.length == 2 && + addressComponents[0] == 'eip155' && + viem.isAddress(addressComponents[1]) + ); + } +} diff --git a/packages/d-node-notif/src/lib/pushstream/DataModifier.ts b/packages/d-node-notif/src/lib/pushstream/DataModifier.ts new file mode 100644 index 000000000..c2f155349 --- /dev/null +++ b/packages/d-node-notif/src/lib/pushstream/DataModifier.ts @@ -0,0 +1,78 @@ +import { + NotificationEvent, + NotificationEventType, + NotificationType, + NOTIFICATION, +} from './pushStreamTypes'; + +export class DataModifier { + public static mapToNotificationEvent( + data: any, + notificationEventType: NotificationEventType, + origin: 'other' | 'self', + includeRaw = false + ): NotificationEvent { + const notificationType = + (Object.keys(NOTIFICATION.TYPE) as NotificationType[]).find( + (key) => NOTIFICATION.TYPE[key] === data.payload.data.type + ) || 'BROADCAST'; // Assuming 'BROADCAST' as the default + + let recipients: string[]; + + if (Array.isArray(data.payload.recipients)) { + recipients = data.payload.recipients; + } else if (typeof data.payload.recipients === 'string') { + recipients = [data.payload.recipients]; + } else { + recipients = Object.keys(data.payload.recipients); + } + + const notificationEvent: NotificationEvent = { + event: notificationEventType, + origin: origin, + timestamp: data.epoch, + from: data.sender, + to: recipients, + notifID: data.payload_id.toString(), + channel: { + name: data.payload.data.app, + icon: data.payload.data.icon, + url: data.payload.data.url, + }, + meta: { + type: 'NOTIFICATION.' + notificationType, + }, + message: { + notification: { + title: data.payload.notification.title, + body: data.payload.notification.body, + }, + payload: { + title: data.payload.data.asub, + body: data.payload.data.amsg, + cta: data.payload.data.acta, + embed: data.payload.data.aimg, + meta: { + domain: data.payload.data.additionalMeta?.domain || 'push.org', + type: data.payload.data.additionalMeta?.type, + data: data.payload.data.additionalMeta?.data, + }, + }, + }, + config: { + expiry: data.payload.data.etime, + silent: data.payload.data.silent === '1', + hidden: data.payload.data.hidden === '1', + }, + source: data.source, + }; + + if (includeRaw) { + notificationEvent.raw = { + verificationProof: data.payload.verificationProof, + }; + } + + return notificationEvent; + } +} diff --git a/packages/d-node-notif/src/lib/pushstream/PushStream.ts b/packages/d-node-notif/src/lib/pushstream/PushStream.ts new file mode 100644 index 000000000..7052897ff --- /dev/null +++ b/packages/d-node-notif/src/lib/pushstream/PushStream.ts @@ -0,0 +1,315 @@ +import { EventEmitter } from 'events'; +import { createSocketConnection } from './socketClient'; +import { ENV } from '../constants'; +import { + NotificationEventType, + PushStreamInitializeProps, + STREAM, + EVENTS, +} from './pushStreamTypes'; +import { DataModifier } from './DataModifier'; +import { pCAIP10ToWallet } from '../helpers'; +import { ProgressHookType, SignerType } from '../types'; +import { ALPHA_FEATURE_CONFIG } from '../config'; +import { ADDITIONAL_META_TYPE } from '../payloads'; +import { v4 as uuidv4 } from 'uuid'; + +export type StreamType = STREAM | '*'; +export class PushStream extends EventEmitter { + private pushNotificationSocket: any; + + private account: string; + private raw: boolean; + private options: PushStreamInitializeProps; + private listen: StreamType[]; + private disconnected: boolean; + public uid: string; + public notifSocketCount: number; + public notifSocketConnected: boolean; + constructor( + account: string, + private _listen: StreamType[], + options: PushStreamInitializeProps, + private decryptedPgpPvtKey?: string, + private progressHook?: (progress: ProgressHookType) => void, + private signer?: SignerType + ) { + super(); + + this.account = account; + + this.raw = options.raw ?? false; + this.options = options; + this.listen = _listen; + this.disconnected = false; + this.uid = uuidv4(); + this.notifSocketCount = 0; + this.notifSocketConnected = false; + } + + static async initialize( + account: string, + listen: StreamType[], + env: ENV, + decryptedPgpPvtKey?: string, + progressHook?: (progress: ProgressHookType) => void, + signer?: SignerType, + options?: PushStreamInitializeProps + ): Promise { + const defaultOptions: PushStreamInitializeProps = { + raw: false, + connection: { + auto: true, + retries: 3, + }, + env: env, + }; + + if (!listen || listen.length === 0) { + throw new Error( + 'The listen property must have at least one STREAM type.' + ); + } + + const settings = { + ...defaultOptions, + ...options, + }; + + const accountToUse = settings.overrideAccount || account; + + if (listen.includes('*')) { + listen = Object.values(STREAM); + } + + const stream = new PushStream( + accountToUse, + listen, + settings, + decryptedPgpPvtKey, + progressHook, + signer + ); + return stream; + } + + public async reinit( + listen: STREAM[], + newOptions: PushStreamInitializeProps + ): Promise { + this.uid = uuidv4(); + this.listen = listen; + this.options = { ...this.options, ...newOptions }; + await this.disconnect(); + await this.connect(); + } + + public async connect(): Promise { + return new Promise((resolve, reject) => { + (async () => { + const shouldInitializeNotifSocket = + !this.listen || + this.listen.length === 0 || + this.listen.includes(STREAM.NOTIF) || + this.listen.includes(STREAM.NOTIF_OPS); + + let isNotifSocketConnected = false; + // Function to check and emit the STREAM.CONNECT event + const checkAndEmitConnectEvent = () => { + if ( + (shouldInitializeNotifSocket && isNotifSocketConnected) || + !shouldInitializeNotifSocket + ) { + this.emit(STREAM.CONNECT); + console.log( + 'RestAPI::PushStream::connect - Emitted STREAM.CONNECT' + ); + resolve(); + } + }; + + const TIMEOUT_DURATION = 5000; // Timeout duration in milliseconds + setTimeout(() => { + if (!this.notifSocketConnected) { + reject(new Error('Connection timeout')); // Reject the promise if connect event is not emitted within the timeout + } + }, TIMEOUT_DURATION); + + const handleSocketDisconnection = async (socketType: 'notif') => { + if (socketType === 'notif') { + isNotifSocketConnected = false; + this.notifSocketConnected = false; + this.notifSocketCount--; + this.emit(STREAM.DISCONNECT); + console.log( + 'RestAPI::PushStream::handleSocketDisconnection - Emitted STREAM.DISCONNECT for notification.' + ); + } + }; + + if (shouldInitializeNotifSocket) { + if (!this.pushNotificationSocket) { + // If pushNotificationSocket does not exist, create a new socket connection + console.log( + 'RestAPI::PushStream::NotifSocket::Create - pushNotificationSocket does not exist, creating new socket connection...' + ); + this.pushNotificationSocket = await createSocketConnection({ + user: pCAIP10ToWallet(this.account), + env: this.options?.env as ENV, + socketOptions: { + autoConnect: this.options?.connection?.auto ?? true, + reconnectionAttempts: this.options?.connection?.retries ?? 3, + }, + }); + + if (!this.pushNotificationSocket) { + reject( + new Error( + 'RestAPI::PushStream::NotifSocket::Error - Push notification socket not connected' + ) + ); + } + } else if ( + this.pushNotificationSocket && + !this.notifSocketConnected + ) { + // If pushNotificationSocket exists but is not connected, attempt to reconnect + console.log( + 'RestAPI::PushStream::NotifSocket::Reconnect - Attempting to reconnect push notification socket...' + ); + this.notifSocketCount++; + this.pushNotificationSocket.connect(); // Assuming connect() is the method to re-establish connection + } else { + // If pushNotificationSocket is already connected + console.log( + 'RestAPI::PushStream::NotifSocket::Status - Push notification socket already connected' + ); + } + } + + const shouldEmit = (eventType: STREAM): boolean => { + if (!this.listen || this.listen.length === 0) { + return true; + } + return this.listen.includes(eventType); + }; + + if (this.pushNotificationSocket) { + this.pushNotificationSocket.on(EVENTS.CONNECT, async () => { + console.log( + `RestAPI::PushStream::NotifSocket::Connect - Notification Socket Connected (ID: ${this.pushNotificationSocket.id})` + ); + isNotifSocketConnected = true; + this.notifSocketCount++; + this.notifSocketConnected = true; + checkAndEmitConnectEvent(); + }); + + this.pushNotificationSocket.on(EVENTS.DISCONNECT, async () => { + console.log( + 'RestAPI::PushStream::NotifSocket::Disconnect - Notification socket disconnected.' + ); + await handleSocketDisconnection('notif'); + }); + + this.pushNotificationSocket.on(EVENTS.USER_FEEDS, (data: any) => { + try { + if ( + data.payload.data.additionalMeta?.type === + `${ADDITIONAL_META_TYPE.PUSH_VIDEO}+1` + ) { + // VIDEO NOTIF NOT IMPLEMENTED + } 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) { + console.error( + `RestAPI::PushStream::NotifSocket::UserFeeds::Error - Error handling event: ${error}, Data: ${JSON.stringify( + data + )}` + ); + } + }); + + this.pushNotificationSocket.on( + EVENTS.USER_SPAM_FEEDS, + (data: any) => { + try { + const modifiedData = DataModifier.mapToNotificationEvent( + data, + NotificationEventType.SPAM, + this.account === data.sender ? 'self' : 'other', + this.raw + ); + modifiedData.origin = + this.account === modifiedData.from ? 'self' : 'other'; + if (this.shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + this.emit(STREAM.NOTIF, modifiedData); + } + } + } catch (error) { + console.error( + 'Error handling USER_SPAM_FEEDS event:', + error, + 'Data:', + data + ); + } + } + ); + } + + this.disconnected = false; + })(); + }); + } + + public connected(): boolean { + // Log the connection status of both sockets with detailed prefix + console.log( + `RestAPI::PushStream::connected::Notification Socket Connected: ${this.notifSocketConnected}` + ); + return this.notifSocketConnected; + } + + public async disconnect(): Promise { + // Disconnect push notification socket if connected + if (this.pushNotificationSocket && this.notifSocketConnected) { + this.pushNotificationSocket.disconnect(); + console.log( + 'RestAPI::PushStream::disconnect::Push notification socket disconnected.' + ); + } + } + + public info() { + return { + options: this.options, + listen: this.listen, + }; + } + + private shouldEmitChannel(dataChannelId: string): boolean { + if ( + !this.options.filter?.channels || + this.options.filter.channels.length === 0 || + this.options.filter.channels.includes('*') + ) { + return true; + } + return this.options.filter.channels.includes(dataChannelId); + } +} diff --git a/packages/d-node-notif/src/lib/pushstream/pushStreamTypes.ts b/packages/d-node-notif/src/lib/pushstream/pushStreamTypes.ts new file mode 100644 index 000000000..aba5f30dd --- /dev/null +++ b/packages/d-node-notif/src/lib/pushstream/pushStreamTypes.ts @@ -0,0 +1,114 @@ +import { ENV } from '../constants'; + +export type PushStreamInitializeProps = { + filter?: { + channels?: string[]; + }; + connection?: { + auto?: boolean; + retries?: number; + }; + raw?: boolean; + env?: ENV; + overrideAccount?: string; +}; + +export enum STREAM { + PROFILE = 'STREAM.PROFILE', + ENCRYPTION = 'STREAM.ENCRYPTION', + NOTIF = 'STREAM.NOTIF', + NOTIF_OPS = 'STREAM.NOTIF_OPS', + CONNECT = 'STREAM.CONNECT', + DISCONNECT = 'STREAM.DISCONNECT', +} + +export enum NotificationEventType { + INBOX = 'notification.inbox', + SPAM = 'notification.spam', +} + +export interface Profile { + image: string; + publicKey: string; +} + +export const NOTIFICATION = { + TYPE: { + BROADCAST: 1, + TARGETTED: 3, + SUBSET: 4, + }, +} as const; + +export type NotificationType = keyof typeof NOTIFICATION.TYPE; + +export interface NotificationEvent { + event: NotificationEventType; + origin: 'other' | 'self'; + timestamp: string; + from: string; + to: string[]; + notifID: string; + channel: { + name: string; + icon: string; + url: string; + }; + meta: { + type: string; + }; + message: { + notification: { + title: string; + body: string; + }; + payload?: { + title?: string; + body?: string; + cta?: string; + embed?: string; + meta?: { + domain?: string; + type: string; + data: string; + }; + }; + }; + config?: { + expiry?: number; + silent?: boolean; + hidden?: boolean; + }; + advanced?: { + chatid?: string; + }; + source: string; + streamUid?: string; + raw?: { + verificationProof: string; + }; +} + +export enum EVENTS { + // Websocket + CONNECT = 'connect', + DISCONNECT = 'disconnect', + + // Notification + USER_FEEDS = 'userFeeds', + USER_SPAM_FEEDS = 'userSpamFeeds', +} + +export type SocketInputOptions = { + user: string; + env: ENV; + apiKey?: string; + socketOptions?: SocketOptions; +}; + +type SocketOptions = { + autoConnect: boolean; + reconnectionAttempts?: number; + reconnectionDelayMax?: number; + reconnectionDelay?: number; +}; diff --git a/packages/d-node-notif/src/lib/pushstream/socketClient.ts b/packages/d-node-notif/src/lib/pushstream/socketClient.ts new file mode 100644 index 000000000..768acb3cb --- /dev/null +++ b/packages/d-node-notif/src/lib/pushstream/socketClient.ts @@ -0,0 +1,44 @@ +import { io } from 'socket.io-client'; +import { API_BASE_URL } from '../config'; +import { getCAIPAddress } from '../helpers'; +import { SocketInputOptions } from './pushStreamTypes'; + +export async function createSocketConnection({ + user, + env, + socketOptions, +}: SocketInputOptions) { + const { + autoConnect = true, + reconnectionAttempts = 5, + reconnectionDelay, + reconnectionDelayMax, + } = socketOptions || {}; + + let pushWSUrl = API_BASE_URL[env]; + + if (pushWSUrl.endsWith('/apis')) { + pushWSUrl = pushWSUrl.substring(0, pushWSUrl.length - 5); + } + const transports = ['websocket']; + + let pushSocket = null; + + try { + const userAddressInCAIP = await getCAIPAddress(env, user, 'User'); + const query = { address: userAddressInCAIP }; + + pushSocket = io(pushWSUrl, { + transports, + query, + autoConnect, + reconnectionAttempts, + ...(reconnectionDelay !== undefined && { reconnectionDelay }), + ...(reconnectionDelayMax !== undefined && { reconnectionDelayMax }), + }); + } catch (e) { + console.error('[PUSH-SDK] - Socket connection error: ', e); + } + + return pushSocket; +} diff --git a/packages/d-node-notif/src/lib/types/index.ts b/packages/d-node-notif/src/lib/types/index.ts new file mode 100644 index 000000000..0ff5d65a0 --- /dev/null +++ b/packages/d-node-notif/src/lib/types/index.ts @@ -0,0 +1,419 @@ +// for namespace TYPES +/* eslint-disable @typescript-eslint/no-namespace */ +import { + ADDITIONAL_META_TYPE, + IDENTITY_TYPE, + NOTIFICATION_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, +} from '../../lib/payloads/constants'; +import { ENV } from '../constants'; +import { EthEncryptedData } from '@metamask/eth-sig-util'; + +export type Env = (typeof ENV)[keyof typeof ENV]; + +// the type for the the response of the input data to be parsed +export type ApiNotificationType = { + payload_id: number; + channel: string; + epoch: string; + payload: { + apns: { + payload: { + aps: { + category: string; + 'mutable-content': number; + 'content-available': number; + }; + }; + fcm_options: { + image: string; + }; + }; + data: { + app: string; + sid: string; + url: string; + acta: string; + aimg: string; + amsg: string; + asub: string; + icon: string; + type: string; + epoch: string; + appbot: string; + hidden: string; + secret: string; + }; + android: { + notification: { + icon: string; + color: string; + image: string; + default_vibrate_timings: boolean; + }; + }; + notification: { + body: string; + title: string; + }; + }; + source: string; +}; + +// The output response from parsing a notification object +export type ParsedResponseType = { + cta: string; + title: string; + message: string; + icon: string; + url: string; + sid: string; + app: string; + image: string; + blockchain: string; + secret: string; + notification: { + title: string; + body: string; + }; +}; + +export type ApiSubscriptionType = { + channel: string; + user_settings: string | null; +}; + +export type NotificationSettingType = { + type: number; + default?: number | { upper: number; lower: number }; + description: string; + data?: { + upper: number; + lower: number; + ticker?: number; + }; + userPreferance?: { + value: number | { upper: number; lower: number }; + enabled: boolean; + }; +}; + +export type ApiSubscribersType = { + itemcount: number; + subscribers: { + subscriber: string; + settings: string | null; + }[]; +}; + +export interface VideoNotificationRules { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE; + data: { + chatId?: string; + }; + }; +} + +// SendNotificationRules can be extended in the future for other use cases +export type SendNotificationRules = VideoNotificationRules; + +export interface ISendNotificationInputOptions { + senderType?: 0 | 1; + signer: any; + type: NOTIFICATION_TYPE; + identityType: IDENTITY_TYPE; + notification?: { + title: string; + body: string; + }; + payload?: { + sectype?: string; + title: string; + body: string; + cta: string; + img: string; + hidden?: boolean; + etime?: number; + silent?: boolean; + additionalMeta?: + | { + /** + * type = ADDITIONAL_META_TYPE+VERSION + * VERSION > 0 + */ + type: `${ADDITIONAL_META_TYPE}+${number}`; + data: string; + domain?: string; + } + | string; + /** + * @deprecated + * use additionalMeta instead + */ + metadata?: any; + index?: string; + }; + recipients?: string | string[]; // CAIP or plain ETH + channel: string; // CAIP or plain ETH + /** + * @deprecated + * use payload.etime instead + */ + expiry?: number; + /** + * @deprecated + * use payload.hidden instead + */ + hidden?: boolean; + graph?: { + id: string; + counter: number; + }; + ipfsHash?: string; + env?: ENV; + /** @deprecated - Use `rules` object instead */ + chatId?: string; + rules?: SendNotificationRules; + pgpPrivateKey?: string; + channelFound?: boolean; +} + +export interface INotificationPayload { + notification: { + title: string; + body: string; + }; + data: { + acta: string; + aimg: string; + amsg: string; + asub: string; + type: string; + etime?: number; + hidden?: boolean; + sectype?: string; + }; + recipients: any; +} + +export interface UserProfile { + name: string | null; + desc: string | null; + picture: string | null; + blockedUsersList: Array | null; + profileVerificationProof: string | null; +} + +export interface UserV2 { + msgSent: number; + maxMsgPersisted: number; + did: string; + wallets: string; + profile: UserProfile; + encryptedPrivateKey: string | null; + publicKey: string | null; + verificationProof: string | null; + origin?: string | null; +} + +export interface Subscribers { + itemcount: number; + subscribers: Array; +} + +export interface IUser { + msgSent: number; + maxMsgPersisted: number; + did: string; + wallets: string; + profile: { + name: string | null; + desc: string | null; + picture: string | null; + profileVerificationProof: string | null; + blockedUsersList: Array | null; + }; + encryptedPrivateKey: string; + publicKey: string; + verificationProof: string; + origin?: string | null; + + /** + * @deprecated Use `profile.name` instead. + */ + name: string | null; + /** + * @deprecated Use `profile.desc` instead. + */ + about: string | null; + /** + * @deprecated Use `profile.picture` instead. + */ + profilePicture: string | null; + /** + * @deprecated Use `msgSent` instead. + */ + numMsg: number; + /** + * @deprecated Use `maxMsgPersisted` instead. + */ + allowedNumMsg: number; + /** + * @deprecated Use `encryptedPrivateKey.version` instead. + */ + encryptionType: string; + /** + * @deprecated Use `verificationProof` instead. + */ + signature: string; + /** + * @deprecated Use `verificationProof` instead. + */ + sigType: string; + /** + * @deprecated Use `encryptedPrivateKey.encryptedPassword` instead. + */ + encryptedPassword: string | null; + /** + * @deprecated + */ + nftOwner: string | null; + /** + * @deprecated Not recommended to be used anywhere + */ + linkedListHash?: string | null; + /** + * @deprecated Not recommended to be used anywhere + */ + nfts?: [] | null; +} + +export interface IConnectedUser extends IUser { + privateKey: string | null; +} + +export interface AccountEnvOptionsType extends EnvOptionsType { + /** + * Environment variable + */ + account: string; +} + +export interface ConversationHashOptionsType extends AccountEnvOptionsType { + conversationId: string; +} + +export interface UserInfo { + wallets: string; + publicKey: string; + name: string; + image: string; + isAdmin: boolean; +} + +export type TypedDataField = { + name: string; + type: string; +}; + +export type TypedDataDomain = { + chainId?: number | undefined; + name?: string | undefined; + salt?: string | undefined; + verifyingContract?: string | undefined; + version?: string | undefined; +}; + +export type ethersV5SignerType = { + _signTypedData: ( + domain: TypedDataDomain, + types: Record>, + value: Record + ) => Promise; + getAddress: () => Promise; + signMessage: (message: Uint8Array | string) => Promise; + privateKey?: string; + provider?: any; +}; + +export type ethersV6SignerType = { + signTypedData: ( + domain: TypedDataDomain, + types: Record>, + value: Record + ) => Promise; + getAddress: () => Promise; + signMessage: (message: Uint8Array | string) => Promise; + privateKey?: string; + provider?: any; +}; + +export type viemSignerType = { + signTypedData: (args: { + account: any; + domain: any; + types: any; + primaryType: any; + message: any; + }) => Promise<`0x${string}`>; + getChainId: () => Promise; + signMessage: (args: { + message: any; + account: any; + [key: string]: any; + }) => Promise<`0x${string}`>; + account: { [key: string]: any }; + privateKey?: string; + provider?: any; +}; + +export type SignerType = + | ethersV5SignerType + | ethersV6SignerType + | viemSignerType; + +export type EnvOptionsType = { + env?: ENV; +}; + +export type walletType = { + account: string | null; + signer: SignerType | null; +}; + +export type encryptedPrivateKeyTypeV1 = EthEncryptedData; + +export type encryptedPrivateKeyTypeV2 = { + ciphertext: string; + version?: string; + salt?: string; + nonce: string; + preKey?: string; + encryptedPassword?: encryptedPrivateKeyTypeV2; +}; + +export type encryptedPrivateKeyType = { + version?: string; + nonce: string; + ephemPublicKey?: string; + ciphertext: string; + salt?: string; + preKey?: string; + encryptedPassword?: encryptedPrivateKeyTypeV2; +}; + +export type ProgressHookType = { + progressId: string; + progressTitle: string; + progressInfo: string; + level: 'INFO' | 'SUCCESS' | 'WARN' | 'ERROR'; +}; + +export type ProgressHookTypeFunction = (...args: any[]) => ProgressHookType; + +export enum NotifictaionType { + BROADCAT = 1, + TARGETTED = 3, + SUBSET = 4, +} diff --git a/packages/d-node-notif/src/lib/user/auth.updateUser.ts b/packages/d-node-notif/src/lib/user/auth.updateUser.ts new file mode 100644 index 000000000..252839a70 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/auth.updateUser.ts @@ -0,0 +1,142 @@ +import { + authUpdateUserService, + getAccountAddress, + getWallet, +} from '../chat/helpers'; +import Constants, { ENV, ENCRYPTION_TYPE } from '../constants'; +import { + encryptPGPKey, + isValidPushCAIP, + preparePGPPublicKey, + walletToPCAIP10, +} from '../helpers'; +import PROGRESSHOOK from '../progressHook'; +import { + SignerType, + IUser, + ProgressHookType, + encryptedPrivateKeyType, + encryptedPrivateKeyTypeV2, + ProgressHookTypeFunction, +} from '../types'; +import { get } from './getUser'; + +//used only in progressHook to abstract encryption algotrithms +enum ENCRYPTION_TYPE_VERSION { + 'x25519-xsalsa20-poly1305' = 'PGP_V1', + 'aes256GcmHkdfSha256' = 'PGP_V2', + 'eip191-aes256-gcm-hkdf-sha256' = 'PGP_V3', + 'pgpv1:nft' = 'NFTPGP_V1', +} + +export type AuthUpdateProps = { + pgpPrivateKey: string; // decrypted pgp priv key + pgpEncryptionVersion: ENCRYPTION_TYPE; + signer: SignerType; + pgpPublicKey: string; + account?: string; + env?: ENV; + additionalMeta?: { + NFTPGP_V1?: { + password: string; //new nft profile password + }; + }; + progressHook?: (progress: ProgressHookType) => void; +}; + +/** + * Updation of encryption keys of a Push Profile to a specific version + */ +export const authUpdate = async (options: AuthUpdateProps): Promise => { + const { + pgpPrivateKey, + pgpEncryptionVersion, + signer, + pgpPublicKey, + account = null, + env = Constants.ENV.PROD, + additionalMeta, + progressHook, + } = options || {}; + + try { + const wallet = getWallet({ account, signer }); + const address = await getAccountAddress(wallet); + + const updatingCreds = + pgpEncryptionVersion === Constants.USER.ENCRYPTION_TYPE.NFTPGP_V1 + ? true + : false; + + if (!isValidPushCAIP(address)) { + throw new Error(`Invalid address!`); + } + + const caip10 = walletToPCAIP10(address); + const user = await get({ account: caip10, env: env }); + + if (!user || !user.encryptedPrivateKey) { + throw new Error('User not Found!'); + } + + // Report Progress + updatingCreds + ? progressHook?.(PROGRESSHOOK['PUSH-AUTH-UPDATE-05'] as ProgressHookType) + : progressHook?.( + (PROGRESSHOOK['PUSH-AUTH-UPDATE-01'] as ProgressHookTypeFunction)( + ENCRYPTION_TYPE_VERSION[pgpEncryptionVersion] + ) + ); + const signedPublicKey = await preparePGPPublicKey( + pgpEncryptionVersion, + pgpPublicKey, + wallet + ); + + // Report Progress + updatingCreds + ? progressHook?.(PROGRESSHOOK['PUSH-AUTH-UPDATE-06'] as ProgressHookType) + : progressHook?.( + (PROGRESSHOOK['PUSH-AUTH-UPDATE-02'] as ProgressHookTypeFunction)( + ENCRYPTION_TYPE_VERSION[pgpEncryptionVersion] + ) + ); + const encryptedPgpPrivateKey: encryptedPrivateKeyType = await encryptPGPKey( + pgpEncryptionVersion, + pgpPrivateKey, + wallet, + additionalMeta + ); + if (pgpEncryptionVersion === ENCRYPTION_TYPE.NFTPGP_V1) { + const encryptedPassword: encryptedPrivateKeyTypeV2 = await encryptPGPKey( + ENCRYPTION_TYPE.PGP_V3, + additionalMeta?.NFTPGP_V1?.password as string, + wallet, + additionalMeta + ); + encryptedPgpPrivateKey.encryptedPassword = encryptedPassword; + } + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-AUTH-UPDATE-03'] as ProgressHookType); + const body = { + user: user.did, + wallet, + publicKey: signedPublicKey, + encryptedPrivateKey: JSON.stringify(encryptedPgpPrivateKey), + env, + }; + const updatedUser = await authUpdateUserService(body); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-AUTH-UPDATE-04'] as ProgressHookType); + return updatedUser; + } catch (err) { + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-00' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(authUpdate.name, err)); + throw Error(`[Push SDK] - API - Error - API ${authUpdate.name} -: ${err}`); + } +}; diff --git a/packages/d-node-notif/src/lib/user/createUser.ts b/packages/d-node-notif/src/lib/user/createUser.ts new file mode 100644 index 000000000..c14822009 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/createUser.ts @@ -0,0 +1,151 @@ +import { + IPGPHelper, + PGPHelper, + createUserService, + generateRandomSecret, + getAccountAddress, + getWallet, +} from '../chat/helpers'; +import Constants, { ENV } from '../constants'; +import { + isValidPushCAIP, + walletToPCAIP10, + encryptPGPKey, + preparePGPPublicKey, + isValidNFTCAIP, + validatePssword, +} from '../helpers'; +import { + SignerType, + encryptedPrivateKeyType, + ProgressHookType, + IUser, + encryptedPrivateKeyTypeV2, + ProgressHookTypeFunction, +} from '../types'; +import PROGRESSHOOK from '../progressHook'; + +export type CreateUserProps = { + env?: ENV; + account?: string; + signer?: SignerType; + version?: typeof Constants.ENC_TYPE_V1 | typeof Constants.ENC_TYPE_V3; + additionalMeta?: { + NFTPGP_V1?: { + password: string; + }; + }; + progressHook?: (progress: ProgressHookType) => void; + origin?: string | null; +}; + +interface ICreateUser extends IUser { + decryptedPrivateKey?: string; +} + +export const create = async ( + options: CreateUserProps +): Promise => { + return await createUserCore(options, PGPHelper); +}; + +export const createUserCore = async ( + options: CreateUserProps, + pgpHelper: IPGPHelper +): Promise => { + const passPrefix = '$0Pc'; //password prefix to ensure password validation + const { + env = Constants.ENV.PROD, + account = null, + signer = null, + version = Constants.ENC_TYPE_V3, + additionalMeta = { + NFTPGP_V1: { + password: passPrefix + generateRandomSecret(10), + }, + }, + progressHook, + origin, + } = options || {}; + + try { + if (account == null && signer == null) { + throw new Error(`At least one from account or signer is necessary!`); + } + + const wallet = getWallet({ account, signer }); + const address = await getAccountAddress(wallet); + + if (!isValidPushCAIP(address)) { + throw new Error(`Invalid address!`); + } + if (additionalMeta?.NFTPGP_V1?.password) { + validatePssword(additionalMeta.NFTPGP_V1.password); + } + + const caip10: string = walletToPCAIP10(address); + let encryptionType = version; + + if (isValidNFTCAIP(caip10)) { + // upgrade to v4 (nft encryption) + encryptionType = Constants.ENC_TYPE_V4; + } else { + // downgrade to v1 + if (!signer) encryptionType = Constants.ENC_TYPE_V1; + } + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-CREATE-01'] as ProgressHookType); + const keyPairs = await pgpHelper.generateKeyPair(); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-CREATE-02'] as ProgressHookType); + const publicKey: string = await preparePGPPublicKey( + encryptionType, + keyPairs.publicKeyArmored, + wallet + ); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-CREATE-03'] as ProgressHookType); + const encryptedPrivateKey: encryptedPrivateKeyType = await encryptPGPKey( + encryptionType, + keyPairs.privateKeyArmored, + wallet, + additionalMeta + ); + if (encryptionType === Constants.ENC_TYPE_V4) { + const encryptedPassword: encryptedPrivateKeyTypeV2 = await encryptPGPKey( + Constants.ENC_TYPE_V3, + additionalMeta.NFTPGP_V1?.password as string, + wallet, + additionalMeta + ); + encryptedPrivateKey.encryptedPassword = encryptedPassword; + } + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-CREATE-04'] as ProgressHookType); + const body = { + user: caip10, + wallet, + publicKey: publicKey, + encryptedPrivateKey: JSON.stringify(encryptedPrivateKey), + env, + origin: origin, + }; + const createdUser: ICreateUser = await createUserService(body); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-CREATE-05'] as ProgressHookType); + createdUser.decryptedPrivateKey = keyPairs.privateKeyArmored; + return createdUser; + } catch (err) { + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-00' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(create.name, err)); + throw Error(`[Push SDK] - API - Error - API ${create.name} -: ${err}`); + } +}; diff --git a/packages/d-node-notif/src/lib/user/createUserWithProfile.ts b/packages/d-node-notif/src/lib/user/createUserWithProfile.ts new file mode 100644 index 000000000..6b031976e --- /dev/null +++ b/packages/d-node-notif/src/lib/user/createUserWithProfile.ts @@ -0,0 +1,52 @@ +import { create } from './createUser'; +import { IUser, ProgressHookType, SignerType } from '../types'; +import { profileUpdate } from './profile.updateUser'; +import { decryptPGPKey } from '../../../src/lib/helpers'; +import Constants, { ENV } from '../constants'; + +export type CreateUserPropsWithProfile = { + env?: ENV; + account?: string; + signer?: SignerType; + version?: typeof Constants.ENC_TYPE_V1 | typeof Constants.ENC_TYPE_V3; + additionalMeta?: { + NFTPGP_V1?: { + password: string; + }; + }; + profile?: { + name?: string; + desc?: string; + picture?: string; + blockedUsersList?: Array; + }; + progressHook?: (progress: ProgressHookType) => void; +}; + +export const createUserWithProfile = async ( + userOptions: CreateUserPropsWithProfile +): Promise => { + try { + let user = await create(userOptions); + + if (userOptions.profile) { + const pk = await decryptPGPKey({ + account: user.did, + encryptedPGPPrivateKey: user.encryptedPrivateKey, + env: userOptions.env, + signer: userOptions.signer, + }); + + + user = await profileUpdate({ + account: user.did, + env: userOptions.env, + pgpPrivateKey: pk, + profile: userOptions.profile + }); + } + return user; + } catch (err) { + throw new Error(`[Push SDK] - Error in createUserWithProfile -: ${err}`); + } +}; diff --git a/packages/d-node-notif/src/lib/user/decryptAuth.ts b/packages/d-node-notif/src/lib/user/decryptAuth.ts new file mode 100644 index 000000000..47f6c42c5 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/decryptAuth.ts @@ -0,0 +1,68 @@ +import Constants, { ENV } from '../constants'; +import { decryptPGPKey, isValidNFTCAIP } from '../helpers'; +import PROGRESSHOOK from '../progressHook'; +import { + ProgressHookType, + ProgressHookTypeFunction, + SignerType, +} from '../types'; + +type decryptAuthProps = { + signer: SignerType; + account?: string; + env?: ENV; + additionalMeta?: { + NFTPGP_V1?: { + encryptedPassword: string; + }; + }; + /** + * To get Progress Related to fn + */ + progressHook?: (progress: ProgressHookType) => void; +}; + +/** + * + * @returns Decrypted Push Profile Password + */ +export const decryptAuth = async ( + options: decryptAuthProps +): Promise => { + const { + account, + signer, + env = Constants.ENV.PROD, + additionalMeta, + progressHook, + } = options || {}; + try { + if (!isValidNFTCAIP(account as string)) { + return null; + } + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-DECRYPT-AUTH-01'] as ProgressHookType); + const password = await decryptPGPKey({ + encryptedPGPPrivateKey: additionalMeta?.NFTPGP_V1 + ?.encryptedPassword as string, + signer, + account, + env, + }); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-DECRYPT-AUTH-02'] as ProgressHookType); + return password; + } catch (err) { + // Report Progress + progressHook?.( + (PROGRESSHOOK['PUSH-ERROR-00'] as ProgressHookTypeFunction)( + decryptAuth.name, + err + ) + ); + throw Error( + `[Push SDK] - API - Error - API ${decryptAuth.name} -: ${err}` + ); + } +}; diff --git a/packages/d-node-notif/src/lib/user/getDelegations.ts b/packages/d-node-notif/src/lib/user/getDelegations.ts new file mode 100644 index 000000000..fe7fc175a --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getDelegations.ts @@ -0,0 +1,40 @@ +import { + getCAIPAddress, + getAPIBaseUrls +} from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; + +/** + * GET /users/:userAddressInCAIP/delegations + */ + +export type UserDelegationsOptionsType = { + /** wallet address of user */ + user: string; + env?: ENV; +} + +/** + * Returns the list of channels that the user has been delegated to + */ + +export const getDelegations = async ( + options : UserDelegationsOptionsType +) => { + const { + user, + env = Constants.ENV.PROD, + } = options || {}; + + const _user = await getCAIPAddress(env, user, 'User'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/users/${_user}/delegations`; + const requestUrl = `${apiEndpoint}`; + + return axiosGet(requestUrl) + .then((response) => response.data?.delegations || []) + .catch((err) => { + console.error(`[EPNS-SDK] - API ${requestUrl}: `, err); + }); +} diff --git a/packages/d-node-notif/src/lib/user/getFeeds.ts b/packages/d-node-notif/src/lib/user/getFeeds.ts new file mode 100644 index 000000000..4317838f8 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getFeeds.ts @@ -0,0 +1,53 @@ +import { + getCAIPAddress, + getAPIBaseUrls, + getQueryParams, + getLimit, +} from '../helpers'; +import Constants, {ENV} from '../constants'; +import { parseApiResponse } from '../utils'; +import { axiosGet } from '../utils/axiosUtil'; + +export type FeedsOptionsType = { + user: string; + env?: ENV; + page?: number; + limit?: number; + spam?: boolean; + raw?: boolean; +} + +export const getFeeds = async ( + options: FeedsOptionsType +) => { + const { + user, + env = Constants.ENV.PROD, + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + spam = false, + raw = false, + } = options || {}; + + const _user = await getCAIPAddress(env, user, 'User'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/users/${_user}/feeds`; + + const queryObj = { + page, + limit: getLimit(limit), + spam + }; + + const requestUrl = `${apiEndpoint}?${getQueryParams(queryObj)}`; + return axiosGet(requestUrl) + .then((response) => { + if (raw) { + return response?.data?.feeds || []; + } + return parseApiResponse(response?.data?.feeds) || []; + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + }); +} diff --git a/packages/d-node-notif/src/lib/user/getFeedsPerChannel.ts b/packages/d-node-notif/src/lib/user/getFeedsPerChannel.ts new file mode 100644 index 000000000..a82dcc3e2 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getFeedsPerChannel.ts @@ -0,0 +1,58 @@ +import { + getCAIPAddress, + getAPIBaseUrls, + getQueryParams, + getLimit, +} from '../helpers'; +import Constants, { ENV } from '../constants'; +import { parseApiResponse } from '../utils'; +import { axiosGet } from '../utils/axiosUtil'; + +export type FeedsPerChannelOptionsType = { + user: string; + env?: ENV; + channels?: string[]; + page?: number; + limit?: number; + spam?: boolean; + raw?: boolean; +}; + +export const getFeedsPerChannel = async ( + options: FeedsPerChannelOptionsType +) => { + const { + user, + env = Constants.ENV.PROD, + page = Constants.PAGINATION.INITIAL_PAGE, + limit = Constants.PAGINATION.LIMIT, + spam = false, + raw = false, + channels = [], + } = options || {}; + + const _user = await getCAIPAddress(env, user, 'User'); + const API_BASE_URL = getAPIBaseUrls(env); + if (channels.length == 0) { + throw new Error('channels cannot be empty'); + } + const _channel = await getCAIPAddress(env, channels[0], 'Channel'); + const apiEndpoint = `${API_BASE_URL}/v1/users/${_user}/channels/${_channel}/feeds`; + const queryObj = { + page, + limit: getLimit(limit), + spam, + }; + + const requestUrl = `${apiEndpoint}?${getQueryParams(queryObj)}`; + return axiosGet(requestUrl) + .then((response) => { + if (raw) { + return response?.data?.feeds || []; + } + return parseApiResponse(response?.data?.feeds) || []; + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + }); +}; diff --git a/packages/d-node-notif/src/lib/user/getSubscriptions.ts b/packages/d-node-notif/src/lib/user/getSubscriptions.ts new file mode 100644 index 000000000..d67d2f994 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getSubscriptions.ts @@ -0,0 +1,45 @@ +import { getCAIPAddress, getAPIBaseUrls, getQueryParams } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { axiosGet } from '../utils/axiosUtil'; +import { parseSubscriptionsApiResponse } from '../utils/pasreSubscriptionAPI'; + +export type UserSubscriptionsOptionsType = { + user: string; + env?: ENV; + channel?: string | null; + raw?: boolean; +}; + +export const getSubscriptions = async ( + options: UserSubscriptionsOptionsType +) => { + const { + user, + env = Constants.ENV.PROD, + channel = null, + raw = true, + } = options || {}; + + const _user = await getCAIPAddress(env, user, 'User'); + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v1/users/${_user}/subscriptions`; + const query = channel? getQueryParams({ + channel: channel + }): "" + const requestUrl = `${apiEndpoint}?${query}`; + + + return axiosGet(requestUrl) + .then((response) => { + if (raw) { + return response.data?.subscriptions || []; + } else { + return parseSubscriptionsApiResponse( + response.data?.subscriptions || [] + ); + } + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + }); +}; diff --git a/packages/d-node-notif/src/lib/user/getUser.ts b/packages/d-node-notif/src/lib/user/getUser.ts new file mode 100644 index 000000000..ee67b568b --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getUser.ts @@ -0,0 +1,33 @@ +import { AccountEnvOptionsType, IUser } from '../types'; +import { isValidPushCAIP, walletToPCAIP10 } from '../helpers/address'; +import { getAPIBaseUrls, verifyProfileKeys } from '../helpers'; +import Constants from '../constants'; +import { populateDeprecatedUser } from '../utils/populateIUser'; +import { axiosGet } from '../utils/axiosUtil'; + +export const get = async (options: AccountEnvOptionsType): Promise => { + const { account, env = Constants.ENV.PROD } = options || {}; + if (!isValidPushCAIP(account)) { + throw new Error(`Invalid address!`); + } + const caip10 = walletToPCAIP10(account); + const API_BASE_URL = getAPIBaseUrls(env); + const requestUrl = `${API_BASE_URL}/v2/users/?caip10=${caip10}`; + return axiosGet(requestUrl) + .then(async (response) => { + if (response.data) { + response.data.publicKey = await verifyProfileKeys( + response.data.encryptedPrivateKey, + response.data.publicKey, + response.data.did, + response.data.wallets, + response.data.verificationProof + ); + } + return populateDeprecatedUser(response.data); + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + throw Error(`[Push SDK] - API ${requestUrl}: ${err}`); + }); +}; diff --git a/packages/d-node-notif/src/lib/user/getUsersBatch.ts b/packages/d-node-notif/src/lib/user/getUsersBatch.ts new file mode 100644 index 000000000..03b3b05a0 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/getUsersBatch.ts @@ -0,0 +1,56 @@ +import { IUser } from '../types'; +import { isValidPushCAIP, walletToPCAIP10 } from '../helpers/address'; +import { getAPIBaseUrls, verifyProfileKeys } from '../helpers'; +import Constants, { ENV } from '../constants'; +import { populateDeprecatedUser } from '../utils/populateIUser'; +import { axiosPost } from '../utils/axiosUtil'; + +export interface GetBatchType { + userIds: string[]; + env?: ENV; +} + +export const getBatch = async (options: GetBatchType): Promise => { + const { env = Constants.ENV.PROD, userIds } = options || {}; + + const API_BASE_URL = getAPIBaseUrls(env); + const requestUrl = `${API_BASE_URL}/v2/users/batch`; + + const MAX_USER_IDS_LENGTH = 100; + if (userIds.length > MAX_USER_IDS_LENGTH) { + throw new Error( + `Too many user IDs. Maximum allowed: ${MAX_USER_IDS_LENGTH}` + ); + } + + for (let i = 0; i < userIds.length; i++) { + if (!isValidPushCAIP(userIds[i])) { + throw new Error(`Invalid user address!`); + } + } + + const pcaipUserIds = userIds.map(walletToPCAIP10); + const requestBody = { userIds: pcaipUserIds }; + + return axiosPost(requestUrl, requestBody) + .then((response) => { + response.data.users.forEach(async (user: any, index: number) => { + response.data.users[index].publicKey = await verifyProfileKeys( + user.encryptedPrivateKey, + user.publicKey, + user.did, + user.caip10, + user.verificationProof + ); + + response.data.users[index] = populateDeprecatedUser( + response.data.users[index] + ); + }); + return response.data; + }) + .catch((err) => { + console.error(`[Push SDK] - API ${requestUrl}: `, err); + throw Error(`[Push SDK] - API ${requestUrl}: ${err}`); + }); +}; diff --git a/packages/d-node-notif/src/lib/user/index.ts b/packages/d-node-notif/src/lib/user/index.ts new file mode 100644 index 000000000..edcd89cb0 --- /dev/null +++ b/packages/d-node-notif/src/lib/user/index.ts @@ -0,0 +1,21 @@ +import { authUpdate } from './auth.updateUser'; +import { profileUpdate, profileUpdateCore } from './profile.updateUser'; +export type { ProfileUpdateProps } from './profile.updateUser'; +export * from './createUser'; +export * from './getFeeds'; +export * from './getSubscriptions'; +export * from './getUser'; +export * from './getDelegations'; +export * from './getUsersBatch'; +export * from './upgradeUser'; +export * from './decryptAuth'; +export * from './createUserWithProfile'; +export * from './getFeedsPerChannel'; + +export const auth = { + update: authUpdate, +}; +export const profile = { + update: profileUpdate, + updateCore: profileUpdateCore, +}; diff --git a/packages/d-node-notif/src/lib/user/profile.updateUser.ts b/packages/d-node-notif/src/lib/user/profile.updateUser.ts new file mode 100644 index 000000000..e33d76bad --- /dev/null +++ b/packages/d-node-notif/src/lib/user/profile.updateUser.ts @@ -0,0 +1,133 @@ +import * as CryptoJS from 'crypto-js'; +import { IPGPHelper, PGPHelper } from '../chat/helpers'; +import Constants, { ENV } from '../constants'; +import { + convertToValidDID, + getAPIBaseUrls, + isValidPushCAIP, + verifyProfileKeys, +} from '../helpers'; +import { IUser, ProgressHookType, ProgressHookTypeFunction } from '../types'; +import { get } from './getUser'; +import { populateDeprecatedUser } from '../utils/populateIUser'; +import PROGRESSHOOK from '../progressHook'; +import { axiosPut } from '../utils/axiosUtil'; + +export type ProfileUpdateProps = { + /** + * PGP Private Key + */ + pgpPrivateKey: string; + /** + * DID + */ + account: string; + /** + * Profile properties that can be changed + */ + profile: { + name?: string; + desc?: string; + picture?: string; + blockedUsersList?: Array; + }; + env?: ENV; + progressHook?: (progress: ProgressHookType) => void; +}; + +/** + * Updation of profile + */ +export const profileUpdate = async ( + options: ProfileUpdateProps +): Promise => { + return profileUpdateCore(options, PGPHelper); +}; + +export const profileUpdateCore = async ( + options: ProfileUpdateProps, + pgpHelper: IPGPHelper +): Promise => { + const { + pgpPrivateKey, + account, + profile, + env = Constants.ENV.PROD, + progressHook, + } = options || {}; + try { + if (!isValidPushCAIP(account)) { + throw new Error(`Invalid account!`); + } + + const user = await get({ account, env }); + if (!user || !user.did) { + throw new Error('User not Found!'); + } + let blockedUsersList = null; + if (profile.blockedUsersList) { + for (const element of profile.blockedUsersList) { + // Check if the element is a valid CAIP-10 address + if (!isValidPushCAIP(element)) { + throw new Error( + 'Invalid address in the blockedUsersList: ' + element + ); + } + } + + const convertedBlockedListUsersPromise = profile.blockedUsersList.map( + async (each) => { + return convertToValidDID(each, env); + } + ); + blockedUsersList = await Promise.all(convertedBlockedListUsersPromise); + + blockedUsersList = Array.from(new Set(blockedUsersList)); + } + + const updatedProfile = { + name: profile.name ? profile.name : user.profile.name, + desc: profile.desc ? profile.desc : user.profile.desc, + picture: profile.picture ? profile.picture : user.profile.picture, + // If profile.blockedUsersList is empty no users in block list + blockedUsersList: profile.blockedUsersList ? blockedUsersList : [], + }; + const hash = CryptoJS.SHA256(JSON.stringify(updatedProfile)).toString(); + const signature = await pgpHelper.sign({ + message: hash, + signingKey: pgpPrivateKey, + }); + const sigType = 'pgpv2'; + const verificationProof = `${sigType}:${signature}`; + + const body = { ...updatedProfile, verificationProof }; + + const API_BASE_URL = getAPIBaseUrls(env); + const apiEndpoint = `${API_BASE_URL}/v2/users/${user.did}/profile`; + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-PROFILE-UPDATE-01'] as ProgressHookType); + const response = await axiosPut(apiEndpoint, body); + if (response.data) + response.data.publicKey = await verifyProfileKeys( + response.data.encryptedPrivateKey, + response.data.publicKey, + response.data.did, + response.data.wallets, + response.data.verificationProof + ); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-PROFILE-UPDATE-02'] as ProgressHookType); + return populateDeprecatedUser(response.data); + } catch (err) { + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-00' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(profileUpdate.name, err)); + throw Error( + `[Push SDK] - API - Error - API ${profileUpdate.name} -: ${err}` + ); + } +}; diff --git a/packages/d-node-notif/src/lib/user/upgradeUser.ts b/packages/d-node-notif/src/lib/user/upgradeUser.ts new file mode 100644 index 000000000..9df50d84a --- /dev/null +++ b/packages/d-node-notif/src/lib/user/upgradeUser.ts @@ -0,0 +1,97 @@ +import { getAccountAddress, getWallet } from '../chat/helpers'; +import Constants, { ENV } from '../constants'; +import { isValidPushCAIP, decryptPGPKey } from '../helpers'; +import { + SignerType, + IUser, + ProgressHookType, + ProgressHookTypeFunction, +} from '../types'; +import { authUpdate } from './auth.updateUser'; +import { get } from './getUser'; +import PROGRESSHOOK from '../progressHook'; + +export type UpgradeUserProps = { + env?: ENV; + account?: string; + signer: SignerType; + additionalMeta?: { + NFTPGP_V1?: { + password: string; + }; + }; + progressHook?: (progress: ProgressHookType) => void; +}; + +/** + * Upgrades the Push Profile keys from current version to recommended version + * @param options + * @returns + */ +export const upgrade = async (options: UpgradeUserProps): Promise => { + const { + env = Constants.ENV.PROD, + account = null, + signer, + additionalMeta, + progressHook, + } = options || {}; + + try { + const wallet = getWallet({ account, signer }); + const address = await getAccountAddress(wallet); + + if (!isValidPushCAIP(address)) { + throw new Error(`Invalid address!`); + } + + const user: IUser = await get({ account: address, env: env }); + + if (!user || !user.encryptedPrivateKey) { + throw new Error('User Not Found!'); + } + + const recommendedPgpEncryptionVersion = + Constants.USER.ENCRYPTION_TYPE.PGP_V3; + const { version } = JSON.parse(user.encryptedPrivateKey); + + if ( + version === recommendedPgpEncryptionVersion || + version === Constants.USER.ENCRYPTION_TYPE.NFTPGP_V1 + ) { + return user; + } + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-UPGRADE-02'] as ProgressHookType); + const pgpPrivateKey = await decryptPGPKey({ + encryptedPGPPrivateKey: user.encryptedPrivateKey, + signer: signer, + env, + toUpgrade: false, + additionalMeta, + }); + + const upgradedUser = await authUpdate({ + pgpPrivateKey, // decrypted pgp priv key + pgpEncryptionVersion: recommendedPgpEncryptionVersion, + signer, + pgpPublicKey: user.publicKey, + account: user.did, + env, + additionalMeta: additionalMeta, + progressHook: progressHook, + }); + + // Report Progress + progressHook?.(PROGRESSHOOK['PUSH-UPGRADE-05'] as ProgressHookType); + return upgradedUser; + } catch (err) { + // Report Progress + const errorProgressHook = PROGRESSHOOK[ + 'PUSH-ERROR-00' + ] as ProgressHookTypeFunction; + progressHook?.(errorProgressHook(upgrade.name, err)); + throw Error(`[Push SDK] - API - Error - API ${upgrade.name} -: ${err}`); + } +}; diff --git a/packages/d-node-notif/src/lib/utils/axiosUtil.ts b/packages/d-node-notif/src/lib/utils/axiosUtil.ts new file mode 100644 index 000000000..9f1db0ba9 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/axiosUtil.ts @@ -0,0 +1,67 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require('../../../../restapi/package.json'); +const version = packageJson.version; + +const addSdkVersionHeader = ( + config?: AxiosRequestConfig +): AxiosRequestConfig => { + const headers = { ...config?.headers, 'X-JS-SDK-VERSION': version }; + return { ...config, headers }; +}; + +const checkForDeprecationHeader = ( + response: AxiosResponse +): AxiosResponse => { + const deprecationNotice = response.headers['x-deprecation-notice']; + if (deprecationNotice) { + const method = response.config.method?.toUpperCase(); + const path = response.config.url; + console.warn( + `%cDeprecation Notice%c Method: ${method}, Path: ${path}, Notice: ${deprecationNotice}`, + 'color: white; background-color: red; font-weight: bold; padding: 2px 4px;', + 'color: red; font-weight: bold;' + ); + } + return response; +}; + +const axiosGet = async ( + url: string, + config?: AxiosRequestConfig +): Promise> => { + return axios + .get(url, addSdkVersionHeader(config)) + .then((response) => checkForDeprecationHeader(response)); +}; + +const axiosPost = async ( + url: string, + data: any, + config?: AxiosRequestConfig +): Promise> => { + return axios + .post(url, data, addSdkVersionHeader(config)) + .then((response) => checkForDeprecationHeader(response)); +}; + +const axiosPut = async ( + url: string, + data: any, + config?: AxiosRequestConfig +): Promise> => { + return axios + .put(url, data, addSdkVersionHeader(config)) + .then((response) => checkForDeprecationHeader(response)); +}; + +const axiosDelete = async ( + url: string, + config?: AxiosRequestConfig +): Promise> => { + return axios + .delete(url, addSdkVersionHeader(config)) + .then((response) => checkForDeprecationHeader(response)); +}; + +export { axiosGet, axiosPost, axiosPut, axiosDelete }; diff --git a/packages/d-node-notif/src/lib/utils/index.ts b/packages/d-node-notif/src/lib/utils/index.ts new file mode 100644 index 000000000..476b78a58 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/index.ts @@ -0,0 +1,4 @@ +/** + * Only externally used Helpers + */ +export * from './parseAPI'; \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/utils/parseAPI.ts b/packages/d-node-notif/src/lib/utils/parseAPI.ts new file mode 100644 index 000000000..cb4f5ac07 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/parseAPI.ts @@ -0,0 +1,42 @@ +import { ApiNotificationType, ParsedResponseType } from '../types'; + +/** + * @description parse the response gotten from the API + * @param {ApiNotificationType[]} response + * @returns {ParsedResponseType[]} + */ +export function parseApiResponse(response: ApiNotificationType[]): ParsedResponseType[] { + return response.map((apiNotification: ApiNotificationType) => { + const { + payload: { + data: { + acta: cta = "", + amsg: bigMessage = "", + asub = "", + icon = "", + url = "", + sid = "", + app = "", + aimg = "", + secret = "" + }, + notification, + }, + source, + } = apiNotification; + + return { + cta, + title: asub || '', + message: bigMessage || notification.body || '', + icon, + url, + sid, + app, + image: aimg, + blockchain: source, + notification, + secret + }; + }); +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/utils/parseSettings.ts b/packages/d-node-notif/src/lib/utils/parseSettings.ts new file mode 100644 index 000000000..3152a4363 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/parseSettings.ts @@ -0,0 +1,66 @@ +import { NotificationSettingType } from '../types/index'; + +export const parseSettings = (settings: any): NotificationSettingType[] => { + let settingsObj; + try { + settingsObj = JSON.parse(settings); + } catch (error) { + settingsObj = settings; + } + const parsedSettings: NotificationSettingType[] = []; + for (let i = 0; i < settingsObj.length; i++) { + const setting = settingsObj[i]; + if (setting.type == 1) { + parsedSettings.push({ + type: setting.type, + description: setting.description, + ...(setting.user + ? { + userPreferance: { + value: setting.user, + enabled: setting.user, + }, + } + : { default: setting.default }), + }); + } else if (setting.type == 2) { + parsedSettings.push({ + type: setting.type, + description: setting.description, + data: { + upper: setting.upperLimit, + lower: setting.lowerLimit, + ticker: setting.ticker ?? 1, + }, + ...(setting.user + ? { + userPreferance: { + value: setting.user, + enabled: setting.enabled, + }, + } + : { default: setting.default }), + }); + } else if (setting.type == 3) { + parsedSettings.push({ + type: setting.type, + description: setting.description, + data: { + upper: setting.upperLimit, + lower: setting.lowerLimit, + ticker: setting.ticker ?? 1, + }, + ...(setting.user + ? { + userPreferance: { + value: setting.user, + enabled: setting.enabled, + }, + } + : { default: setting.default }), + }); + } + } + + return parsedSettings; +}; diff --git a/packages/d-node-notif/src/lib/utils/parseSubscribersAPI.ts b/packages/d-node-notif/src/lib/utils/parseSubscribersAPI.ts new file mode 100644 index 000000000..11d3331d9 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/parseSubscribersAPI.ts @@ -0,0 +1,24 @@ +import { ApiSubscribersType, NotificationSettingType } from '../types'; +import { parseSettings } from './parseSettings'; +/** + * @description parse the response gotten from the API + * @param {ApiSubscribersType[]} response + * @returns {NotificationSettingType[]} + */ + +export type SubscriberResponse = { + itemcount: number; + subscribers: {subscriber: string, settings: NotificationSettingType[] | null}[] +} +export function parseSubscrbersApiResponse(response: ApiSubscribersType):SubscriberResponse { + const parsedSubscribers = response.subscribers.map((apisubscribers: {subscriber: string, settings: string| null}) => { + return { + subscriber: apisubscribers.subscriber, + settings: apisubscribers.settings? parseSettings(apisubscribers.settings): null + } + }); + return { + itemcount: response.itemcount, + subscribers: [...parsedSubscribers] + } +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/utils/pasreSubscriptionAPI.ts b/packages/d-node-notif/src/lib/utils/pasreSubscriptionAPI.ts new file mode 100644 index 000000000..6216c5023 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/pasreSubscriptionAPI.ts @@ -0,0 +1,20 @@ +import { ApiSubscriptionType, NotificationSettingType } from '../types'; +import { parseSettings } from './parseSettings'; +/** + * @description parse the response gotten from the API + * @param {ApiSubscriptionType[]} response + * @returns {SubscriptionResponse[]} + */ + +export type SubscriptionResponse = { + channel: string, + user_settings: NotificationSettingType[] | null +} +export function parseSubscriptionsApiResponse(response: ApiSubscriptionType[]):SubscriptionResponse[] { + return response.map((apisubscription: ApiSubscriptionType) => { + return { + channel: apisubscription.channel, + user_settings: apisubscription.user_settings? parseSettings(apisubscription.user_settings): null + } + }); +} \ No newline at end of file diff --git a/packages/d-node-notif/src/lib/utils/populateIUser.ts b/packages/d-node-notif/src/lib/utils/populateIUser.ts new file mode 100644 index 000000000..e8cfe6886 --- /dev/null +++ b/packages/d-node-notif/src/lib/utils/populateIUser.ts @@ -0,0 +1,39 @@ +import { IUser } from '../types'; + +/** + * To be removed in v2 verisons of sdk + * @param user + * @returns User with deprecated params + */ +export const populateDeprecatedUser = (user: IUser): IUser => { + if (!user) return user; + user.name = user.profile.name; + user.about = user.profile.desc; + user.profilePicture = user.profile.picture; + user.numMsg = user.msgSent; + user.allowedNumMsg = user.maxMsgPersisted; + let encryptionType = ''; + let sigType = ''; + let signature = ''; + try { + const { version } = JSON.parse(user.encryptedPrivateKey); + encryptionType = version; + } catch (err) { + //ignore since no encryption found + } + user.encryptionType = encryptionType; + try { + sigType = user.verificationProof.split(':')[0]; + signature = user.verificationProof.split(':')[1]; + } catch (err) { + //ignore since no verification proof found + } + user.signature = signature; + user.sigType = sigType; + user.encryptedPassword = null; + //TODO FOR NFT PROFILE + user.nftOwner = null; + user.linkedListHash = null; + user.nfts = null; + return user; +}; diff --git a/packages/d-node-notif/tests/.env.sample b/packages/d-node-notif/tests/.env.sample new file mode 100644 index 000000000..1111df0ea --- /dev/null +++ b/packages/d-node-notif/tests/.env.sample @@ -0,0 +1,14 @@ +# MAKE A COPY OF THIS AND FILL WITH YOUR CREDENTIALS AND NAME IT .env (Remove .sample Part) + +# ENVIRONMENT | 'STAGING' or 'PROD' or 'DEV' +ENV=env_name + +## CHANNEL WITH ALIAS +BERACHAIN_CHANNEL_PRIVATE_KEY=your_berachain_channel_private_key +ARBITRUM_CHANNEL_PRIVATE_KEY=your_arbitrum_channel_private_key +OPTIMISM_CHANNEL_PRIVATE_KEY=your_optimism_channel_private_key +POLYGON_CHANNEL_PRIVATE_KEY=your_polygon_channel_private_key +POLYGON_ZKEVM_CHANNEL_PRIVATE_KEY=your_polygon_zkevm_channel_private_key + +WALLET_PRIVATE_KEY=your_wallet_private_key + diff --git a/packages/d-node-notif/tests/lib/aliasChains/arbitrum.test.ts b/packages/d-node-notif/tests/lib/aliasChains/arbitrum.test.ts new file mode 100644 index 000000000..9aaddb592 --- /dev/null +++ b/packages/d-node-notif/tests/lib/aliasChains/arbitrum.test.ts @@ -0,0 +1,87 @@ +import { ethers } from 'ethers'; +import { PushAPI } from '../../../src'; +import { ENV } from '../../../src/lib/constants'; + +describe('ARBITRUM ALIAS functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let account: string; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + before(async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'https://sepolia-rollup.arbitrum.io/rpc' + ); + const signer = new ethers.Wallet( + `0x${process.env['ARBITRUM_CHANNEL_PRIVATE_KEY']}`, + provider + ); + account = signer.address; + userAlice = await PushAPI.initialize(signer, { + env: _env, + }); + + const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + account2 = signer2.address; + userBob = await PushAPI.initialize(signer2, { env: _env }); + }); + + it.skip('Should be able to create channel', async () => { + const channelInfo = await userAlice.channel.info(); + if (channelInfo) return; // skip if already exists + const res = await userAlice.channel.create({ + name: 'SDK Alias Test', + description: 'Testing using sdk', + url: 'https://push.org', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + alias: `eip155:421614:${account}`, + progressHook: (progress: any) => console.log(progress), + }); + console.log(res); + }); + + it('Should be able to send notifications', async () => { + await userAlice.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:421614:${account}`, + }); + }); + + it('Should be able to add delegatee', async () => { + await userAlice.channel.delegate.add(account2); + }); + + it('Should be able to send notifications from delegate', async () => { + await userBob.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:421614:${account}`, + }); + }); + + it('Should be able to remove delegatee', async () => { + await userAlice.channel.delegate.remove(account2); + }); +}); diff --git a/packages/d-node-notif/tests/lib/aliasChains/berachain.test.ts b/packages/d-node-notif/tests/lib/aliasChains/berachain.test.ts new file mode 100644 index 000000000..9dcce6bf5 --- /dev/null +++ b/packages/d-node-notif/tests/lib/aliasChains/berachain.test.ts @@ -0,0 +1,87 @@ +import { ethers } from 'ethers'; +import { PushAPI } from '../../../src'; +import { ENV } from '../../../src/lib/constants'; + +describe('BERACHAIN ALIAS functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let account: string; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + before(async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'https://artio.rpc.berachain.com' // berachain artio Provider + ); + const signer = new ethers.Wallet( + `0x${process.env['BERACHAIN_CHANNEL_PRIVATE_KEY']}`, + provider + ); + account = signer.address; + userAlice = await PushAPI.initialize(signer, { + env: _env, + }); + + const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + account2 = signer2.address; + userBob = await PushAPI.initialize(signer2, { env: _env }); + }); + + it.skip('Should be able to create channel', async () => { + const channelInfo = await userAlice.channel.info(); + if (channelInfo) return; // skip if already exists + const res = await userAlice.channel.create({ + name: 'SDK Alias Test', + description: 'Testing using sdk', + url: 'https://push.org', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + alias: `eip155:80085:${account}`, + progressHook: (progress: any) => console.log(progress), + }); + console.log(res); + }); + + it('Should be able to send notifications', async () => { + await userAlice.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:80085:${account}`, + }); + }); + + it('Should be able to add delegatee', async () => { + await userAlice.channel.delegate.add(account2); + }); + + it('Should be able to send notifications from delegate', async () => { + await userBob.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:80085:${account}`, + }); + }); + + it('Should be able to remove delegatee', async () => { + await userAlice.channel.delegate.remove(account2); + }); +}); diff --git a/packages/d-node-notif/tests/lib/aliasChains/optimism.test.ts b/packages/d-node-notif/tests/lib/aliasChains/optimism.test.ts new file mode 100644 index 000000000..a18d61d41 --- /dev/null +++ b/packages/d-node-notif/tests/lib/aliasChains/optimism.test.ts @@ -0,0 +1,87 @@ +import { ethers } from 'ethers'; +import { PushAPI } from '../../../src'; +import { ENV } from '../../../src/lib/constants'; + +describe('OPTIMISM ALIAS functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let account: string; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + before(async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'https://sepolia.optimism.io' + ); + const signer = new ethers.Wallet( + `0x${process.env['OPTIMISM_CHANNEL_PRIVATE_KEY']}`, + provider + ); + account = signer.address; + userAlice = await PushAPI.initialize(signer, { + env: _env, + }); + + const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + account2 = signer2.address; + userBob = await PushAPI.initialize(signer2, { env: _env }); + }); + + it.skip('Should be able to create channel', async () => { + const channelInfo = await userAlice.channel.info(); + if (channelInfo) return; // skip if already exists + const res = await userAlice.channel.create({ + name: 'SDK Alias Test', + description: 'Testing using sdk', + url: 'https://push.org', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + alias: `eip155:11155420:${account}`, + progressHook: (progress: any) => console.log(progress), + }); + console.log(res); + }); + + it('Should be able to send notifications', async () => { + await userAlice.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:11155420:${account}`, + }); + }); + + it('Should be able to add delegatee', async () => { + await userAlice.channel.delegate.add(account2); + }); + + it('Should be able to send notifications from delegate', async () => { + await userBob.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:11155420:${account}`, + }); + }); + + it('Should be able to remove delegatee', async () => { + await userAlice.channel.delegate.remove(account2); + }); +}); diff --git a/packages/d-node-notif/tests/lib/aliasChains/polygon.test.ts b/packages/d-node-notif/tests/lib/aliasChains/polygon.test.ts new file mode 100644 index 000000000..5aa2cfc2e --- /dev/null +++ b/packages/d-node-notif/tests/lib/aliasChains/polygon.test.ts @@ -0,0 +1,84 @@ +import { ethers } from 'ethers'; +import { PushAPI } from '../../../src'; +import { ENV } from '../../../src/lib/constants'; + +describe('POLYGON ALIAS functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let account: string; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + before(async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'https://rpc-amoy.polygon.technology/' + ); + const signer = new ethers.Wallet( + `0x${process.env['POLYGON_CHANNEL_PRIVATE_KEY']}`, + provider + ); + account = signer.address; + userAlice = await PushAPI.initialize(signer, { + env: _env, + }); + + const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + account2 = signer2.address; + userBob = await PushAPI.initialize(signer2, { env: _env }); + }); + + it.skip('Should be able to create channel', async () => { + const res = await userAlice.channel.create({ + name: 'SDK Alias Test', + description: 'Testing using sdk', + url: 'https://push.org', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + alias: `eip155:80002:${account}`, + progressHook: (progress: any) => console.log(progress), + }); + }); + + it('Should be able to send notifications', async () => { + await userAlice.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:80002:${account}`, + }); + }); + + it('Should be able to add delegatee', async () => { + await userAlice.channel.delegate.add(account2); + }); + + it('Should be able to send notifications from delegate', async () => { + await userBob.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:80002:${account}`, + }); + }); + + it('Should be able to remove delegatee', async () => { + await userAlice.channel.delegate.remove(account2); + }); +}); diff --git a/packages/d-node-notif/tests/lib/aliasChains/polygonZkevm.test.ts b/packages/d-node-notif/tests/lib/aliasChains/polygonZkevm.test.ts new file mode 100644 index 000000000..bc56975fe --- /dev/null +++ b/packages/d-node-notif/tests/lib/aliasChains/polygonZkevm.test.ts @@ -0,0 +1,85 @@ +import { ethers } from 'ethers'; +import { PushAPI } from '../../../src'; +import { ENV } from '../../../src/lib/constants'; + +describe('POLYGON ZKEVM ALIAS functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let account: string; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + before(async () => { + const provider = new ethers.providers.JsonRpcProvider( + 'https://rpc.cardona.zkevm-rpc.com/' + ); + const signer = new ethers.Wallet( + `0x${process.env['POLYGON_ZKEVM_CHANNEL_PRIVATE_KEY']}`, + provider + ); + account = signer.address; + userAlice = await PushAPI.initialize(signer, { + env: _env, + }); + + const signer2 = new ethers.Wallet(ethers.Wallet.createRandom().privateKey); + account2 = signer2.address; + userBob = await PushAPI.initialize(signer2, { env: _env }); + }); + + it.skip('Should be able to create channel', async () => { + const res = await userAlice.channel.create({ + name: 'SDK Alias Test', + description: 'Testing using sdk', + url: 'https://push.org', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + alias: `eip155:2442:${account}`, + progressHook: (progress: any) => console.log(progress), + }); + console.log(res); + }); + + it('Should be able to send notifications', async () => { + await userAlice.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:2442:${account}`, + }); + }); + + it('Should be able to add delegatee', async () => { + await userAlice.channel.delegate.add(account2); + }); + + it('Should be able to send notifications from delegate', async () => { + await userBob.channel.send(['*'], { + notification: { + title: 'hi', + body: 'test-broadcast', + }, + payload: { + title: 'testing broadcast notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:2442:${account}`, + }); + }); + + it('Should be able to remove delegatee', async () => { + await userAlice.channel.delegate.remove(account2); + }); +}); diff --git a/packages/d-node-notif/tests/lib/notification/alias.test.ts b/packages/d-node-notif/tests/lib/notification/alias.test.ts new file mode 100644 index 000000000..08f4b798b --- /dev/null +++ b/packages/d-node-notif/tests/lib/notification/alias.test.ts @@ -0,0 +1,169 @@ +import { PushAPI } from '../../../src/lib/pushAPI/PushAPI'; +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import { bsc, bscTestnet, sepolia } from 'viem/chains'; +import { ENV } from '../../../src/lib/constants'; +import { createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; + +describe('PushAPI.alias functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let userKate: PushAPI; + let userSam: PushAPI; + let signer1: any; + let account1: string; + let signer2: any; + let viemUser: any; + let viemUser1: any; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + beforeEach(async () => { + signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); + account1 = await signer1.getAddress(); + + 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 provider1 = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider( + 'https://bsc-testnet-rpc.publicnode.com' + ) + : new (ethers as any).JsonRpcProvider( + 'https://bsc-testnet-rpc.publicnode.com' + ); + + signer2 = new ethers.Wallet( + `0x${process.env['WALLET_PRIVATE_KEY']}`, + provider + ); + + const signer3 = createWalletClient({ + account: privateKeyToAccount(`0x${process.env['WALLET_PRIVATE_KEY']}`), + chain: sepolia, + transport: http(), + }); + + const signer4 = new ethers.Wallet( + `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_1']}`, + provider1 + ); + + const signer5 = createWalletClient({ + account: privateKeyToAccount( + `0x${process.env['NFT_HOLDER_WALLET_PRIVATE_KEY_1']}` + ), + chain: bscTestnet, + transport: http(), + }); + + account2 = await signer2.getAddress(); + + // initialization with signer + userAlice = await PushAPI.initialize(signer1, { env: _env }); + + // initialization with signer and provider + userKate = await PushAPI.initialize(signer2, { env: _env }); + + //initialisation without signer + userBob = await PushAPI.initialize(signer1, { env: _env }); + + // initalisation with viem + viemUser = await PushAPI.initialize(signer3, { env: _env }); + + // initalisation the verification account + userSam = await PushAPI.initialize(signer4, { env: _env }); + viemUser1 = await PushAPI.initialize(signer5, { env: _env }); + }); + + describe('alias :: info', () => { + it('Should return response', async () => { + const res = await userBob.channel.alias.info({ + alias: '0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + aliasChain: 'POLYGON', + }); + expect(res).not.null; + }); + }); + + describe('alias :: add', () => { + // TODO: remove skip after signer becomes optional + it('Without signer and account :: should throw error', async () => { + await expect(() => + userBob.channel.alias.initiate( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and without provider :: should throw error', async () => { + await expect(() => + userAlice.channel.alias.initiate( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and provider :: should add alias', async () => { + const res = await userKate.channel.alias.initiate( + 'eip155:97:0x2dFc6E90B9Cd0Fc1E0A2da82e3b094e9369caCdB' + ); + expect(res).not.null; + }, 100000000); + + it('With signer and provider :: should add alias', async () => { + const res = await userKate.channel.alias.initiate( + 'eip155:97:0x2dFc6E90B9Cd0Fc1E0A2da82e3b094e9369caCdB', + { raw: true } + ); + expect(res).not.null; + }, 100000000); + + it('With viem signer and provider :: should add alias', async () => { + const res = await viemUser.channel.alias.initiate( + 'eip155:97:0x2dFc6E90B9Cd0Fc1E0A2da82e3b094e9369caCdB' + ); + expect(res).not.null; + }, 100000000); + + it('With signer and provider :: should throw error and provider doesnt match', async () => { + await expect(() => + userAlice.channel.alias.initiate( + 'eip155:97:0x2dFc6E90B9Cd0Fc1E0A2da82e3b094e9369caCdB' + ) + ).to.Throw; + }); + }); + + describe('alias :: verify', () => { + it('Without signer and account :: should throw error', async () => { + await expect(() => + userBob.channel.alias.verify( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and without provider :: should throw error', async () => { + await expect(() => + userAlice.channel.alias.verify( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and provider :: should verify alias', async () => { + const res = await userSam.channel.alias.verify( + '0x6eF394b8dcc840d3d65a835E371066244187B1C6', + { raw: true } + ); + expect(res).not.null; + }, 100000000); + }); +}); diff --git a/packages/d-node-notif/tests/lib/notification/channel.test.ts b/packages/d-node-notif/tests/lib/notification/channel.test.ts new file mode 100644 index 000000000..3869dd0cd --- /dev/null +++ b/packages/d-node-notif/tests/lib/notification/channel.test.ts @@ -0,0 +1,589 @@ +import { PushAPI } from '../../../src/lib/pushAPI/PushAPI'; +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import { sepolia } from 'viem/chains'; +import { + createWalletClient, + http, + getContract, + createPublicClient, +} from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import CONSTANTS from '../../../src/lib/constants'; +import { inspect } from 'util'; +import { ENV } from '../../../src/lib/constants'; + +describe('PushAPI.channel functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let userKate: PushAPI; + let signer1: any; + let account1: string; + let signer2: any; + let account2: string; + let userNoChannel: PushAPI; + let noChannelSigner: any; + let noChannelAddress: string; + let viemUser: any; + let viemSigner: any; + + beforeEach(async () => { + signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); + account1 = await signer1.getAddress(); + + 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']}`, + provider + ); + account2 = await signer2.getAddress(); + + const WALLET = ethers.Wallet.createRandom(); + noChannelSigner = new ethers.Wallet(WALLET.privateKey); + noChannelAddress = await noChannelSigner.getAddress(); + viemSigner = createWalletClient({ + account: privateKeyToAccount(`0x${process.env['WALLET_PRIVATE_KEY']}`), + chain: sepolia, + transport: http(), + }); + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + // initialisation with signer and provider + userKate = await PushAPI.initialize(signer2, { env: _env }); + // initialisation with signer + userAlice = await PushAPI.initialize(signer2, { env: _env }); + // TODO: remove signer1 after chat makes signer as optional + //initialisation without signer + userBob = await PushAPI.initialize(signer1, { env: _env }); + // initialisation with a signer that has no channel + userNoChannel = await PushAPI.initialize(noChannelSigner, { env: _env }); + // viem signer + viemUser = await PushAPI.initialize(viemSigner, { env: _env }); + }); + + describe('channel :: info', () => { + // TODO: remove skip after signer becomes optional + it.skip('Without signer and account: Should throw error', async () => { + await expect(() => userBob.channel.info()).to.Throw; + }); + + it('Without signer but with non-caip account: Should return response', async () => { + const res = await userBob.channel.info( + '0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + expect(res).not.null; + }); + + it('Without signer and with valid caip account: Should return response', async () => { + const res = await userBob.channel.info( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + { + raw: false, + } + ); + console.log(res.channel_settings); + expect(res).not.null; + }); + }); + + describe('channel :: search', () => { + it('Without signer and account : Should return response', async () => { + const res = await userBob.channel.search(' '); + // console.log(res); + expect(res).not.null; + }); + + it('With signer: Should return response', async () => { + const res = await userBob.channel.search(' '); + // console.log(res); + expect(res).not.null; + }); + + it('Should throw error for empty query', async () => { + // const res = await userBob.channel.search('') + await expect(() => userBob.channel.search('')).to.Throw; + }); + }); + + describe('channel :: subscribers', () => { + // TODO: remove skip after signer becomes optional + it.skip('Without signer and account : Should throw error', async () => { + await expect(() => userBob.channel.subscribers()).to.Throw; + }); + + it('Without signer and account : Should return response as address is passed', async () => { + const res = await userBob.channel.subscribers({ + channel: 'eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + }); + console.log(res); + expect(res).not.null; + }); + + it('Without signer and account : Should return response as address is passed', async () => { + const res = await userBob.channel.subscribers({ + channel: '0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + }); + // console.log(res) + expect(res).not.null; + }); + + it('Without signer and account : Should return response as address is passed', async () => { + const res = await userBob.channel.subscribers({ + channel: '0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + page: 1, + limit: 10, + }); + // console.log(res) + expect(res).not.null; + }); + + it('Without signer and account : Should return response for alias address', async () => { + const res = await userBob.channel.subscribers({ + channel: 'eip155:80001:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + }); + // console.log(res) + expect(res).not.null; + }); + + it('Without signer and account : Should return response without passing the options', async () => { + const res = await userKate.channel.subscribers(); + expect(res).not.null; + }); + + it('With signer and account : Should return response without passing the options', async () => { + const res = await userKate.channel.subscribers({ page: 1, limit: 10 }); + // console.log(res) + expect(res).not.null; + }); + + it('With signer and account : Should return response with settings', async () => { + const res = await userKate.channel.subscribers({ + page: 1, + limit: 10, + setting: true, + }); + console.log(res); + expect(res).not.null; + }); + + it('With signer and account : Should return response without settings', async () => { + const res = await userKate.channel.subscribers({ + page: 1, + limit: 10, + setting: false, + category: 1, + }); + // console.log(res) + expect(res).not.null; + }); + + it('With signer and account : Should return response with settings', async () => { + const res = await userKate.channel.subscribers({ + page: 1, + limit: 10, + setting: true, + raw: false, + }); + // console.log(JSON.stringify(res)) + expect(res).not.null; + }); + + it('Without signer and account : Should throw error for invalid caip', async () => { + await expect(() => + userBob.channel.subscribers({ + channel: '0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + }) + ).to.Throw; + }); + }); + + describe('channel :: send', () => { + // TODO: remove skip after signer becomes optional + it.skip('Without signer and account : Should throw error', async () => { + await expect(() => { + userBob.channel.send(['*'], { + notification: { + title: 'test', + body: 'test', + }, + }); + }).to.Throw; + }); + + it('With signer : broadcast : Should send notification with title and body', async () => { + const res = await userAlice.channel.send(['*'], { + notification: { + title: 'test', + body: 'test', + }, + }); + expect(res.status).to.equal(204); + }); + + it('With signer : targeted : Should send notification with title and body', async () => { + const res = await userAlice.channel.send( + ['eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681'], + { + notification: { + title: 'hi', + body: 'test-targeted', + }, + } + ); + expect(res.status).to.equal(204); + }); + + it('With signer : targeted : Should send notification with title and body', async () => { + const res = await userAlice.channel.send( + ['eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5'], + { + notification: { + title: 'hi', + body: 'test-targeted', + }, + } + ); + expect(res.status).to.equal(204); + }); + + it('With signer : subset : Should send notification with title and body', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-targeted', + }, + } + ); + expect(res.status).to.equal(204); + }); + + it('With signer : subset : Should send notification with title and body along with additional options', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-targeted', + }, + payload: { + title: 'testing first notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + category: 2, + }, + } + ); + expect(res.status).to.equal(204); + }); + + it('With signer : subset : Should send notification with title and body along with additional options', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-targeted', + }, + payload: { + title: 'testing first notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + category: 3, + }, + } + ); + expect(res.status).to.equal(204); + }); + + it('With signer : subset : Should send notification with title and body along with additional options', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:11155111:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-subset', + }, + payload: { + title: 'testing first subset notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + } + ); + expect(res.status).to.equal(204); + }); + + it.skip('With signer : subset : Should send notification with title and body along with additional options for alias', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:97:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:97:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-subset', + }, + payload: { + title: 'testing first subset notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:97:${account2}`, + } + ); + expect(res.status).to.equal(204); + }); + + it.skip('With signer : subset : Should send notification with title and body along with additional options for alias', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:80001:0xC8c243a4fd7F34c49901fe441958953402b7C024', + 'eip155:80001:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-subset', + }, + payload: { + title: 'testing first subset notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: `eip155:80001:${account2}`, + } + ); + expect(res.status).to.equal(204); + }); + + it.skip('With signer : subset : Should send notification with title and body along with additional options for alias', async () => { + const res = await userAlice.channel.send( + [ + 'eip155:97:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:97:0x93A829d16DE51745Db0530A0F8E8A9B8CA5370E5', + ], + { + notification: { + title: 'hi', + body: 'test-subset', + }, + payload: { + title: 'testing first subset notification', + body: 'testing with random body', + cta: 'https://google.com/', + embed: 'https://avatars.githubusercontent.com/u/64157541?s=200&v=4', + }, + channel: 'eip155:97:0xD8634C39BBFd4033c0d3289C4515275102423681', + } + ); + expect(res.status).to.equal(204); + }); + it('With signer : SIMULATED : Should send notification with title and body', async () => { + const res = await userNoChannel.channel.send( + [`eip155:11155111:${noChannelAddress}`], + { + notification: { + title: 'hi', + body: 'test-targeted-simulated', + }, + } + ); + expect(res.status).to.equal(204); + }); + }); + + describe('channel :: update', () => { + it('Should update channel meta', async () => { + const res = await userKate.channel.update({ + name: 'Updated Name', + description: 'Testing new description', + url: 'https://google.com', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + }); + // console.log(res) + expect(res).not.null; + }, 10000000000); + + it('Should update channel meta', async () => { + const res = await viemUser.channel.update({ + name: 'Updated Name', + description: 'Testing new description', + url: 'https://google.com', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + }); + // console.log(res) + expect(res).not.null; + }, 10000000000); + }); + + 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', + url: 'https://google.com', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC', + }); + // console.log(res) + expect(res).not.null; + }, 10000000000); + }); + + describe('channel :: settings', () => { + it('Should create channel settings', async () => { + const res = await userKate.channel.setting([ + { + type: 1, + default: 1, + description: 'test1', + }, + { + type: 2, + default: 10, + description: 'test2', + data: { + upper: 100, + lower: 1, + }, + }, + { + type: 3, + default: { + lower: 10, + upper: 50, + }, + description: 'test3', + data: { + upper: 100, + lower: 1, + enabled: true, + ticker: 2, + }, + }, + { + type: 3, + default: { + lower: 3, + upper: 5, + }, + description: 'test4', + data: { + upper: 100, + lower: 1, + enabled: false, + ticker: 2, + }, + }, + ]); + // console.log(res) + expect(res).not.null; + }, 10000000000); + + it('Should create channel setting viem signer', async () => { + const res = await viemUser.channel.setting([ + { + type: 1, + default: 1, + description: 'test1', + }, + { + type: 2, + default: 10, + description: 'test2', + data: { + upper: 100, + lower: 1, + }, + }, + { + type: 3, + default: { + lower: 10, + upper: 50, + }, + description: 'test3', + data: { + upper: 100, + lower: 1, + enabled: true, + ticker: 2, + }, + }, + { + type: 3, + default: { + lower: 3, + upper: 5, + }, + description: 'test4', + data: { + upper: 100, + lower: 1, + enabled: false, + ticker: 2, + }, + }, + ]); + // console.log(res) + expect(res).not.null; + }, 10000000000); + }); + + describe('notifications', async () => { + it('Should fetch channel specific feeds', async () => { + const res = await userAlice.channel.notifications( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + { raw: false } + ); + console.log(inspect(res, { depth: null })); + expect(res).not.null; + }); + + it('Should fetch channel specific feeds in raw format', async () => { + const res = await userAlice.channel.notifications( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + { raw: true } + ); + console.log(inspect(res, { depth: null })); + expect(res).not.null; + }); + + it('Should fetch channel specific feeds broadcast type', async () => { + const res = await userAlice.channel.notifications( + '0xD8634C39BBFd4033c0d3289C4515275102423681', + { raw: false, filter: CONSTANTS.NOTIFICATION.TYPE.TARGETTED } + ); + console.log(inspect(res, { depth: null })); + expect(res).not.null; + }); + }); +}); diff --git a/packages/d-node-notif/tests/lib/notification/delegate.test.ts b/packages/d-node-notif/tests/lib/notification/delegate.test.ts new file mode 100644 index 000000000..85f73ed89 --- /dev/null +++ b/packages/d-node-notif/tests/lib/notification/delegate.test.ts @@ -0,0 +1,199 @@ +import { PushAPI } from '../../../src/lib'; // Ensure correct import path +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import { sepolia } from 'viem/chains'; +import { createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +// import tokenABI from './tokenABI'; +import { ENV } from '../../../src/lib/constants'; + +describe('PushAPI.delegate functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let userKate: PushAPI; + let signer1: any; + let account1: string; + let signer2: any; + let viemUser: any; + let account2: string; + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + beforeEach(async () => { + signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); + account1 = await signer1.getAddress(); + + 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']}`, + provider + ); + const signer3 = createWalletClient({ + account: privateKeyToAccount(`0x${process.env['WALLET_PRIVATE_KEY']}`), + chain: sepolia, + transport: http(), + }); + + account2 = await signer2.getAddress(); + + // initialisation with signer and provider + userKate = await PushAPI.initialize(signer2, { env: _env }); + // initialisation with signer + userAlice = await PushAPI.initialize(signer1, { env: _env }); + // initialisation without signer + userBob = await PushAPI.initialize(signer1, { env: _env }); + // initalisation with viem + viemUser = await PushAPI.initialize(signer3, { env: _env }); + }); + + describe('delegate :: add', () => { + // TODO: remove skip after signer becomes optional + it.skip('Without signer and account :: should throw error', async () => { + await expect(() => + userBob.channel.delegate.add( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and without provider :: should throw error', async () => { + await expect(() => + userAlice.channel.delegate.add( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and provider :: should add delegate', async () => { + const res = await userKate.channel.delegate.add( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + // console.log(res); + expect(res).not.null; + }, 100000000); + + it('With viem signer and provider :: should add delegate', async () => { + const res = await viemUser.channel.delegate.add( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + console.log(res); + expect(res).not.null; + }, 100000000); + + it('With signer and provider :: should add delegate', async () => { + const res = await userKate.channel.delegate.add( + '0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + console.log(res); + expect(res).not.null; + }, 100000000); + + it('With signer and provider :: should throw error as delegate caip and provider doesnt match', async () => { + await expect(() => + userKate.channel.delegate.add( + 'eip155:80001:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With viem signer: Should add delegate', async () => { + const res = await viemUser.channel.delegate.add( + '0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + console.log(res); + expect(res).not.null; + }, 10000000); + + it('With viem signer: Should add delegate', async () => { + const res = await viemUser.channel.delegate.add( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + // console.log(res); + expect(res).not.null; + }, 10000000); + }); + + describe('delegate :: remove', () => { + // TODO: remove skip after signer becomes optional + it.skip('Without signer and account :: should throw error', async () => { + await expect(() => + userBob.channel.delegate.remove( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and without provider :: should throw error', async () => { + await expect(() => + userAlice.channel.delegate.remove( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With signer and provider :: should add delegate', async () => { + const res = await userKate.channel.delegate.remove( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + console.log(res); + expect(res).not.null; + }, 100000000); + + it('With signer and provider :: should throw error as delegate caip and provider doesnt match', async () => { + await expect(() => + userKate.channel.delegate.remove( + 'eip155:80001:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ) + ).to.Throw; + }); + + it('With viem signer: Should remove delegate', async () => { + const res = await viemUser.channel.delegate.remove( + 'eip155:11155111:0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924' + ); + // console.log(res); + expect(res).not.null; + }, 10000000); + }); + + describe('delegate :: get', () => { + it.skip('Without signer and account : Should throw error', async () => { + await expect(() => userBob.channel.delegate.get()).to.Throw; + }); + it('Without signer : Should get delegates', async () => { + const res = await userBob.channel.delegate.get({ + channel: '0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(res) + expect(res).not.null; + }); + + it('Without signer : Should fetch delegates', async () => { + const res = await userBob.channel.delegate.get({ + channel: 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(res); + expect(res).not.null; + }); + + it('Without signer : Should fetch delegates for alias', async () => { + const res = await userBob.channel.delegate.get({ + channel: 'eip155:80001:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(res) + expect(res).not.null; + }); + + it('With signer : Should fetch delegates for channel', async () => { + const res = await userKate.channel.delegate.get(); + // console.log(res); + expect(res).not.null; + }); + }); +}); diff --git a/packages/d-node-notif/tests/lib/notification/notification.test.ts b/packages/d-node-notif/tests/lib/notification/notification.test.ts new file mode 100644 index 000000000..461e73b8c --- /dev/null +++ b/packages/d-node-notif/tests/lib/notification/notification.test.ts @@ -0,0 +1,340 @@ +import { PushAPI } from '../../../src/lib'; // Ensure correct import path +import { expect } from 'chai'; +import { ethers } from 'ethers'; +import { createWalletClient, http } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; +import { ENV } from '../../../src/lib/constants'; + +// import tokenABI from './tokenABI'; +describe('PushAPI.notification functionality', () => { + let userAlice: PushAPI; + let userBob: PushAPI; + let userKate: PushAPI; + let signer1: any; + let account1: string; + let signer2: any; + let account2: string; + let viemSigner: any; + let userViem: PushAPI; + beforeEach(async () => { + signer1 = new ethers.Wallet(`0x${process.env['WALLET_PRIVATE_KEY']}`); + account1 = await signer1.getAddress(); + + 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']}`, + provider + ); + account2 = await signer2.getAddress(); + viemSigner = createWalletClient({ + account: privateKeyToAccount(`0x${process.env['WALLET_PRIVATE_KEY']}`), + chain: sepolia, + transport: http(), + }); + + // accessing env dynamically using process.env + type EnvStrings = keyof typeof ENV; + const envMode = process.env.ENV as EnvStrings; + const _env = ENV[envMode]; + + // initialisation with signer and provider + userKate = await PushAPI.initialize(signer2, { env: _env }); + // initialisation with signer + userAlice = await PushAPI.initialize(signer1, { env: _env }); + // TODO: remove signer1 after signer becomes optional + // initialisation without signer + userBob = await PushAPI.initialize(signer1, { env: _env }); + // initialisation with viem + userViem = await PushAPI.initialize(viemSigner, { env: _env }); + }); + + describe('PushAPI.notification functionality', () => { + it('Should return feeds with signer object', async () => { + const response = await userAlice.notification.list('SPAM'); + expect(response).not.null; + }); + + it('Should return feeds with signer object when an account is passed', async () => { + const response = await userAlice.notification.list('SPAM', { + account: 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + }); + + it('Should return feeds without signer object when an account is passed', async () => { + const response = await userBob.notification.list('SPAM', { + account: 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + }); + + it.skip('Should throw error without signer object when an account is not passed', async () => { + await expect(() => userBob.notification.list('SPAM')).to.Throw; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('SPAM'); + expect(response).not.null; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('SPAM', { + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(response) + expect(response).not.null; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('SPAM', { + account: 'eip155:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(response) + expect(response).not.null; + }); + + it('Should return feeds when viem is used', async () => { + const response = await userViem.notification.list('SPAM'); + // console.log(response); + expect(response).not.null; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('INBOX', { + account: 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + channels: ['0xD8634C39BBFd4033c0d3289C4515275102423681'], + raw: true, + }); + // console.log(response) + expect(response).not.null; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('INBOX', { + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', + channels: [ + '0xD8634C39BBFd4033c0d3289C4515275102423681', + '0x53474D90663de06BEf5D0017F450730D83168063', + ], + raw: true, + }); + // console.log(response) + expect(response).not.null; + }); + + it('Should return feeds when signer with provider is used', async () => { + const response = await userKate.notification.list('INBOX', { + account: 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + channels: [ + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + 'eip155:11155111:0x53474D90663de06BEf5D0017F450730D83168063', + ], + raw: true, + }); + // console.log(response); + expect(response).not.null; + }); + }); + + 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' + // ); + }); + it.skip('Without signer object: should throw error', async () => { + await expect(() => + userBob.notification.subscribe( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + ) + ).to.Throw; + }); + + it('With signer object: should convert to eth caip for normal address', async () => { + const res = await userKate.notification.subscribe( + '0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + // console.log(res); + expect(res).not.null; + }); + + it('With signer object: should optin with partial caip', async () => { + const res = await userKate.notification.subscribe( + 'eip155:0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + // console.log(res); + expect(res).not.null; + }); + + it('With signer object: Should subscribe', async () => { + const res = await userAlice.notification.subscribe( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681', + { + settings: [ + { + enabled: false, + }, + { + enabled: false, + value: 0, + }, + ], + } + ); + // console.log(res) + expect(res).not.null; + }); + + it('With signer and provider: Should subscribe', async () => { + const res = await userKate.notification.subscribe( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + // console.log(res) + expect(res).not.null; + }); + + it('With signer and provider: Should subscribe', async () => { + const res = await userKate.notification.subscribe( + 'eip155:11155111:0xC8c243a4fd7F34c49901fe441958953402b7C024', + { + settings: [ + { + enabled: false, + }, + { + enabled: true, + value: 15, + }, + { + enabled: true, + value: { + lower: 5, + upper: 10, + }, + }, + { + enabled: true, + value: { + lower: 5, + upper: 10, + }, + }, + ], + } + ); + // console.log(res); + expect(res).not.null; + }); + + it('With viem signer and provider: Should subscribe', async () => { + const res = await userViem.notification.subscribe( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + expect(res.message).to.equal('successfully opted into channel'); + }); + + it('With viem signer and provider: Should unsubscribe', async () => { + const res = await userViem.notification.unsubscribe( + 'eip155:11155111:0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + expect(res.message).to.equal('successfully opted out channel'); + }); + + it('With signer object: should convert to eth caip for normal address', async () => { + const res = await userKate.notification.unsubscribe( + '0xD8634C39BBFd4033c0d3289C4515275102423681' + ); + // console.log(res); + expect(res).not.null; + }); + }); + + describe('notification :: subscriptions', () => { + it.skip('No signer or account: Should throw error', async () => { + await expect(() => userBob.notification.subscriptions()).to.Throw; + }); + + it('Signer with no account: Should return response', async () => { + const response = await userAlice.notification.subscriptions(); + expect(response).not.null; + }); + + it('Signer with account: Should return response', async () => { + const response = await userAlice.notification.subscriptions({ + account: 'eip155:80001:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + expect(response.length).not.equal(0); + }); + + it('Signer with account: Should return response', async () => { + const response = await userKate.notification.subscriptions({ + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + // console.log(JSON.stringify(response)); + expect(response).not.null; + expect(response.length).not.equal(0); + }); + + it('Signer with account: Should return response', async () => { + const response = await userKate.notification.subscriptions({ + account: 'eip155:0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + expect(response.length).not.equal(0); + }); + + it('Signer with account: Should return response', async () => { + const response = await userKate.notification.subscriptions({ + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + expect(response.length).not.equal(0); + }); + + it('Signer with account: Should return response', async () => { + const response = await userKate.notification.subscriptions({ + account: '0xD8634C39BBFd4033c0d3289C4515275102423681', + raw: false, + channel: '0xD8634C39BBFd4033c0d3289C4515275102423681', + }); + expect(response).not.null; + }); + }); + + // TO RUN THIS, MAKE THE PRIVATE FUNTIONS PUBLIC + // describe('debug :: test private functions', () => { + // it('Fetching data from contract', async () => { + // const contract = userKate.createContractInstance( + // '0x2b9bE9259a4F5Ba6344c1b1c07911539642a2D33', + // tokenABI + // ); + // const balance = await contract['balanceOf']( + // '0xD8634C39BBFd4033c0d3289C4515275102423681' + // ); + // console.log(balance.toString()); + // const fees = ethers.utils.parseUnits('50', 18); + // console.log(fees) + // console.log(fees.lte(balance)) + // }); + + // it("Uploading data to ipfs via push node", async () => { + // await userAlice.uploadToIPFSViaPushNode("test") + // }) +}); +// }); diff --git a/packages/d-node-notif/tests/lib/notification/tokenABI.ts b/packages/d-node-notif/tests/lib/notification/tokenABI.ts new file mode 100644 index 000000000..0f602271d --- /dev/null +++ b/packages/d-node-notif/tests/lib/notification/tokenABI.ts @@ -0,0 +1,709 @@ +export const abi = [ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "delegator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "fromDelegate", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "toDelegate", + "type": "address" + } + ], + "name": "DelegateChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "previousBalance", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newBalance", + "type": "uint256" + } + ], + "name": "DelegateVotesChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "holder", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "weight", + "type": "uint256" + } + ], + "name": "HolderWeightChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DELEGATION_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "rawAmount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "born", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "rawAmount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "name": "checkpoints", + "outputs": [ + { + "internalType": "uint32", + "name": "fromBlock", + "type": "uint32" + }, + { + "internalType": "uint96", + "name": "votes", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + } + ], + "name": "delegate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegatee", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "delegateBySig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "delegates", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "getCurrentVotes", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getPriorVotes", + "outputs": [ + { + "internalType": "uint96", + "name": "", + "type": "uint96" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "holderDelegation", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "holderWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "numCheckpoints", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "rawAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "holder", + "type": "address" + } + ], + "name": "resetHolderWeight", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "delegate", + "type": "address" + } + ], + "name": "returnHolderDelegation", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "returnHolderRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bool", + "name": "value", + "type": "bool" + } + ], + "name": "setHolderDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "rawAmount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "src", + "type": "address" + }, + { + "internalType": "address", + "name": "dst", + "type": "address" + }, + { + "internalType": "uint256", + "name": "rawAmount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ] as const \ No newline at end of file diff --git a/packages/d-node-notif/tests/lib/utils/parseSettings.test.ts b/packages/d-node-notif/tests/lib/utils/parseSettings.test.ts new file mode 100644 index 000000000..a169d9489 --- /dev/null +++ b/packages/d-node-notif/tests/lib/utils/parseSettings.test.ts @@ -0,0 +1,51 @@ +import { parseSettings } from '../../../src/lib/utils/parseSettings'; +import { expect } from 'chai'; + +const userSettingsTestData = + '[{"type": 1, "user": true, "index": 1, "default": true, "description": "test1"}, {"type": 2, "user": 10, "index": 2, "ticker": 1, "default": 10, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test2"}, {"type": 3, "user": {"lower": 10, "upper": 50}, "index": 3, "ticker": 2, "default": {"lower": 10, "upper": 50}, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test3"}, {"type": 3, "user": {"lower": 3, "upper": 5}, "index": 4, "ticker": 2, "default": {"lower": 3, "upper": 5}, "enabled": false, "lowerLimit": 1, "upperLimit": 100, "description": "test4"}]'; +const channelSettinsgTestData = [ + { type: 1, index: 1, default: true, description: 'test1' }, + { + type: 2, + index: 2, + ticker: 1, + default: 10, + enabled: true, + lowerLimit: 1, + upperLimit: 100, + description: 'test2', + }, + { + type: 3, + index: 3, + ticker: 2, + default: { lower: 10, upper: 50 }, + enabled: true, + lowerLimit: 1, + upperLimit: 100, + description: 'test3', + }, + { + type: 3, + index: 4, + ticker: 2, + default: { lower: 3, upper: 5 }, + enabled: false, + lowerLimit: 1, + upperLimit: 100, + description: 'test4', + }, +]; +describe('Test parseSettings', () => { + it('Should succesfully parse the settings', () => { + const res = parseSettings(userSettingsTestData); + console.log(res); + expect(res.length).to.be.equal(4); + }); + + it('Should succesfully parse the channel settings', () => { + const res = parseSettings(channelSettinsgTestData); + console.log(res); + expect(res.length).to.be.equal(4); + }); +}); diff --git a/packages/d-node-notif/tests/lib/utils/parseSubscriptionAPI.test.ts b/packages/d-node-notif/tests/lib/utils/parseSubscriptionAPI.test.ts new file mode 100644 index 000000000..3dda1e8dd --- /dev/null +++ b/packages/d-node-notif/tests/lib/utils/parseSubscriptionAPI.test.ts @@ -0,0 +1,25 @@ +import { parseSubscriptionsApiResponse } from '../../../src/lib/utils/pasreSubscriptionAPI'; +import { expect } from 'chai'; + +const testData = [ + { + channel: '0xD8634C39BBFd4033c0d3289C4515275102423681', + user_settings: + '[{"type": 1, "user": true, "index": 1, "default": true, "description": "test1"}, {"type": 2, "user": 10, "index": 2, "ticker": 1, "default": 10, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test2"}, {"type": 3, "user": {"lower": 10, "upper": 50}, "index": 3, "ticker": 2, "default": {"lower": 10, "upper": 50}, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test3"}, {"type": 3, "user": {"lower": 3, "upper": 5}, "index": 4, "ticker": 2, "default": {"lower": 3, "upper": 5}, "enabled": false, "lowerLimit": 1, "upperLimit": 100, "description": "test4"}]', + }, + { + channel: '0x74415Bc4C4Bf4Baecc2DD372426F0a1D016Fa924', + user_settings: null, + }, + { + channel: '0xC8c243a4fd7F34c49901fe441958953402b7C024', + user_settings: + '[{"type": 1, "user": false, "index": 1, "default": true, "description": "test1"}, {"type": 2, "user": 15, "index": 2, "ticker": 1, "default": 10, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test2"}, {"type": 3, "user": {"lower": 5, "upper": 10}, "index": 3, "ticker": 2, "default": {"lower": 10, "upper": 50}, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test3"}, {"type": 3, "user": {"lower": 5, "upper": 10}, "index": 4, "ticker": 2, "default": {"lower": 3, "upper": 5}, "enabled": true, "lowerLimit": 1, "upperLimit": 100, "description": "test4"}]', + }, +]; +describe('Test parseSubscriptionsApiResponse', () => { + it('Should succesfully parse the subscriptions', () => { + const res = parseSubscriptionsApiResponse(testData); + console.log(JSON.stringify(res)) + }); +}); diff --git a/packages/restapi/tests/loaders/envVerifier.ts b/packages/d-node-notif/tests/loaders/envVerifier.ts similarity index 97% rename from packages/restapi/tests/loaders/envVerifier.ts rename to packages/d-node-notif/tests/loaders/envVerifier.ts index cddde942b..c21eae75f 100644 --- a/packages/restapi/tests/loaders/envVerifier.ts +++ b/packages/d-node-notif/tests/loaders/envVerifier.ts @@ -1,9 +1,9 @@ // Load FS and Other dependency -import * as fs from 'fs' -import * as readline from 'readline' +import * as fs from 'fs'; +import * as readline from 'readline'; export default async () => { - /* try { + /* try { // Load environment files const envpath = `${__dirname}/../.env` const envsamplepath = `${__dirname}/../.env.sample` @@ -77,4 +77,4 @@ export default async () => { console.log(' Error on env verifier loader: %o ', e) throw e }*/ -} \ No newline at end of file +}; diff --git a/packages/d-node-notif/tests/process-env.d.ts b/packages/d-node-notif/tests/process-env.d.ts new file mode 100644 index 000000000..01c738239 --- /dev/null +++ b/packages/d-node-notif/tests/process-env.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface ProcessEnv { + readonly ENV: string + } +} \ No newline at end of file diff --git a/packages/d-node-notif/tests/root.ts b/packages/d-node-notif/tests/root.ts new file mode 100644 index 000000000..3e36df7a9 --- /dev/null +++ b/packages/d-node-notif/tests/root.ts @@ -0,0 +1,29 @@ +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +export const mochaHooks = { + // This file is needed to end the test suite. + beforeAll: [ + async function () { + // Load .env file + const envFound = dotenv.config({ path: path.resolve(__dirname, './.env')}) + // check if .env exists + if (!envFound) { + console.log(' .env NOT FOUND '); + process.exit(1); + } else { + // Check environment setup first + console.log(' Verifying ENV ') + const EnvVerifierLoader = (await require('./loaders/envVerifier')).default + await EnvVerifierLoader() + console.log(' ENV Verified / Generated and Loaded! ') + } + }, + ], + + afterAll(done: () => void) { + done(); + console.log(' ALL TEST CASES EXECUTED '); + process.exit(0); + }, +}; \ No newline at end of file diff --git a/packages/d-node-notif/tsconfig.json b/packages/d-node-notif/tsconfig.json new file mode 100644 index 000000000..db7b56666 --- /dev/null +++ b/packages/d-node-notif/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/d-node-notif/tsconfig.lib.json b/packages/d-node-notif/tsconfig.lib.json new file mode 100644 index 000000000..e85ef50f6 --- /dev/null +++ b/packages/d-node-notif/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/d-node-notif/tsconfig.mocha.json b/packages/d-node-notif/tsconfig.mocha.json new file mode 100644 index 000000000..d8df67126 --- /dev/null +++ b/packages/d-node-notif/tsconfig.mocha.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "ts-node": { + "transpileOnly": true, + "esm": true, + }, + + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": false, + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index 69d902ee7..99d89699b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,8 @@ "@pushprotocol/restapi": ["packages/restapi/src/index.ts"], "@pushprotocol/socket": ["packages/socket/src/index.ts"], "@pushprotocol/uiembed": ["packages/uiembed/src/index.ts"], - "@pushprotocol/uiweb": ["packages/uiweb/src/index.ts"] + "@pushprotocol/uiweb": ["packages/uiweb/src/index.ts"], + "@sdk/d-node-notif": ["packages/d-node-notif/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index 37bec3e7e..b7001c5f5 100644 --- a/workspace.json +++ b/workspace.json @@ -2,13 +2,14 @@ "$schema": "./node_modules/nx/schemas/workspace-schema.json", "version": 2, "projects": { + "d-node-notif": "packages/d-node-notif", "examples-sdk-frontend-react": "packages/examples/sdk-frontend-react", "ledgerlive": "packages/ledgerlive", + "react-native-sdk": "packages/reactnative", "restapi": "packages/restapi", "socket": "packages/socket", "uiembed": "packages/uiembed", - "uiweb": "packages/uiweb", - "react-native-sdk": "packages/reactnative", - "uireactnative": "packages/uireactnative" + "uireactnative": "packages/uireactnative", + "uiweb": "packages/uiweb" } }