From d8997fc3e8d828a20b96537df335ea88ca8a64ac Mon Sep 17 00:00:00 2001 From: Chaitanya Date: Tue, 16 Jan 2024 18:18:59 +0530 Subject: [PATCH] feat: Add walletconnect notify provider for web3Inbox (#162) * Add walletconnect provider * chore: fix bug causing an invalid array to be passed as subscribers * Remove unused console log * chore: Add documentation * chore: add env vars to example file * Add notification body gen * chore: remove switch statement * fix: use new notification id * feat: cross reference subscribers * chore: account for rate limiting * Apply suggestions from code review Co-authored-by: Less * feat: use subscribers coming from the function, less query * chore: run lint fix * New fixes * lint fix * rename provider name * Fixes * Remove empty line changes to reduce changes * Move out wait time variable --------- Co-authored-by: Celine Sarafa Co-authored-by: Less --- .env.example | 6 ++ package.json | 1 + src/providers/index.ts | 4 +- src/providers/walletconnectNotify.ts | 146 +++++++++++++++++++++++++++ yarn.lock | 36 ++++++- 5 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 src/providers/walletconnectNotify.ts diff --git a/.env.example b/.env.example index 60c0ec2..dd05b96 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,12 @@ SERVICE_PUSH_NOTIFICATIONS= SERVICE_PUSHER_BEAMS_INSTANCE_ID= SERVICE_PUSHER_BEAMS_SECRET_KEY= +# WalletConnect Provider +WALLETCONNECT_PROJECT_ID= +WALLETCONNECT_PROJECT_SECRET= +WALLETCONNECT_NOTIFY_SERVER_URL=https://notify.walletconnect.com +WALLETCONNECT_NOTIFICATION_TYPE= + # Sentry SENTRY_DSN= SENTRY_TRACE_SAMPLE_RATE= diff --git a/package.json b/package.json index c7d5bc2..b10a9e8 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/express": "^4.17.11", "@types/jest": "^29.5.10", "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.6", "eslint": "^8.47.0", "jest": "^29.7.0", "jest-environment-node-single-context": "^29.1.0", diff --git a/src/providers/index.ts b/src/providers/index.ts index c828631..be85ffa 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,4 +1,5 @@ import { send as webhook } from './webhook'; +import { send as walletconnectNotify } from './walletconnectNotify'; import { send as discord } from './discord'; // import { send as beams } from './beams'; import { send as xmtp } from './xmtp'; @@ -8,5 +9,6 @@ export default [ webhook, discord, // beams, - xmtp + xmtp, + walletconnectNotify ]; diff --git a/src/providers/walletconnectNotify.ts b/src/providers/walletconnectNotify.ts new file mode 100644 index 0000000..61c5cfb --- /dev/null +++ b/src/providers/walletconnectNotify.ts @@ -0,0 +1,146 @@ +import fetch from 'node-fetch'; +import { capture } from '@snapshot-labs/snapshot-sentry'; + +const WALLETCONNECT_NOTIFY_SERVER_URL = + process.env.WALLETCONNECT_NOTIFY_SERVER_URL; +const WALLETCONNECT_PROJECT_SECRET = process.env.WALLETCONNECT_PROJECT_SECRET; +const WALLETCONNECT_PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID; +const WALLETCONNECT_NOTIFICATION_TYPE = + process.env.WALLETCONNECT_NOTIFICATION_TYPE; + +const AUTH_HEADER = { + Authorization: WALLETCONNECT_PROJECT_SECRET + ? `Bearer ${WALLETCONNECT_PROJECT_SECRET}` + : '' +}; + +// Rate limiting numbers: +const MAX_ACCOUNTS_PER_REQUEST = 500; +const PER_SECOND_RATE_LIMIT = 2; +const WAIT_ERROR_MARGIN = 0.25; +const WAIT_TIME = 1 / PER_SECOND_RATE_LIMIT + WAIT_ERROR_MARGIN; + +// Rate limiting logic: +async function wait(seconds: number) { + return new Promise(resolve => { + setTimeout(resolve, seconds * 1_000); + }); +} + +// Fetch subscribers from WalletConnect Notify server +export async function getSubscribersFromWalletConnect() { + const fetchSubscribersUrl = `${WALLETCONNECT_NOTIFY_SERVER_URL}/${WALLETCONNECT_PROJECT_ID}/subscribers`; + + try { + const subscribersRs = await fetch(fetchSubscribersUrl, { + headers: AUTH_HEADER + }); + + const subscribers: string[] = await subscribersRs.json(); + + return subscribers; + } catch (e) { + capture('[WalletConnect] failed to fetch subscribers'); + return []; + } +} + +// Find the CAIP10 of subscribers, since the Notify API requires CAIP10. +async function crossReferenceSubscribers( + space: { id: string }, + spaceSubscribers +) { + const subscribersFromDb = spaceSubscribers; + const subscribersFromWalletConnect = await getSubscribersFromWalletConnect(); + + // optimistically reserve all subscribers from the db + const crossReferencedSubscribers = new Array(subscribersFromDb.length); + + // Create a hashmap for faster lookup + const addressPrefixMap = new Map(); + for (const subscriber of subscribersFromWalletConnect) { + const unprefixedAddress = subscriber.split(':').pop(); + if (unprefixedAddress) { + addressPrefixMap.set(unprefixedAddress, subscriber); + } + } + + for (const subscriber of subscribersFromDb) { + const crossReferencedAddress = addressPrefixMap.get(subscriber); + if (crossReferencedAddress) { + crossReferencedSubscribers.push(crossReferencedAddress); + } + } + + // remove empty elements from the array, since some might not have been found in WalletConnect Notify server + return crossReferencedSubscribers.filter(addresses => addresses); +} + +async function queueNotificationsToSend(notification, accounts: string[]) { + for (let i = 0; i < accounts.length; i += MAX_ACCOUNTS_PER_REQUEST) { + await sendNotification( + notification, + accounts.slice(i, i + MAX_ACCOUNTS_PER_REQUEST) + ); + + await wait(WAIT_TIME); + } +} + +export async function sendNotification(notification, accounts) { + const notifyUrl = `${WALLETCONNECT_NOTIFY_SERVER_URL}/${WALLETCONNECT_PROJECT_ID}/notify`; + + const body = { + accounts, + notification + }; + + try { + const notifyRs = await fetch(notifyUrl, { + method: 'POST', + headers: { + ...AUTH_HEADER, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); + + const notifySuccess = await notifyRs.json(); + + return notifySuccess; + } catch (e) { + capture('[WalletConnect] failed to notify subscribers', e); + } +} + +// Transform proposal event into notification format. +function formatMessage(event, proposal) { + const space = proposal.space; + if (!space) return null; + + const notificationType = WALLETCONNECT_NOTIFICATION_TYPE; + const notificationBody = `🟢 New proposal on ${space.name} @${space.id}\n\n`; + + const url = `${proposal.link}?app=web3inbox`; + return { + title: proposal.title, + body: notificationBody, + url, + icon: space.avatar, + type: notificationType + }; +} + +export async function send(event, proposal, subscribers) { + if (event.event !== 'proposal/start') return; + const crossReferencedSubscribers = await crossReferenceSubscribers( + proposal.space, + subscribers + ); + const notificationMessage = formatMessage(event, proposal); + + await queueNotificationsToSend( + notificationMessage, + crossReferencedSubscribers + ); +} diff --git a/yarn.lock b/yarn.lock index 91c4b25..f188d2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1385,6 +1385,14 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/node-fetch@^2.6.6": + version "2.6.6" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.6.tgz#b72f3f4bc0c0afee1c0bc9cff68e041d01e3e779" + integrity sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*": version "20.4.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.7.tgz#74d323a93f1391a63477b27b9aec56669c98b2ab" @@ -1779,6 +1787,11 @@ async-mutex@^0.4.0: dependencies: tslib "^2.4.0" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -2123,6 +2136,13 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -2284,6 +2304,11 @@ define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -2917,6 +2942,15 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4171,7 +4205,7 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==