notifier for governance proposals just open for voting (#2)

* notifier for governance proposals just open for voting

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* heroky deployment

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Rename procfile to Procfile

* we dont need no port

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* improve logging

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* remove spam

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* code review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2021-08-22 15:48:46 +02:00 committed by GitHub
parent 6dcbdc89db
commit 9b3333fa76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 367 additions and 1 deletions

1
Procfile Normal file
View File

@ -0,0 +1 @@
worker: npm run notifier

View File

@ -13,7 +13,8 @@
"format": "prettier --write .",
"lint": "eslint . --ext ts --ext tsx --ext js --ext jsx",
"test": "jest",
"test-all": "yarn lint && yarn type-check && yarn test"
"test-all": "yarn lint && yarn type-check && yarn test",
"notifier": "TS_NODE_PROJECT=./tsconfig.commonjs.json ts-node scripts/governance-notifier.ts"
},
"lint-staged": {
"*.@(ts|tsx|js|jsx)": [
@ -31,15 +32,18 @@
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@solana/spl-token": "^0.1.3",
"@solana/web3.js": "^1.5.0",
"axios": "^0.21.1",
"immer": "^9.0.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"next": "latest",
"next-themes": "^0.0.14",
"node-fetch": "^2.6.1",
"rc-slider": "^9.7.2",
"react": "^17.0.1",
"react-countdown": "^2.3.2",
"react-dom": "^17.0.1",
"ts-node": "^10.2.1",
"zustand": "^3.4.1"
},
"devDependencies": {

151
scripts/api.ts Normal file
View File

@ -0,0 +1,151 @@
import { PublicKey } from '@solana/web3.js'
import * as bs58 from 'bs58'
import {
Realm,
GovernanceAccountType,
GovernanceAccount,
GovernanceAccountClass,
} from '../models/accounts'
import { RpcContext } from '../models/api'
import { ParsedAccount, GOVERNANCE_SCHEMA } from '../models/serialisation'
import { deserializeBorsh } from '../utils/borsh'
const fetch = require('node-fetch')
export interface IWallet {
publicKey: PublicKey
}
export class MemcmpFilter {
offset: number
bytes: Buffer
constructor(offset: number, bytes: Buffer) {
this.offset = offset
this.bytes = bytes
}
isMatch(buffer: Buffer) {
if (this.offset + this.bytes.length > buffer.length) {
return false
}
for (let i = 0; i < this.bytes.length; i++) {
if (this.bytes[i] !== buffer[this.offset + i]) return false
}
return true
}
}
export const pubkeyFilter = (
offset: number,
pubkey: PublicKey | undefined | null
) => (!pubkey ? undefined : new MemcmpFilter(offset, pubkey.toBuffer()))
export async function getRealms(rpcContext: RpcContext) {
return getGovernanceAccountsImpl<Realm>(
rpcContext.programId,
rpcContext.endpoint,
Realm,
GovernanceAccountType.Realm
)
}
export async function getGovernanceAccounts<TAccount extends GovernanceAccount>(
programId: PublicKey,
endpoint: string,
accountClass: GovernanceAccountClass,
accountTypes: GovernanceAccountType[],
filters: MemcmpFilter[] = []
) {
if (accountTypes.length === 1) {
return getGovernanceAccountsImpl<TAccount>(
programId,
endpoint,
accountClass,
accountTypes[0],
filters
)
}
const all = await Promise.all(
accountTypes.map((at) =>
getGovernanceAccountsImpl<TAccount>(
programId,
endpoint,
accountClass,
at,
filters
)
)
)
return all.reduce((res, r) => ({ ...res, ...r }), {}) as Record<
string,
ParsedAccount<TAccount>
>
}
async function getGovernanceAccountsImpl<TAccount extends GovernanceAccount>(
programId: PublicKey,
endpoint: string,
accountClass: GovernanceAccountClass,
accountType: GovernanceAccountType,
filters: MemcmpFilter[] = []
) {
const getProgramAccounts = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getProgramAccounts',
params: [
programId.toBase58(),
{
commitment: 'single',
encoding: 'base64',
filters: [
{
memcmp: {
offset: 0,
bytes: bs58.encode([accountType]),
},
},
...filters.map((f) => ({
memcmp: { offset: f.offset, bytes: bs58.encode(f.bytes) },
})),
],
},
],
}),
})
const rawAccounts = (await getProgramAccounts.json())['result']
const accounts: Record<string, ParsedAccount<TAccount>> = {}
for (const rawAccount of rawAccounts) {
try {
const account = {
pubkey: new PublicKey(rawAccount.pubkey),
account: {
...rawAccount.account,
data: [], // There is no need to keep the raw data around once we deserialize it into TAccount
},
info: deserializeBorsh(
GOVERNANCE_SCHEMA,
accountClass,
Buffer.from(rawAccount.account.data[0], 'base64')
),
}
accounts[account.pubkey.toBase58()] = account
} catch (ex) {
console.error(`Can't deserialize ${accountClass}`, ex)
}
}
return accounts
}

