Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add walletconnect notify provider for web3Inbox #162

Merged
merged 20 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
];
145 changes: 145 additions & 0 deletions src/providers/walletconnectNotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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;

// 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)
);
const waitTime = 1 / PER_SECOND_RATE_LIMIT + WAIT_ERROR_MARGIN;
await wait(waitTime);
}
}

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