Skip to content

Commit

Permalink
feat: Add walletconnect notify provider for web3Inbox (#162)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Less <[email protected]>
  • Loading branch information
3 people committed Jan 16, 2024
1 parent 74f3c24 commit d8997fc
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,5 +9,6 @@ export default [
webhook,
discord,
// beams,
xmtp
xmtp,
walletconnectNotify
];
146 changes: 146 additions & 0 deletions src/providers/walletconnectNotify.ts
Original file line number Diff line number Diff line change
@@ -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<void>(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<string, string>();
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
);
}
36 changes: 35 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"

[email protected]:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
Expand Down Expand Up @@ -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==

[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
Expand Down Expand Up @@ -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"

[email protected]:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
Expand Down Expand Up @@ -4171,7 +4205,7 @@ [email protected]:
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==
Expand Down

0 comments on commit d8997fc

Please sign in to comment.