View File

@ -0,0 +1,107 @@
import { PublicKey } from '@solana/web3.js'
import axios from 'axios'
import { RealmInfo } from '../@types/types'
import { getAccountTypes, Governance, Proposal } from '../models/accounts'
import { ParsedAccount } from '../models/serialisation'
import { ENDPOINTS } from '../stores/useWalletStore'
import { getGovernanceAccounts, pubkeyFilter } from './api'
const fiveMinutesSeconds = 5 * 60
// run every 5 mins, checks if a mngo governance proposal just opened in the last 5 mins
// and notifies on WEBHOOK_URL
async function runNotifier() {
const nowInSeconds = new Date().getTime() / 1000
const REALMS: RealmInfo[] = [
{
symbol: 'MNGO',
programId: new PublicKey('GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J'),
realmId: new PublicKey('DPiH3H3c7t47BMxqTxLsuPQpEC6Kne8GA9VXbxpnZxFE'),
},
]
const CLUSTER = 'mainnet-beta'
const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
const realmInfo = REALMS.find((r) => r.symbol === 'MNGO')
const governances = await getGovernanceAccounts<Governance>(
realmInfo.programId,
ENDPOINT.url,
Governance,
getAccountTypes(Governance),
[pubkeyFilter(1, realmInfo.realmId)]
)
const governanceIds = Object.keys(governances).map((k) => new PublicKey(k))
const proposalsByGovernance = await Promise.all(
governanceIds.map((governanceId) => {
return getGovernanceAccounts<Proposal>(
realmInfo.programId,
ENDPOINT.url,
Proposal,
getAccountTypes(Proposal),
[pubkeyFilter(1, governanceId)]
)
})
)
const proposals: {
[proposal: string]: ParsedAccount<Proposal>
} = Object.assign({}, ...proposalsByGovernance)
const realmGovernances = Object.fromEntries(
Object.entries(governances).filter(([_k, v]) =>
v.info.realm.equals(realmInfo.realmId)
)
)
const realmProposals = Object.fromEntries(
Object.entries(proposals).filter(([_k, v]) =>
Object.keys(realmGovernances).includes(v.info.governance.toBase58())
)
)
console.log(`- scanning all proposals`)
let countJustOpened = 0
let countSkipped = 0
for (const k in realmProposals) {
const proposal = realmProposals[k]
if (
// voting is closed
proposal.info.votingCompletedAt
) {
countSkipped++
continue
}
if (
// not yet signed i.e. only in draft
!proposal.info.signingOffAt
) {
countSkipped++
continue
}
if (
// proposal opened in last 5 mins
nowInSeconds - proposal.info.signingOffAt.toNumber() <=
fiveMinutesSeconds
) {
countJustOpened++
const msg = `--- ${proposal.info.name} proposal just opened for voting`
console.log(msg)
if (process.env.WEBHOOK_URL) {
axios.post(process.env.WEBHOOK_URL, { msg })
}
}
}
console.log(
`-- countJustOpened: ${countJustOpened}, countSkipped: ${countSkipped}`
)
}
setInterval(runNotifier, fiveMinutesSeconds * 1000)

6
tsconfig.commonjs.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs"
}
}

View File

@ -321,6 +321,18 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
"@cspotcode/source-map-support@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960"
integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@csstools/convert-colors@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
@ -899,6 +911,26 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^7.28.1"
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@types/aria-query@^4.2.0":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b"
@ -1145,6 +1177,11 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
acorn-walk@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
acorn@^7.0.0, acorn@^7.1.1, acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
@ -1153,6 +1190,11 @@ acorn@^8.1.0:
version "8.2.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.1.tgz#0d36af126fb6755095879c1dc6fd7edf7d60a5fb"
acorn@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@ -1222,6 +1264,11 @@ anymatch@^3.0.3, anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -1385,6 +1432,13 @@ aws4@^1.8.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
axios@^0.21.1:
version "0.21.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
dependencies:
follow-redirects "^1.10.0"
babel-jest@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056"
@ -2010,6 +2064,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -2243,6 +2302,11 @@ diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@ -2790,6 +2854,11 @@ flatten@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
follow-redirects@^1.10.0:
version "1.14.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.2.tgz#cecb825047c00f5e66b142f90fed4f515dec789b"
integrity sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -4204,6 +4273,11 @@ make-dir@^3.0.0, make-dir@^3.0.2:
dependencies:
semver "^6.0.0"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
makeerror@1.0.x:
version "1.0.11"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@ -6325,6 +6399,24 @@ traverse-chain@~0.1.0:
resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1"
integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=
ts-node@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5"
integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==
dependencies:
"@cspotcode/source-map-support" "0.6.1"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
yn "3.1.1"
ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
@ -6760,6 +6852,11 @@ yargs@^15.4.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"