├── frontend ├── public │ ├── favicon.ico.vue │ └── image │ │ ├── IBP2.png │ │ ├── dotsama-1.png │ │ ├── github-logo.png │ │ ├── prometheus-logo-grey.svg │ │ └── prometheus-logo-orange.svg ├── .browserslistrc ├── src │ ├── assets │ │ ├── logo.png │ │ └── logo.svg │ ├── layouts │ │ └── default │ │ │ ├── View.vue │ │ │ └── Default.vue │ ├── views │ │ └── Home.vue │ ├── components │ │ ├── utils.ts │ │ ├── LineChart.vue │ │ ├── IsLocalMonitor.vue │ │ ├── Loading.vue │ │ ├── Config.vue │ │ ├── Monitors.vue │ │ ├── Services.vue │ │ ├── Members.vue │ │ ├── types.ts │ │ ├── HelloWorld.vue │ │ ├── Footer.vue │ │ ├── NodeTable.vue │ │ ├── MonitorTable.vue │ │ ├── MonitorList.vue │ │ ├── SideNav.vue │ │ ├── ServiceTable.vue │ │ ├── Home.vue │ │ ├── ServiceList.vue │ │ ├── Monitor.vue │ │ ├── CheckList.vue │ │ └── Check.vue │ ├── vite-env.d.ts │ ├── main.ts │ ├── plugins │ │ ├── webfontloader.ts │ │ ├── index.ts │ │ └── vuetify.ts │ ├── store │ │ ├── modules │ │ │ ├── geo-dns-pool.ts │ │ │ ├── status.ts │ │ │ ├── service.ts │ │ │ ├── member.ts │ │ │ ├── monitor.ts │ │ │ ├── health-check.ts │ │ │ └── libp2p.ts │ │ └── index.ts │ ├── App.vue │ └── router │ │ └── index.ts ├── .editorconfig ├── tsconfig.node.json ├── .eslintrc.js ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json ├── vite.config.ts └── index.html ├── .dockerignore ├── .babelrc ├── boot-nodes ├── Dockerfile ├── docker-compose.yml ├── package.json ├── run.js └── README.md ├── .prettierignore ├── lib ├── check-service │ ├── index.js │ ├── errors.js │ ├── dns.js │ ├── try-connect.js │ ├── async.js │ └── provider-connection-strategy.js ├── consts.js ├── types.js ├── utils.js ├── utils │ └── logger.js └── prometheus-exporter.js ├── .gitignore ├── config ├── index.js └── config.js ├── .env.sample ├── domain ├── membership-level.entity.js ├── service.entity.js ├── health-check.entity.js ├── provider-service.entity.js ├── member.entity.js ├── provider.entity.js └── providers.aggregate.js ├── docker ├── .env.sample ├── Dockerfile.ibp.services ├── Dockerfile.ibp.frontend ├── DOCKER.md └── docker-compose.yml ├── keys └── readme.md ├── dotenv.js ├── data ├── migrations │ ├── 20230628090000-add-column-monitor-url.js │ ├── 20230526121900-add-bootnode-check-enum.js │ ├── 20230423233600-add-health-check-indices.js │ ├── 20230417152010-insert-geodns-pools.js │ ├── 20230629170000-drop-member-service-node-memberServiceId.js │ ├── 20230607111300-add-bridgehub-westend-chain.js │ ├── 20230629160000-alter-member-region.js │ ├── 20230607112800-add-polkadot-and-westend-bridgehub-services.js │ ├── 20230417152000-create-geodns-pool.js │ ├── 20230919081241-add-health-check-node-origin-field.js │ ├── 20230415201100-create-monitor.js │ ├── 20230918203653-add-external-membership-type.js │ ├── 20230628090100-drop-log-table.js │ ├── 20230411123355-create-membership-level.js │ ├── 20230411123404-insert-membership-levels.js │ ├── 20230928211547-remove-health_check-check_origin.js │ ├── 20230415204015-create-log.js │ ├── 20230928174531-add-member-foreign-to-provider.js │ ├── 20230411123340-create-chain.js │ ├── 20230928175025-remove-constraints-from-member.js │ ├── 20230411123643-create-member.js │ ├── 20230411123418-create-service.js │ ├── 20230928192339-rename-member_service-like-tables.js │ ├── 20230415190244-create-member-service.js │ ├── 20230928181621-remove-external-membership-type.js │ ├── 20230415193015-create-member-service-node.js │ ├── 20230928172000-create-provider.js │ ├── 20230928184941-remove-member-redundant-fields.js │ ├── 20230928185952-rename-memberId-columns-to-providerId.js │ ├── 20230415202600-create-health-check.js │ ├── 20230411123348-insert-chains.js │ ├── 20230628090200-drop-column-id.js │ └── 20230411123426-insert-services.js ├── models │ ├── geo-dns-pool.js │ ├── membership-level.js │ ├── monitor.js │ ├── provider-service-node.js │ ├── provider-service.js │ ├── chain.js │ ├── log.js │ ├── service.js │ ├── member.js │ ├── provider.js │ └── health-check.js ├── migrate.js └── DATA.md ├── .prettierrc ├── .vscode └── settings.json ├── .github └── dependabot.yml ├── etc └── nginx │ └── ibp-monitor.conf ├── RELEASE_MANAGEMENT.md ├── api.js ├── package.json ├── workers ├── f-update-memberships.js └── f-check-service.js └── workers.js /frontend/public/favicon.ico.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | keys 2 | **/node_modules 3 | **/static -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /boot-nodes/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM parity/polkadot 2 | 3 | CMD [ "polkadot" ] -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | not ie 11 5 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibp-network/ibp-monitor/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target 3 | node_modules 4 | dist 5 | build 6 | **/static 7 | **/docker-compose.yml 8 | *.md -------------------------------------------------------------------------------- /frontend/public/image/IBP2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibp-network/ibp-monitor/HEAD/frontend/public/image/IBP2.png -------------------------------------------------------------------------------- /frontend/public/image/dotsama-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibp-network/ibp-monitor/HEAD/frontend/public/image/dotsama-1.png -------------------------------------------------------------------------------- /frontend/public/image/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibp-network/ibp-monitor/HEAD/frontend/public/image/github-logo.png -------------------------------------------------------------------------------- /lib/check-service/index.js: -------------------------------------------------------------------------------- 1 | export { asyncCallWithTimeout, retry } from './async.js' 2 | export { HealthChecker } from './health-checker.js' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keys/peer-id.json 2 | config/config.local.js 3 | **/mariadb 4 | prototype/** 5 | _archive/** 6 | **/node_modules 7 | .DS_Store 8 | **/static 9 | .env -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /frontend/src/layouts/default/View.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | import { config } from '../config/config.js' 2 | import { config as configLocal } from '../config/config.local.js' 3 | 4 | export default Object.assign(config, configLocal) 5 | -------------------------------------------------------------------------------- /frontend/src/components/utils.ts: -------------------------------------------------------------------------------- 1 | export function shortStash(stash: string): string { 2 | if (!stash || stash === '') return '' 3 | return stash.slice(0, 6) + '...' + stash.slice(-6) 4 | } 5 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # public DNS address of the host - comment out if not used 2 | P2P_PUBLIC_HOST=ibp-monitor.test.org 3 | # public IPv4 or IPv6 address of the host - comment out if not used 4 | P2P_PUBLIC_IP=11.11.11.11 -------------------------------------------------------------------------------- /boot-nodes/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | boot-test: 5 | image: parity/polkadot 6 | 7 | ports: 8 | - '9444:9444' 9 | environment: 10 | - name=value 11 | -------------------------------------------------------------------------------- /frontend/src/layouts/default/Default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /lib/consts.js: -------------------------------------------------------------------------------- 1 | export const SECOND_AS_MILLISECONDS = 1_000 2 | export const MINUTE_AS_MILLISECONDS = 60 * SECOND_AS_MILLISECONDS 3 | export const HOUR_AS_MILLISECONDS = 60 * MINUTE_AS_MILLISECONDS 4 | export const DAY_AS_MILLISECONDS = 24 * HOUR_AS_MILLISECONDS 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript'], 7 | rules: { 8 | 'vue/multi-word-component-names': 'off', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /domain/membership-level.entity.js: -------------------------------------------------------------------------------- 1 | export class MembershipLevelEntity { 2 | /** @type {number} */ id 3 | /** @type {string} */ name 4 | /** @type {string} */ subdomain 5 | 6 | constructor({ id, name, subdomain }) { 7 | this.id = id 8 | this.name = name 9 | this.subdomain = subdomain 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docker/.env.sample: -------------------------------------------------------------------------------- 1 | # public DNS address of the host - comment out if not used 2 | P2P_PUBLIC_HOST=ibp-monitor.test.org 3 | # public IPv4 or IPv6 address of the host - comment out if not used 4 | P2P_PUBLIC_IP=11.11.11.11 5 | P2P_PUBLIC_PORT=30000 6 | 7 | # ui http port 8 | HTTP_PORT=30001 9 | # api http port 10 | API_PORT=30002 11 | -------------------------------------------------------------------------------- /frontend/src/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /boot-nodes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boot-nodes", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "node-docker-api": "^1.1.22", 13 | "split2": "^4.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docker/Dockerfile.ibp.services: -------------------------------------------------------------------------------- 1 | FROM node:16-slim 2 | 3 | RUN apt-get update 4 | RUN apt-get install python3 make gcc g++ -y 5 | # RUN npm install -g npm 6 | 7 | COPY . /home/ibp 8 | 9 | WORKDIR /home/ibp 10 | 11 | RUN rm -rf config/config.local.js 12 | RUN rm -rf node_modules 13 | RUN rm -rf frontend 14 | RUN cp config/config.js config/config.local.js 15 | RUN npm install 16 | -------------------------------------------------------------------------------- /keys/readme.md: -------------------------------------------------------------------------------- 1 | ## peerId 2 | 3 | - after 1st run, this folder will contain peer-id.json 4 | - the peerId is particular to you and is secret! do not share your private key 5 | 6 | - the peerId.toString() is printed when the server starts 7 | 8 | ``` 9 | Our peerId 12D3KooWK88CwRP1eHSoHheuQbXFcQrQMni2cgVDmB8bu9NtaqVu 10 | ``` 11 | 12 | - the private key will be used to sign 13 | -------------------------------------------------------------------------------- /dotenv.js: -------------------------------------------------------------------------------- 1 | // due to the way dotenv works, we need to import it here 2 | // the dotenv.config() call needs to complete before we can import anything else 3 | import * as dotenv from 'dotenv' 4 | dotenv.config() 5 | 6 | // console.debug({ 7 | // API_PORT: process.env.API_PORT, 8 | // HTTP_PORT: process.env.HTTP_PORT, 9 | // P2P_PUBLIC_PORT: process.env.P2P_PUBLIC_PORT, 10 | // }) 11 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * main.ts 3 | * 4 | * Bootstraps Vuetify and other plugins then mounts the App` 5 | */ 6 | 7 | // Components 8 | import App from './App.vue' 9 | 10 | // Composables 11 | import { createApp } from 'vue' 12 | 13 | // Plugins 14 | import { registerPlugins } from '@/plugins' 15 | 16 | const app = createApp(App) 17 | 18 | registerPlugins(app) 19 | 20 | app.mount('#app') 21 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | class Model {} 2 | 3 | class DataStore { 4 | Service = undefined 5 | Peer = undefined 6 | HealthCheck = undefined 7 | Log = undefined 8 | pruning = { 9 | age: 90 * 24 * 60 * 60, // days as seconds 10 | interval: 1 * 24 * 60 * 60, // 1 day as seconds 11 | } 12 | 13 | constructor() {} 14 | } 15 | 16 | class DataStoreLoki extends DataStore { 17 | constructor() { 18 | super() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /data/migrations/20230628090000-add-column-monitor-url.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | await queryInterface.addColumn('member', 'monitorUrl', { 5 | type: DataTypes.STRING(132), 6 | allowNull: true, 7 | }) 8 | } 9 | 10 | async function down({ context: queryInterface }) { 11 | // await queryInterface.removeColumn('member', 'monitorUrl') 12 | } 13 | 14 | export { up, down } 15 | -------------------------------------------------------------------------------- /frontend/src/plugins/webfontloader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/webfontloader.ts 3 | * 4 | * webfontloader documentation: https://github.com/typekit/webfontloader 5 | */ 6 | 7 | export async function loadFonts() { 8 | const webFontLoader = await import(/* webpackChunkName: "webfontloader" */ 'webfontloader') 9 | 10 | webFontLoader.load({ 11 | google: { 12 | families: ['Roboto:100,300,400,500,700,900&display=swap'], 13 | }, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /data/migrations/20230526121900-add-bootnode-check-enum.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | await queryInterface.changeColumn('health_check', 'type', { 5 | type: DataTypes.ENUM('service_check', 'system_health', 'best_block', 'bootnode_check'), 6 | allowNull: false, 7 | }) 8 | } 9 | 10 | async function down({ context: queryInterface }) { 11 | // no-op 12 | } 13 | 14 | export { up, down } 15 | -------------------------------------------------------------------------------- /lib/check-service/errors.js: -------------------------------------------------------------------------------- 1 | export class TimeoutException extends Error { 2 | constructor(message) { 3 | super(message) 4 | this.name = 'TimeoutException' 5 | } 6 | } 7 | 8 | export class ProviderError extends Error { 9 | constructor(err) { 10 | super(err) 11 | this.name = 'ProviderError' 12 | } 13 | } 14 | 15 | export class ApiError extends Error { 16 | constructor(err) { 17 | super(err) 18 | this.name = 'ApiError' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 100, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "es5", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/index.ts 3 | * 4 | * Automatically included in `./src/main.ts` 5 | */ 6 | 7 | // Plugins 8 | import { loadFonts } from './webfontloader' 9 | import vuetify from './vuetify' 10 | import router from '../router' 11 | import { store } from '../store' 12 | 13 | // Types 14 | import type { App } from 'vue' 15 | 16 | export function registerPlugins(app: App) { 17 | loadFonts() 18 | app.use(store).use(vuetify).use(router) 19 | } 20 | -------------------------------------------------------------------------------- /data/migrations/20230423233600-add-health-check-indices.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface 3 | // .addIndex('health_check', ['createdAt']) 4 | .sequelize.query( 5 | 'ALTER TABLE `health_check` ADD INDEX IF NOT EXISTS `health_check_created_at` (`createdAt`)' 6 | ) 7 | } 8 | 9 | async function down({ context: queryInterface }) { 10 | await queryInterface.removeIndex('health_check', ['createdAt']) 11 | } 12 | 13 | export { up, down } 14 | -------------------------------------------------------------------------------- /data/migrations/20230417152010-insert-geodns-pools.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('geo_dns_pool', [ 3 | // level 1 4 | { 5 | name: 'Dotters', 6 | host: 'dotters.network', 7 | }, 8 | { 9 | name: 'IBP', 10 | host: 'ibp.network', 11 | }, 12 | ]) 13 | } 14 | 15 | async function down({ context: queryInterface }) { 16 | await queryInterface.bulkDelete('geo_dns_pool', null, {}) 17 | } 18 | 19 | export { up, down } 20 | -------------------------------------------------------------------------------- /data/migrations/20230629170000-drop-member-service-node-memberServiceId.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | try { 3 | queryInterface//.removeColumn('member_service_node', 'memberServiceId') 4 | .sequelize 5 | .query(`ALTER TABLE member_service_node DROP COLUMN IF EXISTS memberServiceId;`) 6 | } catch (err) { 7 | console.warn('Warning: member_service_node.memberServiceId does not exist') 8 | } 9 | } 10 | 11 | async function down({ context: queryInterface }) {} 12 | 13 | export { up, down } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqltools.connections": [ 3 | { 4 | "previewLimit": 50, 5 | "driver": "SQLite", 6 | "name": "dotter.ibp", 7 | "database": "${workspaceFolder:dotters.network}/gossip/data/datastore.sqlite" 8 | }, 9 | { 10 | "previewLimit": 50, 11 | "driver": "SQLite", 12 | "name": "ibp-monitor", 13 | "database": "${workspaceFolder:dotters.network}/monitor/data/datastore.sqlite" 14 | } 15 | ], 16 | "sqltools.useNodeRuntime": true, 17 | "editor.tabSize": 2 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic set up for three package managers 2 | 3 | version: 2 4 | updates: 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: 'github-actions' 7 | directory: '/' 8 | schedule: 9 | interval: 'weekly' 10 | 11 | # Maintain dependencies for npm 12 | - package-ecosystem: 'npm' 13 | directory: '/' 14 | schedule: 15 | interval: 'weekly' 16 | 17 | # # Maintain dependencies for Composer 18 | # - package-ecosystem: "composer" 19 | # directory: "/" 20 | # schedule: 21 | # interval: "weekly" 22 | -------------------------------------------------------------------------------- /boot-nodes/run.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { Docker } = require('node-docker-api') 3 | const docker = new Docker({ socketPath: '/var/run/docker.sock' }) 4 | 5 | ;(async () => { 6 | const image = 'redis' 7 | docker.image.get(image) 8 | docker.container 9 | .create({ 10 | Image: image, 11 | name: 'test', 12 | }) 13 | .then((container) => container.start()) 14 | .then((container) => container.stop()) 15 | .then((container) => container.restart()) 16 | .then((container) => container.delete({ force: true })) 17 | .catch((error) => console.log(error)) 18 | })() 19 | -------------------------------------------------------------------------------- /data/migrations/20230607111300-add-bridgehub-westend-chain.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('chain', [ 3 | { 4 | id: 'bridgehub-westend', 5 | genesisHash: '0441383e31d1266a92b4cb2ddd4c2e3661ac476996db7e5844c52433b81fe782', 6 | name: 'Bridge Hub Westend', 7 | relayChainId: 'westend', 8 | logoUrl: 'https://parachains.info/images/parachains/1677333524_bridgehub_new.svg', 9 | }, 10 | ]) 11 | } 12 | 13 | async function down({ context: queryInterface }) { 14 | // no-op 15 | } 16 | 17 | export { up, down } 18 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # default 2 | 3 | ## Project setup 4 | 5 | ``` 6 | # yarn 7 | yarn 8 | 9 | # npm 10 | npm install 11 | 12 | # pnpm 13 | pnpm install 14 | ``` 15 | 16 | ### Compiles and hot-reloads for development 17 | 18 | ``` 19 | # yarn 20 | yarn dev 21 | 22 | # npm 23 | npm run dev 24 | 25 | # pnpm 26 | pnpm dev 27 | ``` 28 | 29 | ### Compiles and minifies for production 30 | 31 | ``` 32 | # yarn 33 | yarn build 34 | 35 | # npm 36 | npm run build 37 | 38 | # pnpm 39 | pnpm build 40 | ``` 41 | 42 | ### Customize configuration 43 | 44 | See [Configuration Reference](https://vitejs.dev/config/). 45 | -------------------------------------------------------------------------------- /data/migrations/20230629160000-alter-member-region.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.sequelize.query(` 3 | ALTER TABLE member 4 | MODIFY region 5 | ENUM('africa','asia','central_america','europe','middle_east','north_america','oceania', '') 6 | NOT NULL DEFAULT ''; 7 | `) 8 | } 9 | 10 | async function down({ context: queryInterface }) { 11 | await queryInterface.sequelize.query(` 12 | ALTER TABLE member 13 | MODIFY region 14 | ENUM('africa','asia','central_america','europe','middle_east','north_america','oceania') 15 | NOT NULL; 16 | `) 17 | } 18 | 19 | export { up, down } 20 | -------------------------------------------------------------------------------- /frontend/src/components/IsLocalMonitor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /data/migrations/20230607112800-add-polkadot-and-westend-bridgehub-services.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('service', [ 3 | { 4 | id: 'bridgehub-westend-rpc', 5 | chainId: 'bridgehub-westend', 6 | type: 'rpc', 7 | membershipLevelId: 5, 8 | status: 'active', 9 | }, 10 | { 11 | id: 'bridgehub-polkadot-rpc', 12 | chainId: 'bridgehub-polkadot', 13 | type: 'rpc', 14 | membershipLevelId: 5, 15 | status: 'active', 16 | }, 17 | ]) 18 | } 19 | 20 | async function down({ context: queryInterface }) { 21 | // no-op 22 | } 23 | 24 | export { up, down } 25 | -------------------------------------------------------------------------------- /data/migrations/20230417152000-create-geodns-pool.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('geo_dns_pool', geoDnsPoolModel.definition) 3 | .sequelize 4 | .query( 5 | 'CREATE TABLE `geo_dns_pool` ( \ 6 | `id` int(11) NOT NULL AUTO_INCREMENT, \ 7 | `name` varchar(128) NOT NULL, \ 8 | `host` varchar(256) DEFAULT NULL, \ 9 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 10 | PRIMARY KEY (`id`) \ 11 | )' 12 | ) 13 | } 14 | 15 | async function down({ context: queryInterface }) { 16 | await queryInterface.dropTable('geo_dns_pool') 17 | } 18 | 19 | export { up, down } 20 | -------------------------------------------------------------------------------- /frontend/src/store/modules/geo-dns-pool.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module } from 'vuex' 3 | import { IState as IRootState } from '../index' 4 | 5 | export interface IState { 6 | list: any[] 7 | } 8 | 9 | const geoDnsPool: Module = { 10 | namespaced: true, 11 | state: { 12 | list: [], 13 | }, 14 | mutations: { 15 | SET_LIST(state: IState, list: any[]) { 16 | state.list = list 17 | }, 18 | }, 19 | actions: { 20 | async getList({ commit, dispatch }: any) { 21 | const res = await axios.get('/api/geoDnsPool') 22 | commit('SET_LIST', res.data.geoDnsPools) 23 | }, 24 | }, 25 | } 26 | 27 | export default geoDnsPool 28 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "noEmit": true, 16 | "paths": { 17 | "@/*": ["src/*"] 18 | } 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 21 | "references": [{ "path": "./tsconfig.node.json" }], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/Config.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /data/migrations/20230919081241-add-health-check-node-origin-field.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | try { 5 | await queryInterface.addColumn('health_check', 'checkOrigin', { 6 | type: DataTypes.ENUM('member', 'external'), 7 | allowNull: false, 8 | after: 'type', 9 | }) 10 | } catch (err) { 11 | console.warn('Warning: could not add health_check.checkOrigin column') 12 | } 13 | } 14 | 15 | async function down({ context: queryInterface }) { 16 | try { 17 | await queryInterface.removeColumn('health_check', 'checkOrigin') 18 | } catch (err) { 19 | console.warn('Warning: health_check.checkOrigin does not exist') 20 | } 21 | } 22 | 23 | export { up, down } 24 | -------------------------------------------------------------------------------- /frontend/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * plugins/vuetify.ts 3 | * 4 | * Framework documentation: https://vuetifyjs.com` 5 | */ 6 | 7 | // Styles 8 | import '@mdi/font/css/materialdesignicons.css' 9 | import 'vuetify/styles' 10 | 11 | // Composables 12 | import { createVuetify } from 'vuetify' 13 | // import colors from 'vuetify/lib/util/colors' 14 | 15 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 16 | export default createVuetify({ 17 | ssr: true, 18 | theme: { 19 | // dark: true, 20 | themes: { 21 | dark: { 22 | dark: true, 23 | }, 24 | // light: { 25 | // colors: { 26 | // primary: '#1867C0', 27 | // secondary: '#5CBBF6', 28 | // }, 29 | // }, 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /data/models/geo-dns-pool.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const geoDnsPoolModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | autoIncrement: true, 8 | allowNull: false, 9 | primaryKey: true, 10 | }, 11 | name: { 12 | type: DataTypes.STRING(128), 13 | allowNull: false, 14 | }, 15 | host: { 16 | type: DataTypes.STRING(256), 17 | allowNull: true, 18 | }, 19 | createdAt: { 20 | type: DataTypes.DATE, 21 | allowNull: false, 22 | defaultValue: Sequelize.fn('now'), 23 | }, 24 | }, 25 | options: { 26 | tableName: 'geo_dns_pool', 27 | timestamps: true, 28 | createdAt: true, 29 | updatedAt: false, 30 | defaultScope: { 31 | order: [['id', 'ASC']], 32 | }, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /data/models/membership-level.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const membershipLevelModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING(64), 11 | allowNull: false, 12 | }, 13 | subdomain: { 14 | type: DataTypes.STRING(64), 15 | allowNull: false, 16 | }, 17 | createdAt: { 18 | type: DataTypes.DATE, 19 | allowNull: false, 20 | defaultValue: Sequelize.fn('now'), 21 | }, 22 | }, 23 | options: { 24 | tableName: 'membership_level', 25 | timestamps: true, 26 | createdAt: true, 27 | updatedAt: false, 28 | defaultScope: { 29 | attributes: { 30 | exclude: [], 31 | }, 32 | order: [['id', 'ASC']], 33 | }, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /data/migrations/20230415201100-create-monitor.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('monitor', monitorModel.definition) 3 | .sequelize 4 | .query( 5 | "CREATE TABLE `monitor` ( \ 6 | `id` varchar(64) NOT NULL, \ 7 | `name` varchar(128) DEFAULT NULL, \ 8 | `multiaddress` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`multiaddress`)), \ 9 | `status` enum('active','inactive') NOT NULL DEFAULT 'active', \ 10 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 11 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp(), \ 12 | PRIMARY KEY (`id`) \ 13 | )" 14 | ) 15 | } 16 | 17 | async function down({ context: queryInterface }) { 18 | await queryInterface.dropTable('monitor') 19 | } 20 | 21 | export { up, down } 22 | -------------------------------------------------------------------------------- /data/migrations/20230918203653-add-external-membership-type.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | try { 5 | await queryInterface.changeColumn('member', 'membershipType', { 6 | type: DataTypes.ENUM('hobbyist', 'professional', 'external'), 7 | allowNull: false, 8 | }) 9 | } catch (err) { 10 | console.warn('Warning: could not change member.membershipType', { err }) 11 | } 12 | } 13 | 14 | async function down({ context: queryInterface }) { 15 | try { 16 | await queryInterface.bulkDelete('member', null, {}) 17 | await queryInterface.changeColumn('member', 'membershipType', { 18 | type: DataTypes.ENUM('hobbyist', 'professional'), 19 | allowNull: false, 20 | }) 21 | } catch (err) { 22 | console.warn('Warning: could not change back member.membershipType', { err }) 23 | } 24 | } 25 | 26 | export { up, down } 27 | -------------------------------------------------------------------------------- /docker/Dockerfile.ibp.frontend: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM node:16-slim as build-stage 2 | 3 | # https://docs.docker.com/build/building/multi-platform/ 4 | ARG BUILDPLATFORM 5 | ARG TARGETPLATFORM 6 | RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" 7 | 8 | RUN apt-get update 9 | RUN apt-get install python3 make gcc g++ -y 10 | # RUN npm install -g npm 11 | 12 | COPY . /home/ibp 13 | 14 | WORKDIR /home/ibp 15 | 16 | RUN rm -rf config/config.local.js 17 | RUN rm -rf node_modules 18 | RUN cp config/config.js config/config.local.js 19 | RUN npm install 20 | 21 | WORKDIR /home/ibp/frontend 22 | 23 | RUN npm install 24 | RUN npm run build 25 | 26 | FROM --platform=$TARGETPLATFORM nginx as deploy-stage 27 | 28 | COPY --from=build-stage /home/ibp/frontend/static/. /usr/share/nginx/html/ 29 | COPY --from=build-stage /home/ibp/etc/nginx/ibp-monitor.conf /etc/nginx/conf.d/default.conf 30 | 31 | CMD ["nginx", "-g", "daemon off;"] 32 | -------------------------------------------------------------------------------- /lib/check-service/dns.js: -------------------------------------------------------------------------------- 1 | import dns from 'node:dns' 2 | import edns from 'evil-dns' 3 | import { Logger } from '../utils.js' 4 | 5 | const logger = new Logger('lib:checkService:dns') 6 | 7 | export async function setDNS(domain, ip) { 8 | edns.add(domain, ip) 9 | var { address } = await lookupAsync(domain) 10 | logger.debug(`${domain} now resolves to ${address}, and should be ${ip}`) 11 | } 12 | 13 | export async function clearDNS(domain, ip) { 14 | logger.log(`removing eDns for ${domain}`) 15 | edns.remove(domain, ip) 16 | var { address } = await lookupAsync(domain) 17 | logger.log(`${domain} now resolves to ${address}\n`) 18 | } 19 | 20 | // eDns has patched node:dns and not node:dns/promises 21 | export async function lookupAsync(domain) { 22 | return new Promise((resolve, reject) => { 23 | dns.lookup(domain, (err, address, family) => { 24 | if (err) reject(err) 25 | resolve({ address, family }) 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /data/migrations/20230628090100-drop-log-table.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | // Start a transaction 5 | const transaction = await queryInterface.sequelize.transaction(); 6 | try { 7 | 8 | // drop the log table 9 | await queryInterface.dropTable('log', { transaction }); 10 | 11 | // Commit the transaction 12 | await transaction.commit(); 13 | } catch (err) { 14 | // If there is an error, rollback the transaction 15 | await transaction.rollback(); 16 | throw err; 17 | } 18 | } 19 | 20 | async function down({ context: queryInterface }) { 21 | // In this case, the down migration is hard to implement because we cannot undo the deletion of rows and removal of id column. 22 | // Therefore, it's better to backup your database before running the up migration. 23 | // throw new Error('This migration cannot be undone'); 24 | } 25 | 26 | export { up, down } 27 | -------------------------------------------------------------------------------- /data/migrations/20230411123355-create-membership-level.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | // await queryInterface.createTable('membership_level', membershipLevelModel.definition).then(() => 3 | await queryInterface.sequelize 4 | .query( 5 | 'CREATE TABLE `membership_level` ( \ 6 | `id` int(11) NOT NULL, \ 7 | `name` varchar(64) NOT NULL, \ 8 | `subdomain` varchar(64) NOT NULL, \ 9 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 10 | PRIMARY KEY (`id`) \ 11 | )' 12 | ) 13 | .then(() => 14 | // UNIQUE KEY `u_membership_level_name` (`name`) 15 | queryInterface.addConstraint('membership_level', { 16 | type: 'UNIQUE', 17 | name: 'u_membership_level_name', 18 | fields: ['name'], 19 | }) 20 | ) 21 | } 22 | 23 | async function down({ context: queryInterface }) { 24 | await queryInterface.dropTable('membership_level') 25 | } 26 | 27 | export { up, down } 28 | -------------------------------------------------------------------------------- /data/models/monitor.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const monitorModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.STRING(64), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(128), 12 | allowNull: true, 13 | }, 14 | multiaddress: { 15 | type: DataTypes.JSON, 16 | allowNull: false, 17 | }, 18 | status: { 19 | type: DataTypes.ENUM('active', 'inactive'), 20 | allowNull: false, 21 | defaultValue: 'active', 22 | }, 23 | createdAt: { 24 | type: DataTypes.DATE, 25 | allowNull: false, 26 | defaultValue: Sequelize.fn('now'), 27 | }, 28 | updatedAt: { 29 | type: DataTypes.DATE, 30 | allowNull: false, 31 | defaultValue: Sequelize.fn('now'), 32 | }, 33 | }, 34 | options: { 35 | tableName: 'monitor', 36 | timestamps: true, 37 | createdAt: true, 38 | updatedAt: true, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /data/models/provider-service-node.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const providerServiceNodeModel = { 4 | definition: { 5 | peerId: { 6 | type: DataTypes.STRING(64), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | serviceId: { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | }, 14 | providerId: { 15 | type: DataTypes.STRING(128), 16 | allowNull: false, 17 | }, 18 | name: { 19 | type: DataTypes.STRING(128), 20 | allowNull: true, 21 | }, 22 | createdAt: { 23 | type: DataTypes.DATE, 24 | allowNull: false, 25 | defaultValue: Sequelize.fn('now'), 26 | }, 27 | updatedAt: { 28 | type: DataTypes.DATE, 29 | allowNull: false, 30 | defaultValue: Sequelize.fn('now'), 31 | }, 32 | }, 33 | options: { 34 | tableName: 'provider_service_node', 35 | timestamps: true, 36 | createdAt: true, 37 | updatedAt: true, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /domain/service.entity.js: -------------------------------------------------------------------------------- 1 | import { MembershipLevelEntity } from './membership-level.entity.js' 2 | 3 | export class ServiceEntity { 4 | /** @type {String} */ id 5 | /** @type {String} */ chainId 6 | /** @type {'rpc' | 'bootnode'} */ type 7 | /** @type {Number} */ membershipLevelId 8 | /** @type {'active' | 'planned'} */ status 9 | /** @type {Date} */ createdAt 10 | /** @type {Date} */ updatedAt 11 | /** @type {MembershipLevelEntity?} */ membershipLevel 12 | 13 | /** 14 | * @param {ServiceEntity} initializator 15 | */ 16 | constructor({ 17 | id, 18 | chainId, 19 | type, 20 | membershipLevel, 21 | membershipLevelId, 22 | status, 23 | createdAt, 24 | updatedAt, 25 | }) { 26 | this.id = id 27 | this.chainId = chainId 28 | this.type = type 29 | this.membershipLevel = membershipLevel 30 | this.membershipLevelId = membershipLevelId 31 | this.status = status 32 | this.createdAt = createdAt 33 | this.updatedAt = updatedAt 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 37 | -------------------------------------------------------------------------------- /data/models/provider-service.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const providerServiceModel = { 4 | definition: { 5 | providerId: { 6 | type: DataTypes.STRING(128), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | serviceId: { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | primaryKey: true, 14 | }, 15 | serviceUrl: { 16 | type: DataTypes.STRING(256), 17 | allowNull: false, 18 | }, 19 | status: { 20 | type: DataTypes.ENUM('active', 'inactive'), 21 | allowNull: false, 22 | }, 23 | createdAt: { 24 | type: DataTypes.DATE, 25 | allowNull: false, 26 | defaultValue: Sequelize.fn('now'), 27 | }, 28 | updatedAt: { 29 | type: DataTypes.DATE, 30 | allowNull: false, 31 | defaultValue: Sequelize.fn('now'), 32 | }, 33 | }, 34 | options: { 35 | tableName: 'provider_service', 36 | timestamps: true, 37 | createdAt: true, 38 | updatedAt: true, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /data/models/chain.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const chainModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.STRING(64), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | genesisHash: { 11 | type: DataTypes.STRING(64), 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: DataTypes.STRING(64), 16 | allowNull: false, 17 | }, 18 | relayChainId: { 19 | type: DataTypes.STRING(64), 20 | allowNull: true, 21 | }, 22 | logoUrl: { 23 | type: DataTypes.STRING(256), 24 | allowNull: true, 25 | }, 26 | createdAt: { 27 | type: DataTypes.DATE, 28 | allowNull: false, 29 | defaultValue: Sequelize.fn('now'), 30 | }, 31 | }, 32 | options: { 33 | tableName: 'chain', 34 | timestamps: true, 35 | createdAt: true, 36 | updatedAt: false, 37 | defaultScope: { 38 | attributes: { 39 | exclude: [], 40 | }, 41 | order: [['id', 'ASC']], 42 | }, 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibp-monitor-frontend", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "preview": "vite preview", 9 | "lint": "eslint . --fix --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "@mdi/font": "7.2.96", 13 | "chart.js": "^4.3.0", 14 | "chartjs-adapter-date-fns": "^3.0.0", 15 | "date-fns": "^2.30.0", 16 | "roboto-fontface": "*", 17 | "vue": "^3.3.2", 18 | "vue-chartjs": "^5.2.0", 19 | "vue-router": "^4.2.5", 20 | "vuetify": "^3.2.5", 21 | "vuex": "^4.1.0", 22 | "webfontloader": "^1.6.28" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.1.5", 26 | "@types/webfontloader": "^1.6.35", 27 | "@vitejs/plugin-vue": "^4.2.3", 28 | "@vue/eslint-config-typescript": "^11.0.3", 29 | "eslint": "^8.40.0", 30 | "eslint-plugin-vue": "^9.13.0", 31 | "typescript": "^5.0.4", 32 | "vite": "^4.3.9", 33 | "vite-plugin-vuetify": "^1.0.2", 34 | "vue-tsc": "^1.6.9" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/models/log.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const logModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | autoIncrement: true, 8 | allowNull: false, 9 | primaryKey: true, 10 | }, 11 | level: { 12 | type: DataTypes.ENUM('trace', 'debug', 'info', 'warning', 'error'), 13 | allowNull: false, 14 | }, 15 | peerId: { 16 | type: DataTypes.STRING(64), 17 | allowNull: true, 18 | }, 19 | memberServiceId: { 20 | type: DataTypes.INTEGER, 21 | allowNull: true, 22 | }, 23 | data: { 24 | type: DataTypes.JSON, 25 | allowNull: false, 26 | }, 27 | createdAt: { 28 | type: DataTypes.DATE, 29 | allowNull: false, 30 | defaultValue: Sequelize.fn('now'), 31 | }, 32 | }, 33 | options: { 34 | tableName: 'log', 35 | timestamps: true, 36 | createdAt: true, 37 | updatedAt: false, 38 | defaultScope: { 39 | attributes: { 40 | exclude: [], 41 | }, 42 | order: [['id', 'ASC']], 43 | }, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /domain/health-check.entity.js: -------------------------------------------------------------------------------- 1 | export class HealthCheckEntity { 2 | /** @type {string} */ monitorId 3 | /** @type {string} */ serviceId 4 | /** @type {string} */ providerId 5 | /** @type {string?} */ memberId 6 | /** @type {string?} */ peerId 7 | /** @type {'check' | 'gossip'} */ source 8 | /** @type {'serivce_check' | 'system_health' | 'best_block'} */ type 9 | /** @type {'success' | 'warning' | 'error'} */ status 10 | /** @type {number} */ responseTimeMs 11 | /** @type {Object} */ record 12 | 13 | /** 14 | * @param {HealthCheckEntity} initializer 15 | */ 16 | constructor({ 17 | monitorId, 18 | serviceId, 19 | providerId, 20 | memberId, 21 | 22 | peerId, 23 | source, 24 | type, 25 | status, 26 | responseTimeMs, 27 | 28 | record, 29 | }) { 30 | this.monitorId = monitorId 31 | this.serviceId = serviceId 32 | this.providerId = providerId 33 | this.memberId = memberId 34 | this.peerId = peerId 35 | this.source = source 36 | this.type = type 37 | this.status = status 38 | this.responseTimeMs = responseTimeMs 39 | this.record = record 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /data/migrations/20230411123404-insert-membership-levels.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('membership_level', [ 3 | { 4 | id: 1, 5 | name: 'Professional 1', 6 | subdomain: 'rpc', 7 | }, 8 | { 9 | id: 2, 10 | name: 'Professional 2', 11 | subdomain: 'rpc', 12 | }, 13 | { 14 | id: 3, 15 | name: 'Professional 3', 16 | subdomain: 'rpc', 17 | }, 18 | { 19 | id: 4, 20 | name: 'Professional 4', 21 | subdomain: 'rpc', 22 | }, 23 | { 24 | id: 5, 25 | name: 'Professional 5', 26 | subdomain: 'sys', 27 | }, 28 | { 29 | id: 6, 30 | name: 'Professional 6', 31 | subdomain: 'sys', 32 | }, 33 | { 34 | id: 7, 35 | name: 'Professional 7', 36 | subdomain: 'sys', 37 | }, 38 | { 39 | id: 8, 40 | name: 'Professional 8', 41 | subdomain: 'sys', 42 | }, 43 | ]) 44 | } 45 | 46 | async function down({ context: queryInterface }) { 47 | await queryInterface.bulkDelete('membership_level', null, {}) 48 | } 49 | 50 | export { up, down } 51 | -------------------------------------------------------------------------------- /frontend/src/components/Monitors.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 39 | -------------------------------------------------------------------------------- /data/models/service.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const serviceModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.STRING(128), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | chainId: { 11 | type: DataTypes.STRING(64), 12 | allowNull: false, 13 | }, 14 | type: { 15 | type: DataTypes.ENUM('rpc', 'bootnode'), 16 | allowNull: false, 17 | }, 18 | membershipLevelId: { 19 | type: DataTypes.INTEGER, 20 | allowNull: false, 21 | }, 22 | status: { 23 | type: DataTypes.ENUM('active', 'planned'), 24 | allowNull: false, 25 | }, 26 | createdAt: { 27 | type: DataTypes.DATE, 28 | allowNull: false, 29 | defaultValue: Sequelize.fn('now'), 30 | }, 31 | updatedAt: { 32 | type: DataTypes.DATE, 33 | allowNull: false, 34 | defaultValue: Sequelize.fn('now'), 35 | }, 36 | }, 37 | options: { 38 | tableName: 'service', 39 | timestamps: true, 40 | createdAt: true, 41 | updatedAt: true, 42 | defaultScope: { 43 | attributes: { 44 | exclude: [], 45 | }, 46 | order: [['id', 'ASC']], 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Plugins 2 | import vue from '@vitejs/plugin-vue' 3 | import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' 4 | 5 | // Utilities 6 | import { defineConfig } from 'vite' 7 | import { fileURLToPath, URL } from 'node:url' 8 | 9 | import '../dotenv.js' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | build: { 14 | outDir: './static', 15 | }, 16 | plugins: [ 17 | vue({ 18 | template: { transformAssetUrls }, 19 | }), 20 | // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin 21 | vuetify({ 22 | autoImport: true, 23 | }), 24 | ], 25 | define: { 'process.env': {} }, 26 | resolve: { 27 | alias: { 28 | '@': fileURLToPath(new URL('./src', import.meta.url)), 29 | }, 30 | extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'], 31 | }, 32 | server: { 33 | port: Number(process.env.HTTP_PORT || 30001), 34 | // https://vitejs.dev/config/server-options.html#server-proxy 35 | proxy: { 36 | '/api': `http://localhost:${process.env.API_PORT || 30002}`, 37 | // '/api': { 38 | // target: 'http://localhost/30002', 39 | // changeOrigin: true 40 | // } 41 | }, 42 | }, 43 | }) 44 | -------------------------------------------------------------------------------- /etc/nginx/ibp-monitor.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | #access_log /var/log/nginx/host.access.log main; 6 | 7 | location /api { 8 | # to preserve the url encoding, don't put the /api on the end 9 | proxy_pass http://ibp-monitor-api:30002; 10 | } 11 | location / { 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | try_files $uri $uri/ /index.html; 15 | } 16 | #error_page 404 /404.html; 17 | 18 | # redirect server error pages to the static page /50x.html 19 | # 20 | error_page 500 502 503 504 /50x.html; 21 | location = /50x.html { 22 | root /usr/share/nginx/html; 23 | } 24 | 25 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 26 | # 27 | #location ~ \.php$ { 28 | # proxy_pass http://127.0.0.1; 29 | #} 30 | 31 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 32 | # 33 | #location ~ \.php$ { 34 | # root html; 35 | # fastcgi_pass 127.0.0.1:9000; 36 | # fastcgi_index index.php; 37 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 38 | # include fastcgi_params; 39 | #} 40 | 41 | # deny access to .htaccess files, if Apache's document root 42 | # concurs with nginx's one 43 | # 44 | #location ~ /\.ht { 45 | # deny all; 46 | #} 47 | } 48 | -------------------------------------------------------------------------------- /data/migrations/20230928211547-remove-health_check-check_origin.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.removeColumn('health_check', 'checkOrigin', { 8 | transaction, 9 | }) 10 | 11 | await transaction.commit() 12 | } catch (err) { 13 | await transaction.rollback() 14 | throw err 15 | } 16 | } 17 | 18 | async function down({ context: queryInterface }) { 19 | const transaction = await queryInterface.sequelize.transaction() 20 | 21 | try { 22 | await queryInterface.addColumn('health_check', 'checkOrigin', { 23 | type: DataTypes.ENUM('member', 'external'), 24 | allowNull: false, 25 | after: 'type', 26 | 27 | transaction, 28 | }) 29 | 30 | await queryInterface.query( 31 | ` 32 | UPDATE health_check 33 | SET checkOrigin = 'member' 34 | WHERE memberId != NULL 35 | `, 36 | { transaction } 37 | ) 38 | 39 | await queryInterface.query( 40 | ` 41 | UPDATE health_check 42 | SET checkOrigin = 'external' 43 | WHERE memberId == NULL 44 | `, 45 | { transaction } 46 | ) 47 | 48 | await transaction.commit() 49 | } catch (err) { 50 | await transaction.rollback() 51 | throw err 52 | } 53 | } 54 | 55 | export { up, down } 56 | -------------------------------------------------------------------------------- /frontend/src/components/Services.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 51 | -------------------------------------------------------------------------------- /frontend/src/store/modules/status.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module } from 'vuex' 3 | import { IState as IRootState } from '../index' 4 | 5 | export interface IStatus { 6 | [serviceId: string]: { 7 | [hourTimestamp: number]: { 8 | [memberId: string]: { 9 | success: number 10 | warning: number 11 | error: number 12 | status: string 13 | } 14 | } 15 | } 16 | } 17 | 18 | export interface IState { 19 | services: any[] 20 | members: any[] 21 | status: { 22 | [serviceId: string]: { 23 | [hourTimestamp: number]: { 24 | [memberId: string]: string 25 | } 26 | } 27 | } 28 | } 29 | 30 | const status: Module = { 31 | namespaced: true, 32 | state: { 33 | services: [], 34 | members: [], 35 | status: {}, 36 | }, 37 | mutations: { 38 | SET_SERVICES(state: IState, services: any[]) { 39 | state.services = services 40 | }, 41 | SET_MEMBERS(state: IState, members: any[]) { 42 | state.members = members 43 | }, 44 | SET_STATUS(state: IState, status: {}) { 45 | state.status = status 46 | }, 47 | }, 48 | actions: { 49 | async getData({ commit, dispatch }: any) { 50 | const statusData: IState = (await axios.get('/api/status')).data 51 | commit('SET_SERVICES', statusData.services) 52 | commit('SET_MEMBERS', statusData.members) 53 | commit('SET_STATUS', statusData.status) 54 | }, 55 | }, 56 | } 57 | 58 | export default status 59 | -------------------------------------------------------------------------------- /data/migrate.js: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize' 2 | import { Umzug, SequelizeStorage } from 'umzug' 3 | import { config } from '../config/config.js' 4 | import { config as configLocal } from '../config/config.local.js' 5 | 6 | const cfg = Object.assign(config, configLocal) 7 | const sequelize = new Sequelize( 8 | cfg.sequelize.database, 9 | cfg.sequelize.username, 10 | cfg.sequelize.password, 11 | cfg.sequelize.options 12 | ) 13 | 14 | ;(async () => { 15 | const umzug = new Umzug({ 16 | migrations: { 17 | glob: './migrations/*.js', 18 | resolve: (params) => { 19 | if (params.path.endsWith('.mjs') || params.path.endsWith('.js')) { 20 | const getModule = () => import(`file:///${params.path.replace(/\\/g, '/')}`) 21 | return { 22 | name: params.name, 23 | path: params.path, 24 | up: async (upParams) => (await getModule()).up(upParams), 25 | down: async (downParams) => (await getModule()).up(downParams), 26 | } 27 | } 28 | return { 29 | name: params.name, 30 | path: params.path, 31 | ...require(params.path), 32 | } 33 | }, 34 | }, 35 | context: sequelize.getQueryInterface(), 36 | storage: new SequelizeStorage({ sequelize, modelName: 'sequelize_migrations' }), 37 | logger: console, 38 | }) 39 | 40 | console.log('Running migrations...') 41 | await umzug.up() 42 | 43 | setTimeout(() => { 44 | process.exit(0) 45 | }, 1000) 46 | })() 47 | -------------------------------------------------------------------------------- /lib/check-service/try-connect.js: -------------------------------------------------------------------------------- 1 | import { WsProvider, ApiPromise } from '@polkadot/api' 2 | import { Logger } from '../utils.js' 3 | 4 | /** 5 | * 6 | * @param {WsProvider} provider The provider to attempt connection from 7 | * @returns {Promise} It resolves 8 | */ 9 | export function tryConnectProvider(provider) { 10 | // any error is 'out of context' in the handler and does not stop the `await provider.isReady` 11 | // provider.on('connected | disconnected | error') 12 | // https://github.com/polkadot-js/api/issues/5249#issue-1392411072 13 | return new Promise((resolve, reject) => { 14 | provider.on('disconnected', reject) 15 | provider.on('error', reject) 16 | provider.on('connected', () => resolve()) 17 | 18 | provider.connect() 19 | }) 20 | } 21 | 22 | /** 23 | * 24 | * @param {ApiPromise} api The API to try connect to 25 | * @param {Logger?} logger A logger to send messages 26 | * @returns {Promise} It resolves if connection is successful, rejects otherwise 27 | * (also, disconnecting provider) 28 | */ 29 | export function tryConnectApi(api, logger) { 30 | return new Promise((resolve, reject) => { 31 | api.on('disconnect', async (err) => { 32 | logger?.error(err) 33 | return reject() 34 | }) 35 | api.on('error', async (err) => { 36 | logger?.error(err) 37 | return reject() 38 | }) 39 | 40 | logger?.log('waiting for api...') 41 | api.isReady.then((api) => { 42 | logger?.log('api is ready...') 43 | return resolve(api) 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /data/models/member.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const memberModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.STRING(128), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | providerId: { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | }, 14 | serviceIpAddress: { 15 | type: DataTypes.STRING(64), 16 | allowNull: false, 17 | }, 18 | monitorUrl: { 19 | type: DataTypes.STRING(256), 20 | allowNull: true, 21 | }, 22 | membershipType: { 23 | type: DataTypes.ENUM('hobbyist', 'professional'), 24 | allowNull: false, 25 | }, 26 | membershipLevelId: { 27 | type: DataTypes.INTEGER, 28 | allowNull: false, 29 | }, 30 | membershipLevelTimestamp: { 31 | type: DataTypes.INTEGER, 32 | allowNull: false, 33 | }, 34 | status: { 35 | type: DataTypes.ENUM('active', 'pending'), 36 | allowNull: false, 37 | }, 38 | createdAt: { 39 | type: DataTypes.DATE, 40 | allowNull: false, 41 | defaultValue: Sequelize.fn('now'), 42 | }, 43 | updatedAt: { 44 | type: DataTypes.DATE, 45 | allowNull: false, 46 | defaultValue: Sequelize.fn('now'), 47 | }, 48 | }, 49 | options: { 50 | tableName: 'member', 51 | timestamps: true, 52 | createdAt: true, 53 | updatedAt: true, 54 | defaultScope: { 55 | attributes: { 56 | exclude: [], 57 | }, 58 | order: [['id', 'ASC']], 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | IBP Monitor 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/Members.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 51 | -------------------------------------------------------------------------------- /frontend/public/image/prometheus-logo-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /data/migrations/20230415204015-create-log.js: -------------------------------------------------------------------------------- 1 | import { logModel } from '../models/log.js' 2 | 3 | async function up({ context: queryInterface }) { 4 | await queryInterface 5 | // .createTable('log', logModel.definition) 6 | .sequelize.query( 7 | 'CREATE TABLE `log` ( \ 8 | `id` int(11) NOT NULL AUTO_INCREMENT, \ 9 | `level` varchar(16) NOT NULL, \ 10 | `peerId` varchar(128) NOT NULL, \ 11 | `memberServiceId` int(11) NOT NULL, \ 12 | `data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`data`)), \ 13 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 14 | PRIMARY KEY (`id`) \ 15 | )' 16 | ) 17 | .then(() => 18 | queryInterface.addConstraint('log', { 19 | type: 'FOREIGN KEY', 20 | name: 'fk_log_member_service_node', 21 | fields: ['peerId'], 22 | references: { 23 | table: 'member_service_node', 24 | field: 'peerId', 25 | }, 26 | onUpdate: 'RESTRICT', 27 | onDelete: 'RESTRICT', 28 | }) 29 | ) 30 | .then(() => 31 | queryInterface.addConstraint('log', { 32 | type: 'FOREIGN KEY', 33 | name: 'fk_log_member_service', 34 | fields: ['memberServiceId'], 35 | references: { 36 | table: 'member_service', 37 | field: 'id', 38 | }, 39 | onUpdate: 'RESTRICT', 40 | onDelete: 'RESTRICT', 41 | }) 42 | ) 43 | } 44 | 45 | async function down({ context: queryInterface }) { 46 | await queryInterface.dropTable('log') 47 | } 48 | 49 | export { up, down } 50 | -------------------------------------------------------------------------------- /data/migrations/20230928174531-add-member-foreign-to-provider.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.addColumn( 8 | 'member', 9 | 'providerId', 10 | { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | after: 'id', 14 | }, 15 | { transaction } 16 | ) 17 | 18 | await queryInterface.sequelize.query( 19 | ` 20 | UPDATE member 21 | SET providerId = id 22 | `, 23 | { transaction } 24 | ) 25 | 26 | await queryInterface.addConstraint('member', { 27 | type: 'FOREIGN KEY', 28 | name: 'fk_member_provider', 29 | fields: ['providerId'], 30 | references: { 31 | table: 'provider', 32 | field: 'id', 33 | }, 34 | onUpdate: 'RESTRICT', 35 | onDelete: 'RESTRICT', 36 | 37 | transaction, 38 | }) 39 | 40 | await transaction.commit() 41 | } catch (err) { 42 | await transaction.rollback() 43 | throw err 44 | } 45 | } 46 | 47 | async function down({ context: queryInterface }) { 48 | const transaction = await queryInterface.sequelize.transaction() 49 | 50 | try { 51 | await queryInterface.removeConstraint('member', 'fk_member_provider', { 52 | transaction, 53 | }) 54 | await queryInterface.removeColumn('member', 'providerId') 55 | 56 | await transaction.commit() 57 | } catch (err) { 58 | await transaction.rollback() 59 | throw err 60 | } 61 | } 62 | 63 | export { up, down } 64 | -------------------------------------------------------------------------------- /lib/check-service/async.js: -------------------------------------------------------------------------------- 1 | import { Logger } from '../utils.js' 2 | import { TimeoutException } from './errors.js' 3 | 4 | const logger = new Logger('lib:checkService:async') 5 | 6 | /** 7 | * Call an async function with a maximum time limit (in milliseconds) for the timeout 8 | * @param {Promise} asyncPromise An asynchronous promise to resolve 9 | * @param {number} timeLimit Time limit to attempt function in milliseconds 10 | * @returns {Promise | undefined} Resolved promise for async function call, or an error if time limit reached 11 | */ 12 | export async function asyncCallWithTimeout(asyncPromise, timeLimit) { 13 | let timeoutHandle 14 | 15 | const timeoutPromise = new Promise((_resolve, reject) => { 16 | timeoutHandle = setTimeout(() => reject(new TimeoutException()), timeLimit) 17 | }) 18 | 19 | return Promise.race([asyncPromise, timeoutPromise]).then((result) => { 20 | clearTimeout(timeoutHandle) 21 | return result 22 | }) 23 | } 24 | 25 | export async function retry(fn, job, maxRetries = 3, interval = 1000, attempt = 1) { 26 | try { 27 | logger.log(`attempt ${attempt}/${maxRetries}`) 28 | job.log(`attempt ${attempt}/${maxRetries}`) 29 | return await fn() 30 | } catch (error) { 31 | // typically a timeout error 32 | logger.error(error) 33 | job.log(error) 34 | 35 | if (attempt < maxRetries) { 36 | // Wait interval milliseconds before next try 37 | await new Promise((resolve) => setTimeout(resolve, interval)) 38 | return retry(fn, job, maxRetries, interval, attempt + 1) 39 | } else { 40 | throw new Error('Max retries exceeded') 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data/migrations/20230411123340-create-chain.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.sequelize // .createTable('chain', chainModel.definition_v0) 3 | .query( 4 | 'CREATE TABLE `chain` ( \ 5 | `id` varchar(64) NOT NULL, \ 6 | `genesisHash` varchar(64) NOT NULL, \ 7 | `name` varchar(64) NOT NULL, \ 8 | `relayChainId` varchar(64) DEFAULT NULL, \ 9 | `logoUrl` varchar(256) DEFAULT NULL, \ 10 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 11 | PRIMARY KEY (`id`) \ 12 | )' 13 | ) 14 | // KEY `fk_chain_relay_chain` (`relayChainId`) \ 15 | .then(() => 16 | queryInterface.addConstraint('chain', { 17 | type: 'FOREIGN KEY', 18 | name: 'fk_chain_relay_chain', 19 | fields: ['relayChainId'], 20 | references: { 21 | table: 'chain', 22 | field: 'id', 23 | }, 24 | onUpdate: 'RESTRICT', 25 | onDelete: 'RESTRICT', 26 | }) 27 | ) 28 | // UNIQUE KEY `u_chain_name` (`name`), \ 29 | .then(() => 30 | queryInterface.addConstraint('chain', { 31 | type: 'UNIQUE', 32 | name: 'u_chain_name', 33 | fields: ['name'], 34 | }) 35 | ) 36 | // UNIQUE KEY `u_chain_genesis_hash` (`genesisHash`), \ 37 | .then(() => 38 | queryInterface.addConstraint('chain', { 39 | type: 'UNIQUE', 40 | name: 'u_chain_genesis_hash', 41 | fields: ['genesisHash'], 42 | }) 43 | ) 44 | } 45 | 46 | async function down({ context: queryInterface }) { 47 | await queryInterface.dropTable('chain') 48 | } 49 | 50 | export { up, down } 51 | -------------------------------------------------------------------------------- /data/DATA.md: -------------------------------------------------------------------------------- 1 | ## Notes on data & migrations 2 | 3 | ### Database 4 | 5 | In order to simplify the models and native SQL in migrations, we took the decision to focus on MariaDB 6 | 7 | When running the application manually (e.g. via PM2) you need to run this command to create the database: 8 | 9 | ```bash 10 | node migrate.js 11 | ``` 12 | 13 | When running the application via Docker, the database is created by a temp container using the same script. 14 | 15 | ### Empty database 16 | 17 | An empty database is created by the following migrations: 18 | 19 | 20230411123340-create-chain.js 20 | 20230411123348-insert-chains.js 21 | 20230411123355-create-membership-level.js 22 | 20230411123404-insert-membership-levels.js 23 | 20230411123418-create-service.js 24 | 20230411123426-insert-services.js 25 | 20230411123643-create-member.js 26 | 20230415190244-create-member-service.js 27 | 20230415193015-create-member-service-node.js 28 | 20230415201100-create-monitor.js 29 | 20230415202600-create-health-check.js 30 | 20230415204015-create-log.js 31 | 20230417152000-create-geodns-pool.js 32 | 20230417152010-insert-geodns-pools.js 33 | 20230423233600-add-health-check-indices.js 34 | 20230526121900-add-bootnode-check-enum.js 35 | 20230607111300-add-bridgehub-westend-chain.js 36 | 20230607112800-add-polkadot-and-westend-bridgehub-services.js 37 | 20230628090000-add-column-monitor-url.js 38 | 20230628090100-drop-log-table.js 39 | 20230628090200-drop-column-id.js 40 | 20230629160000-alter-member-region.js 41 | 20230629170000-drop-member-service-node-memberServiceId.js 42 | 43 | ### Migrations 44 | 45 | Please refer to Sequelize migrations for more information on how to create a new migration. 46 | -------------------------------------------------------------------------------- /data/migrations/20230928175025-remove-constraints-from-member.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.removeConstraint('member_service_node', 'fk_member_service_node_member', { 8 | transaction, 9 | }) 10 | await queryInterface.removeConstraint('member_service', 'fk_member_service_member', { 11 | transaction, 12 | }) 13 | 14 | await transaction.commit() 15 | } catch (err) { 16 | await transaction.rollback() 17 | throw err 18 | } 19 | } 20 | 21 | async function down({ context: queryInterface }) { 22 | const transaction = await queryInterface.sequelize.transaction() 23 | 24 | try { 25 | await queryInterface.addConstraint('member_service_node', { 26 | type: 'FOREIGN KEY', 27 | name: 'fk_member_service_node_member', 28 | fields: ['memberId'], 29 | references: { 30 | table: 'member', 31 | field: 'id', 32 | }, 33 | onUpdate: 'RESTRICT', 34 | onDelete: 'RESTRICT', 35 | 36 | transaction, 37 | }) 38 | await queryInterface.addConstraint('member_service', { 39 | type: 'FOREIGN KEY', 40 | name: 'fk_member_service_member', 41 | fields: ['memberId'], 42 | references: { 43 | table: 'member', 44 | field: 'id', 45 | }, 46 | onUpdate: 'RESTRICT', 47 | onDelete: 'RESTRICT', 48 | 49 | transaction, 50 | }) 51 | 52 | await transaction.commit() 53 | } catch (err) { 54 | await transaction.rollback() 55 | throw err 56 | } 57 | } 58 | 59 | export { up, down } 60 | -------------------------------------------------------------------------------- /data/models/provider.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const providerModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.STRING(128), 7 | allowNull: false, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | }, 14 | websiteUrl: { 15 | type: DataTypes.STRING(256), 16 | allowNull: true, 17 | }, 18 | logoUrl: { 19 | type: DataTypes.STRING(256), 20 | allowNull: true, 21 | }, 22 | status: { 23 | type: DataTypes.ENUM('active', 'pending'), 24 | allowNull: false, 25 | }, 26 | region: { 27 | type: DataTypes.ENUM( 28 | '', 29 | 'africa', 30 | 'asia', 31 | 'central_america', 32 | 'europe', 33 | 'middle_east', 34 | 'north_america', 35 | 'oceania' 36 | ), 37 | allowNull: true, 38 | }, 39 | latitude: { 40 | type: DataTypes.FLOAT, 41 | allowNull: false, 42 | }, 43 | longitude: { 44 | type: DataTypes.FLOAT, 45 | allowNull: false, 46 | }, 47 | createdAt: { 48 | type: DataTypes.DATE, 49 | allowNull: false, 50 | defaultValue: Sequelize.fn('now'), 51 | }, 52 | updatedAt: { 53 | type: DataTypes.DATE, 54 | allowNull: false, 55 | defaultValue: Sequelize.fn('now'), 56 | }, 57 | }, 58 | options: { 59 | tableName: 'provider', 60 | timestamps: true, 61 | createdAt: true, 62 | updatedAt: true, 63 | defaultScope: { 64 | attributes: { 65 | exclude: [], 66 | }, 67 | order: [['id', 'ASC']], 68 | }, 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /RELEASE_MANAGEMENT.md: -------------------------------------------------------------------------------- 1 | # Release Management 2 | 3 | ## Goals 4 | 5 | We need to release our tools and libraries in a way that is easy to use and contribute. 6 | - github.com for issues and pull requests 7 | - git for version control 8 | - Git branching model 9 | - Git Release management\[?] 10 | 11 | ## Branch vs Release 12 | 13 | ### Git Branching Model 14 | 15 | Branches: 16 | 17 | - master 18 | - production ready, latest stable release 19 | - default branch for `git clone` 20 | - merge from hotfix or release branches 21 | - no PRs? 22 | - hotfix branches 23 | - r0.1.0\[-hotfix1] 24 | - bug fixes to master 25 | - merge from master 26 | - release branches 27 | - r0.1.0\[-rc1] - release candidate 28 | - r0.2.0 - release branch 29 | - allows for bug fixes to be applied to a release 30 | - merge from develop 31 | - develop (could be called `next`) 32 | - latest development changes 33 | - merge from hotfix (forward-porting) 34 | - merge from feature branches 35 | - clone this branch for small changes and bug fixes 36 | - feature branches 37 | - new features in development **that contain breaking changes** 38 | - merge bugs from develop (from time to time, prior to merge to develop) 39 | 40 | ### Git Release 41 | 42 | Do we need this? We are not proposing to distribute binaries, so we don't need to tag releases. 43 | We should ship `bootless` binaries (with versions to match upstream) 44 | 45 | # Resources 46 | 47 | - https://nvie.com/posts/a-successful-git-branching-model/ 48 | - https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository?tool=webui 49 | - https://github.com/mobify/branching-strategy/blob/master/release-deployment.md 50 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | import { toString as uint8ArrayToString } from 'uint8arrays' 2 | import { fromString as uint8ArrayFromString } from 'uint8arrays' 3 | import { pipe } from 'it-pipe' 4 | import map from 'it-map' 5 | import { Logger } from './utils/logger.js' 6 | 7 | export { Logger } from './utils/logger.js' 8 | 9 | async function asyncForeach(array, callback) { 10 | for (let index = 0; index < array.length; index++) { 11 | await callback(array[index], index) 12 | } 13 | } 14 | 15 | // outbound message 16 | async function stringToStream(string, stream) { 17 | // console.debug('stringToStream()', string, stream) 18 | pipe( 19 | [uint8ArrayFromString(string)], 20 | stream, 21 | // Sink function 22 | async function (source) { 23 | // For each chunk of data 24 | for await (const data of source) { 25 | // Output the data 26 | Logger.logger.log('received echo:', uint8ArrayToString(data.subarray())) 27 | } 28 | } 29 | ) 30 | } 31 | 32 | // inbound message 33 | async function streamToString(stream) { 34 | // console.debug('streamToString()', stream) 35 | let ret = '' 36 | await pipe( 37 | stream?.source, 38 | (source) => map(source, (buf) => uint8ArrayToString(buf.subarray())), 39 | // Sink function 40 | async (source) => { 41 | // For each chunk of data 42 | for await (const chunk of source) { 43 | ret = ret + chunk.toString() 44 | } 45 | } 46 | ) 47 | return ret 48 | } 49 | 50 | function shortStash(stash = '') { 51 | if (!stash || stash.length < 10) return stash 52 | return stash.slice(0, 5) + '...' + stash.slice(-5) 53 | } 54 | 55 | export { asyncForeach, streamToString, stringToStream, shortStash } 56 | -------------------------------------------------------------------------------- /frontend/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface IChain { 2 | id: string 3 | genesisHash: string 4 | name: string 5 | relayChainId: string 6 | logoUrl: string 7 | } 8 | 9 | export interface IMembershipLevel { 10 | id: number 11 | name: string 12 | subdomain: string 13 | } 14 | 15 | export interface IService { 16 | id: string 17 | chain: IChain 18 | type: string 19 | membershipLevel: IMembershipLevel 20 | status: string 21 | } 22 | 23 | export interface IMember { 24 | id: string 25 | name: string 26 | websiteUrl: string 27 | logoUrl: string 28 | serviceIpAddress: string 29 | membershipType: string 30 | membershipLevelId: number 31 | membershipLevelTimestamp: number 32 | status: string 33 | region: string 34 | services: [IMemberService] 35 | createdAt: number 36 | updatedAt: number 37 | } 38 | 39 | export interface IMemberService { 40 | id: string 41 | memberId: string 42 | serviceId: string 43 | serviceUrl: string 44 | status: string 45 | createdAt: number 46 | updatedAt: number 47 | } 48 | 49 | export interface IMemberServiceNode { 50 | peerId: string 51 | serviceId: string 52 | memberId: string 53 | createdAt: number 54 | updatedAt: number 55 | } 56 | 57 | export interface IMonitor { 58 | id: string 59 | addresses: string[] 60 | status: string 61 | createdAt: number 62 | updatedAt: number 63 | } 64 | 65 | export interface IHealthCheck { 66 | id: number 67 | monitorId: string 68 | serviceId: string 69 | providerId: string 70 | memberId?: string 71 | peerId: string 72 | source: string 73 | status: string 74 | record: any 75 | createdAt: number 76 | } 77 | 78 | export interface IGeoDnsPool { 79 | id: string 80 | name: string 81 | host: string 82 | createdAt: number 83 | } 84 | -------------------------------------------------------------------------------- /data/models/health-check.js: -------------------------------------------------------------------------------- 1 | import { DataTypes, Sequelize } from 'sequelize' 2 | 3 | export const healthCheckModel = { 4 | definition: { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | autoIncrement: true, 8 | allowNull: false, 9 | primaryKey: true, 10 | }, 11 | monitorId: { 12 | type: DataTypes.STRING(64), 13 | allowNull: false, 14 | }, 15 | serviceId: { 16 | type: DataTypes.STRING(128), 17 | allowNull: false, 18 | }, 19 | providerId: { 20 | type: DataTypes.STRING(128), 21 | allowNull: false, 22 | }, 23 | memberId: { 24 | type: DataTypes.STRING(128), 25 | allowNull: true, 26 | }, 27 | // peer id can be null when the service is unreachable 28 | peerId: { 29 | type: DataTypes.STRING(64), 30 | allowNull: true, 31 | }, 32 | source: { 33 | type: DataTypes.ENUM('check', 'gossip'), 34 | allowNull: false, 35 | }, 36 | type: { 37 | type: DataTypes.ENUM('service_check', 'system_health', 'best_block'), 38 | allowNull: false, 39 | }, 40 | status: { 41 | type: DataTypes.ENUM('error', 'warning', 'success'), 42 | allowNull: false, 43 | }, 44 | responseTimeMs: { 45 | type: DataTypes.INTEGER, 46 | allowNull: true, 47 | }, 48 | record: { 49 | type: DataTypes.JSON, 50 | allowNull: true, 51 | }, 52 | createdAt: { 53 | type: DataTypes.DATE, 54 | allowNull: false, 55 | defaultValue: Sequelize.fn('now'), 56 | }, 57 | }, 58 | options: { 59 | tableName: 'health_check', 60 | timestamps: true, 61 | createdAt: true, 62 | updatedAt: false, 63 | defaultScope: { 64 | attributes: { 65 | exclude: [], 66 | }, 67 | order: [['id', 'DESC']], 68 | }, 69 | }, 70 | } 71 | -------------------------------------------------------------------------------- /domain/provider-service.entity.js: -------------------------------------------------------------------------------- 1 | import { ProviderEntity } from './provider.entity.js' 2 | import { ServiceEntity } from './service.entity.js' 3 | 4 | /** 5 | * Represents an entrypoint for a service provided by an entity within the network, 6 | * either a member or a non-member 7 | */ 8 | export class ProviderServiceEntity { 9 | /** @type {ProviderEntity} */ provider 10 | /** @type {ServiceEntity} */ service 11 | /** @type {String} */ serviceUrl 12 | /** @type {'active' | 'inactive'} */ status 13 | 14 | constructor({ provider, service, serviceUrl, status }) { 15 | this.provider = provider 16 | this.service = service 17 | this.serviceUrl = serviceUrl 18 | this.status = status 19 | } 20 | 21 | /** 22 | * Receivs a list of services, as well as a config object for the input member and its 23 | * @param {[string, string]} endpointConfig The config object that defines a member 24 | * @param {ServiceEntity[]} services A list of all the available services 25 | * @param {ProviderEntity} provider The member for which this endpoint belongs to 26 | * @returns {ProviderServiceEntity} An member service descriptor based on the endpoint information 27 | */ 28 | static fromConfig([chainId, serviceUrl], services, provider) { 29 | const service = services.find((service) => service.chainId === chainId) 30 | 31 | return new ProviderServiceEntity({ 32 | provider, 33 | service, 34 | serviceUrl, 35 | status: 'active', 36 | }) 37 | } 38 | 39 | /** 40 | * Transforms the aggregate into a database-friendly version 41 | * @returns A database-modelled version of the object 42 | */ 43 | toRecord() { 44 | return { 45 | providerId: this.provider.id, 46 | serviceId: this.service.id, 47 | serviceUrl: this.serviceUrl, 48 | status: this.status, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /data/migrations/20230411123643-create-member.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('member', memberModel.definition) 3 | .sequelize 4 | .query( 5 | "CREATE TABLE `member` ( \ 6 | `id` varchar(128) NOT NULL, \ 7 | `name` varchar(128) NOT NULL, \ 8 | `websiteUrl` varchar(256) DEFAULT NULL, \ 9 | `logoUrl` varchar(256) DEFAULT NULL, \ 10 | `serviceIpAddress` varchar(64) NOT NULL, \ 11 | `membershipType` enum('hobbyist','professional') NOT NULL, \ 12 | `membershipLevelId` int(11) NOT NULL, \ 13 | `membershipLevelTimestamp` int(11) NOT NULL, \ 14 | `status` enum('active','pending') NOT NULL, \ 15 | `region` enum('africa','asia','central_america','europe','middle_east','north_america','oceania') NOT NULL DEFAULT 'europe', \ 16 | `latitude` float NOT NULL, \ 17 | `longitude` float NOT NULL, \ 18 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 19 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp(), \ 20 | PRIMARY KEY (`id`) \ 21 | )" 22 | ) 23 | // KEY `fk_member_membership_level` (`membershipLevelId`)' 24 | // CONSTRAINT `fk_member_membership_level` FOREIGN KEY (`membershipLevelId`) REFERENCES `membership_level` (`id`)' 25 | .then(() => 26 | queryInterface.addConstraint('member', { 27 | type: 'FOREIGN KEY', 28 | name: 'fk_member_membership_level', 29 | fields: ['membershipLevelId'], 30 | references: { 31 | table: 'membership_level', 32 | field: 'id', 33 | }, 34 | onUpdate: 'RESTRICT', 35 | onDelete: 'RESTRICT', 36 | }) 37 | ) 38 | } 39 | 40 | async function down({ context: queryInterface }) { 41 | await queryInterface.dropTable('member') 42 | } 43 | 44 | export { up, down } 45 | -------------------------------------------------------------------------------- /frontend/src/store/modules/service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module } from 'vuex' 3 | import { IState as IRootState } from '../index' 4 | 5 | export interface IState { 6 | list: any[] 7 | service: any 8 | nodes: any[] 9 | healthChecks: any[] 10 | } 11 | 12 | const service: Module = { 13 | namespaced: true, 14 | state: { 15 | list: [], 16 | service: {}, 17 | nodes: [], 18 | healthChecks: [], 19 | }, 20 | mutations: { 21 | SET_LIST(state: IState, list: any[]) { 22 | state.list = list 23 | }, 24 | SET_SERVICE(state: IState, value: any) { 25 | console.debug('SET_SERVICE()', value) 26 | state.service = value 27 | }, 28 | SET_HEALTH_CHECKS(state: IState, value: any) { 29 | console.debug('SET_HEALTH_CHECKS()', value) 30 | state.healthChecks = value 31 | }, 32 | SET_NODES(state: IState, value: any) { 33 | console.debug('SET_NODES()', value) 34 | state.nodes = value 35 | }, 36 | }, 37 | // getters: { 38 | // isReady () { 39 | // return window.$polkadot ? window.$polkadot.isReady() : false 40 | // } 41 | // }, 42 | actions: { 43 | async getList({ commit, dispatch }: any) { 44 | const res = await axios.get('/api/service') 45 | commit('SET_LIST', res.data.services) 46 | dispatch('setLocalMonitorId', res.data.localMonitorId, { root: true }) 47 | }, 48 | async setService({ state, commit }: any, serviceId: string) { 49 | const res = await axios.get(`/api/service/${serviceId}`) 50 | const nodesRes = await axios.get(`/api/service/${serviceId}/nodes`) 51 | commit('SET_SERVICE', { 52 | ...res.data.service, 53 | monitors: res.data.monitors, 54 | nodes: nodesRes.data.nodes, 55 | healthChecks: res.data.healthChecks, 56 | }) 57 | }, 58 | }, 59 | } 60 | 61 | export default service 62 | -------------------------------------------------------------------------------- /frontend/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 64 | -------------------------------------------------------------------------------- /frontend/src/store/modules/member.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | // import Vue from 'vue' 3 | import { Module } from 'vuex' 4 | 5 | import { IState as IRootState } from '../index' 6 | export interface IState { 7 | list: any[] 8 | model: any 9 | nodes: any[] 10 | healthChecks: any[] 11 | } 12 | 13 | // Vue.use(Vuex) 14 | 15 | const member: Module = { 16 | namespaced: true, 17 | state: { 18 | list: [], 19 | model: {}, 20 | nodes: [], 21 | healthChecks: [], 22 | }, 23 | mutations: { 24 | SET_LIST(state: IState, list: any[]) { 25 | state.list = list 26 | }, 27 | SET_MODEL(state: IState, value: any) { 28 | console.debug('SET_MODEL()', value) 29 | state.model = value 30 | }, 31 | SET_NODES(state: IState, value: any) { 32 | console.debug('SET_NODES()', value) 33 | state.nodes = value 34 | }, 35 | SET_HEALTHCHECKS(state: IState, value: any) { 36 | console.debug('SET_HEALTHCHECKS()', value) 37 | state.healthChecks = value 38 | }, 39 | }, 40 | actions: { 41 | async getList({ commit, dispatch }: any) { 42 | const res = await axios.get('/api/member') 43 | commit('SET_LIST', res.data.members) 44 | }, 45 | async setModel({ state, commit }: any, memberId: string) { 46 | const res = await axios.get(`/api/member/${memberId}`) 47 | commit('SET_MODEL', { 48 | ...res.data.member, 49 | healthChecks: res.data.healthChecks, 50 | }) 51 | }, 52 | async getNodes({ commit, dispatch }: any, memberId: string) { 53 | const res = await axios.get(`/api/member/${memberId}/nodes`) 54 | commit('SET_NODES', res.data.nodes) 55 | }, 56 | async getChecks({ commit, dispatch }: any, memberId: string) { 57 | const res = await axios.get(`/api/member/${memberId}/healthChecks`) 58 | commit('SET_HEALTHCHECKS', res.data.healthChecks) 59 | }, 60 | }, 61 | } 62 | 63 | export default member 64 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { MINUTE_AS_MILLISECONDS, SECOND_AS_MILLISECONDS } from '../lib/consts.js' 4 | 5 | const GOSSIP_PORT = process.env.GOSSIP_PORT || 30000 // 0 for development/debugging, 30000 for deployment 6 | const HTTP_PORT = process.env.HTTP_PORT || 30001 7 | const API_PORT = process.env.API_PORT || 30002 8 | 9 | // you can overwrite this in config.local.js 10 | 11 | const config = { 12 | dateTimeFormat: 'DD/MM/YYYY HH:mm', 13 | sequelize: { 14 | database: 'ibp_monitor', 15 | username: 'ibp_monitor', 16 | password: 'ibp_monitor', 17 | options: { 18 | dialect: 'mariadb', 19 | // hostname = docker service name 20 | host: 'ibp-datastore', 21 | port: 3306, 22 | logging: false, 23 | }, 24 | }, 25 | redis: { 26 | // hostname = docker service name 27 | host: 'ibp-redis', 28 | port: 6379, 29 | }, 30 | httpPort: HTTP_PORT, 31 | listenPort: GOSSIP_PORT, 32 | apiPort: API_PORT, 33 | allowedTopics: ['/ibp', '/ibp/services', '/ibp/healthCheck', '/ibp/signedMessage'], 34 | updateInterval: 5 * MINUTE_AS_MILLISECONDS, 35 | checkTimeout: 3 * SECOND_AS_MILLISECONDS, 36 | bootstrapPeers: [ 37 | // metaspan:dns 38 | '/dns4/ibp-monitor.metaspan.io/tcp/30000/p2p/12D3KooWK88CwRP1eHSoHheuQbXFcQrQMni2cgVDmB8bu9NtaqVu', 39 | // helikon:ip4 40 | '/ip4/78.181.100.160/tcp/30000/p2p/12D3KooWFZzcMsKumdpNyTKtivcGPukPfQAtCaW5o8qinFzSzHuf', 41 | // helikon:dns 42 | '/dns4/ibp-monitor.helikon.io/tcp/30000/p2p/12D3KooWFZzcMsKumdpNyTKtivcGPukPfQAtCaW5o8qinFzSzHuf', 43 | ], 44 | gossipResults: true, 45 | relay: null, 46 | pruning: { 47 | age: 90 * 24 * 60 * 60, // 90 days as seconds 48 | interval: 1 * 60 * 60, // 1 hour as seconds 49 | }, 50 | providers: { 51 | members: 'https://raw.githubusercontent.com/ibp-network/config/main/members.json', 52 | external: 'https://raw.githubusercontent.com/ibp-network/config/main/external.json', 53 | }, 54 | } 55 | 56 | export { config } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 59 | -------------------------------------------------------------------------------- /domain/member.entity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents an entity that might (or not) be a member of the IBP network, 3 | * and that hosts nodes for the services provided by the network 4 | */ 5 | export class MemberEntity { 6 | /** @type {String} */ id 7 | /** @type {String} */ providerId 8 | /** @type {String} */ serviceIpAdress 9 | /** @type {String} */ monitorUrl 10 | 11 | /** @type {'hobbyist' | 'professional' | 'external'} */ membershipType 12 | /** @type {Number} */ membershipLevelId 13 | /** @type {Number} */ membershipLevelTimestamp 14 | /** @type {'active' | 'pending'} */ status 15 | 16 | constructor({ 17 | id, 18 | serviceIpAddress, 19 | monitorUrl, 20 | membershipType, 21 | membershipLevelId, 22 | membershipLevelTimestamp, 23 | status, 24 | }) { 25 | this.id = id 26 | this.providerId = id 27 | this.serviceIpAddress = serviceIpAddress 28 | this.monitorUrl = monitorUrl 29 | this.membershipType = membershipType 30 | this.membershipLevelId = membershipLevelId 31 | this.membershipLevelTimestamp = membershipLevelTimestamp 32 | this.status = status 33 | } 34 | 35 | /** 36 | * Takes a config object as input (typically from a JSON file) and 37 | * turns it into a member entity 38 | * @param {Record} configObject 39 | * @returns {MemberEntity} 40 | */ 41 | static fromConfig(configObject) { 42 | const { 43 | id, 44 | services_address, 45 | monitor_url, 46 | current_level, 47 | membership, 48 | active, 49 | level_timestamp, 50 | } = configObject 51 | 52 | return new MemberEntity({ 53 | id, 54 | serviceIpAddress: services_address, 55 | monitorUrl: monitor_url, 56 | membershipType: membership, 57 | membershipLevelId: Number(current_level) + 1, 58 | membershipLevelTimestamp: Number(level_timestamp[current_level]), 59 | status: !!Number(active) ? 'active' : 'pending', 60 | }) 61 | } 62 | 63 | toRecord() { 64 | return this 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | import './dotenv.js' 2 | import fs from 'fs' 3 | import { createEd25519PeerId, createFromJSON } from '@libp2p/peer-id-factory' 4 | import { DataStore } from './data/data-store.js' 5 | import { HttpHandler } from './lib/http-handler.js' 6 | import { MessageHandler } from './lib/message-handler.js' 7 | import { toString as uint8ArrayToString } from 'uint8arrays/to-string' 8 | import { config } from './config/config.js' 9 | import { config as configLocal } from './config/config.local.js' 10 | 11 | const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8')) 12 | console.log('VERSION', pkg.version) 13 | 14 | const cfg = Object.assign(config, configLocal) 15 | const API_PORT = cfg.apiPort || 30002 16 | 17 | const ds = new DataStore({ pruning: cfg.pruning }) 18 | const mh = new MessageHandler({ datastore: ds }) 19 | const hh = new HttpHandler({ datastore: ds, version: pkg.version, messageHandler: mh }) 20 | 21 | ;(async () => { 22 | // get PeerId 23 | var peerId 24 | if (fs.existsSync('./keys/peer-id.json')) { 25 | const pidJson = JSON.parse(fs.readFileSync('./keys/peer-id.json', 'utf-8')) 26 | // console.debug(pidJson) 27 | peerId = await createFromJSON(pidJson) 28 | } else { 29 | peerId = await createEd25519PeerId() 30 | fs.writeFileSync( 31 | './keys/peer-id.json', 32 | JSON.stringify({ 33 | id: peerId.toString(), 34 | privKey: uint8ArrayToString(peerId.privateKey, 'base64'), 35 | pubKey: uint8ArrayToString(peerId.publicKey, 'base64'), 36 | }), 37 | 'utf-8' 38 | ) 39 | } 40 | console.debug('Our monitorId', peerId.toString()) 41 | hh.setLocalMonitorId(peerId.toString()) 42 | 43 | // start HttpHandler 44 | hh.listen(API_PORT, () => { 45 | console.log(`\nServer is running on port ${API_PORT}, press crtl-c to stop\n`) 46 | process.on('SIGINT', async () => { 47 | console.warn('\nControl-c detected, shutting down...') 48 | // close the DB gracefully 49 | await ds.close() 50 | console.warn('... stopped!') 51 | process.exit() 52 | }) // CTRL+C 53 | }) 54 | })() 55 | -------------------------------------------------------------------------------- /frontend/src/components/NodeTable.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibp-monitor", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "format": "prettier . --write" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@bull-board/api": "^5.8.4", 15 | "@bull-board/express": "^5.8.4", 16 | "@chainsafe/libp2p-gossipsub": "^6.2.0", 17 | "@chainsafe/libp2p-noise": "^11.0.1", 18 | "@libp2p/bootstrap": "^8.0.0", 19 | "@libp2p/kad-dht": "^8.0.5", 20 | "@libp2p/mdns": "^7.0.0", 21 | "@libp2p/mplex": "^8.0.4", 22 | "@libp2p/peer-id-factory": "^2.0.2", 23 | "@libp2p/pubsub-peer-discovery": "^8.0.0", 24 | "@libp2p/tcp": "^7.0.0", 25 | "@libp2p/websockets": "^6.0.0", 26 | "@polkadot/api": "^10.7.1", 27 | "@polkadot/keyring": "^12.2.1", 28 | "@polkadot/util": "^12.2.1", 29 | "@polkadot/util-crypto": "^12.1.2", 30 | "axios": "^1.6.2", 31 | "bullmq": "^4.12.3", 32 | "chalk": "^5.3.0", 33 | "chalk-template": "^1.1.0", 34 | "chartjs-plugin-annotation": "^3.0.1", 35 | "dotenv": "^16.0.3", 36 | "ejs": "^3.1.9", 37 | "evil-dns": "^0.2.0", 38 | "express": "^4.18.2", 39 | "is-ip": "^5.0.0", 40 | "is-valid-hostname": "^1.0.2", 41 | "it-map": "^3.0.3", 42 | "it-pipe": "^3.0.1", 43 | "libp2p": "^0.44.0", 44 | "lokijs": "^1.5.12", 45 | "mariadb": "^2.5.6", 46 | "moment": "^2.29.4", 47 | "mysql2": "^3.3.1", 48 | "pg": "^8.11.0", 49 | "sequelize": "^6.31.1", 50 | "serialize-error": "^11.0.0", 51 | "sqlite3": "^5.1.6", 52 | "uint8arrays": "^4.0.3", 53 | "umzug": "^3.2.1" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.21.8", 57 | "@babel/preset-env": "^7.23.3", 58 | "@polkadot/extension-dapp": "^0.46.3", 59 | "babel-loader": "^9.1.2", 60 | "html-loader": "^4.2.0", 61 | "html-webpack-plugin": "^5.5.1", 62 | "prettier": "^2.8.8", 63 | "sequelize-cli": "^6.6.2", 64 | "webpack": "^5.82.1", 65 | "webpack-cli": "^5.1.1", 66 | "webpack-node-externals": "^3.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /data/migrations/20230411123418-create-service.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('service', serviceModel.definition) 3 | .sequelize 4 | .query( 5 | "CREATE TABLE `service` ( \ 6 | `id` varchar(128) NOT NULL, \ 7 | `chainId` varchar(64) NOT NULL, \ 8 | `type` enum('rpc','bootnode') NOT NULL, \ 9 | `membershipLevelId` int(11) NOT NULL, \ 10 | `status` enum('active','planned') NOT NULL, \ 11 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 12 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp(), \ 13 | PRIMARY KEY (`id`) \ 14 | )" 15 | ) 16 | .then(() => 17 | // UNIQUE KEY `u_service_chain_service_type` (`chainId`,`type`), \ 18 | queryInterface.addConstraint('service', { 19 | type: 'UNIQUE', 20 | name: 'u_service_chain_service_type', 21 | fields: ['chainId', 'type'], 22 | }) 23 | ) 24 | .then(() => 25 | // CONSTRAINT `fk_service_chain` FOREIGN KEY (`chainId`) REFERENCES `chain` (`id`), \ 26 | queryInterface.addConstraint('service', { 27 | type: 'FOREIGN KEY', 28 | name: 'fk_service_chain', 29 | fields: ['chainId'], 30 | references: { 31 | table: 'chain', 32 | field: 'id', 33 | }, 34 | onUpdate: 'RESTRICT', 35 | onDelete: 'RESTRICT', 36 | }) 37 | ) 38 | // KEY `fk_service_membership_level` (`membershipLevelId`), \ 39 | .then(() => 40 | // CONSTRAINT `fk_service_membership_level` FOREIGN KEY (`membershipLevelId`) REFERENCES `membership_level` (`id`)' 41 | queryInterface.addConstraint('service', { 42 | type: 'FOREIGN KEY', 43 | name: 'fk_service_membership_level', 44 | fields: ['membershipLevelId'], 45 | references: { 46 | table: 'membership_level', 47 | field: 'id', 48 | }, 49 | onUpdate: 'RESTRICT', 50 | onDelete: 'RESTRICT', 51 | }) 52 | ) 53 | } 54 | 55 | async function down({ context: queryInterface }) { 56 | await queryInterface.dropTable('service') 57 | } 58 | 59 | export { up, down } 60 | -------------------------------------------------------------------------------- /frontend/src/store/modules/monitor.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import Vue from 'vue' 3 | import { Module } from 'vuex' 4 | // import { ICurrency } from '../../../types' 5 | // import { ApiPromise, WsProvider } from '@polkadot/api' 6 | 7 | import { IState as IRootState } from '../index' 8 | // import { currencies } from './constants' 9 | // import { PolkadotState, PolkadotWindow } from './types' 10 | // declare let window: PolkadotWindow 11 | export interface IState { 12 | list: any[] 13 | monitor: any 14 | healthChecks: any[] 15 | } 16 | 17 | // Vue.use(Vuex) 18 | 19 | // const polkadot: Module = { 20 | const monitor: Module = { 21 | namespaced: true, 22 | state: { 23 | list: [], 24 | monitor: {}, 25 | healthChecks: [], 26 | }, 27 | mutations: { 28 | SET_LIST(state: IState, list: any[]) { 29 | state.list = list 30 | }, 31 | SET_MONITOR(state: IState, value: any) { 32 | console.debug('SET_MONITOR()', value) 33 | state.monitor = value 34 | }, 35 | }, 36 | // getters: { 37 | // isReady () { 38 | // return window.$polkadot ? window.$polkadot.isReady() : false 39 | // } 40 | // }, 41 | actions: { 42 | async getList({ commit, dispatch }: any) { 43 | const res = await axios.get('/api/monitor') 44 | commit('SET_LIST', res.data.monitors) 45 | // dispatch('init') 46 | dispatch('setLocalMonitorId', res.data.localMonitorId, { root: true }) 47 | }, 48 | async setMonitor({ state, commit }: any, monitorId: string) { 49 | // const service = state.list.find((f: any) => f.serviceUrl === serviceUrl) 50 | const res = await axios.get(`/api/monitor/${monitorId}`) 51 | // commit('SET_DATETIME_FORMAT', res.data.dateTimeFormat) 52 | commit('SET_MONITOR', { ...res.data.monitor, healthChecks: res.data.healthChecks }) 53 | // commit('SET_MONITORS', res.data.monitors) 54 | // commit('SET_HEALTH_CHECKS', res.data.healthChecks) 55 | }, 56 | // setCurrency ({ commit }, c: ICurrency) { 57 | // commit('SET_CURRENCY', c) 58 | // commit('SET_WALLET', testWallets[c.code]) 59 | // } 60 | }, 61 | } 62 | 63 | export default monitor 64 | -------------------------------------------------------------------------------- /data/migrations/20230928192339-rename-member_service-like-tables.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.removeConstraint('health_check', 'fk_health_check_member_service_node', { 8 | transaction, 9 | }) 10 | 11 | await queryInterface.renameTable('member_service_node', 'provider_service_node', { 12 | transaction, 13 | }) 14 | await queryInterface.renameTable('member_service', 'provider_service', { transaction }) 15 | 16 | await queryInterface.addConstraint('health_check', { 17 | type: 'FOREIGN KEY', 18 | name: 'fk_health_check_provider_service_node', 19 | fields: ['peerId'], 20 | references: { 21 | table: 'provider_service_node', 22 | field: 'peerId', 23 | }, 24 | onUpdate: 'RESTRICT', 25 | onDelete: 'RESTRICT', 26 | 27 | transaction, 28 | }) 29 | 30 | await transaction.commit() 31 | } catch (err) { 32 | await transaction.rollback() 33 | throw err 34 | } 35 | } 36 | 37 | async function down({ context: queryInterface }) { 38 | const transaction = await queryInterface.sequelize.transaction() 39 | 40 | try { 41 | await queryInterface.removeConstraint('health_check', 'fk_health_check_provider_service_node', { 42 | transaction, 43 | }) 44 | 45 | await queryInterface.renameTable('provider_service_node', 'member_service_node', { 46 | transaction, 47 | }) 48 | await queryInterface.renameTable('provider_service', 'member_service', { transaction }) 49 | 50 | await queryInterface.addConstraint('health_check', { 51 | type: 'FOREIGN KEY', 52 | name: 'fk_health_check_member_service_node', 53 | fields: ['peerId'], 54 | references: { 55 | table: 'member_service_node', 56 | field: 'peerId', 57 | }, 58 | onUpdate: 'RESTRICT', 59 | onDelete: 'RESTRICT', 60 | 61 | transaction, 62 | }) 63 | 64 | await transaction.commit() 65 | } catch (err) { 66 | await transaction.rollback() 67 | throw err 68 | } 69 | } 70 | 71 | export { up, down } 72 | -------------------------------------------------------------------------------- /frontend/src/store/modules/health-check.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module } from 'vuex' 3 | import { IState as IRootState } from '../index' 4 | 5 | export interface IState { 6 | list: any[] 7 | loading: boolean 8 | offset: number 9 | limit: number 10 | pagination: any 11 | healthCheck: any 12 | // healthChecks: any[] 13 | } 14 | 15 | const healthCheck: Module = { 16 | namespaced: true, 17 | state: { 18 | list: [], 19 | loading: false, 20 | offset: 0, 21 | limit: 15, 22 | healthCheck: {}, 23 | pagination: { pages: [] }, 24 | // healthChecks: [] 25 | }, 26 | mutations: { 27 | SET_LIST(state: IState, list: any[]) { 28 | state.list = list 29 | }, 30 | SET_LOADING(state: IState, value: any) { 31 | state.loading = value 32 | }, 33 | SET_PAGINATION(state: IState, value: any) { 34 | state.pagination = value 35 | }, 36 | SET_OFFSET(state: IState, value: number) { 37 | state.offset = value 38 | }, 39 | SET_LIMIT(state: IState, value: number) { 40 | state.limit = value 41 | }, 42 | SET_HEALTH_CHECK(state: IState, value: any) { 43 | console.debug('SET_HEALTH_CHECK()', value) 44 | state.healthCheck = value 45 | }, 46 | }, 47 | actions: { 48 | async getList({ state, commit, dispatch }: any, { isMember, offset, limit }: any) { 49 | if (offset) { 50 | commit('SET_OFFSET', offset) 51 | } 52 | if (limit) { 53 | commit('SET_LIMIT', limit) 54 | } 55 | commit('SET_LOADING', true) 56 | const res = await axios.get('/api/healthCheck', { 57 | params: { isMember, offset: offset || state.offset, limit: limit || state.limit }, 58 | }) 59 | commit('SET_LIST', res.data.models) 60 | commit('SET_LOADING', false) 61 | commit('SET_PAGINATION', res.data.pagination) 62 | dispatch('setLocalMonitorId', res.data.localMonitorId, { root: true }) 63 | }, 64 | async setHealthCheck({ state, commit }: any, monitorId: string) { 65 | const res = await axios.get(`/api/healthCheck/${monitorId}`) 66 | commit('SET_HEALTH_CHECK', res.data.model) 67 | }, 68 | }, 69 | } 70 | 71 | export default healthCheck 72 | -------------------------------------------------------------------------------- /lib/check-service/provider-connection-strategy.js: -------------------------------------------------------------------------------- 1 | import { ProviderServiceEntity } from '../../domain/provider-service.entity.js' 2 | import { WsProvider } from '@polkadot/api' 3 | import { setDNS, clearDNS } from './dns.js' 4 | import { SECOND_AS_MILLISECONDS } from '../consts.js' 5 | 6 | export class ProviderConnectionStrategy { 7 | /** @type {ProviderServiceEntity} */ providerService 8 | 9 | /** 10 | * @param {ProviderServiceEntity} providerService 11 | */ 12 | constructor(providerService) { 13 | this.providerService = providerService 14 | } 15 | 16 | /** 17 | * Retrieves the endpoint for the connection 18 | * @returns {string} 19 | */ 20 | getEndpoint() { 21 | throw new Error('Not implemented') 22 | } 23 | 24 | /** 25 | * This method runs the necessary steps to happen 26 | * before initializing the provider connection 27 | */ 28 | async before() {} 29 | 30 | /** 31 | * This method builds and retrieves the provider, ready to 32 | * attempt a connection. 33 | * @returns {WsProvider} A Polkadot websockets provider 34 | */ 35 | buildProvider() { 36 | return new WsProvider(this.getEndpoint(), false, {}, 10 * SECOND_AS_MILLISECONDS) 37 | } 38 | 39 | /** 40 | * This method runs the necessary steps to happen 41 | * after disconnecting the provider connection 42 | */ 43 | async after() {} 44 | } 45 | 46 | export class ServiceUrlProviderStrategy extends ProviderConnectionStrategy { 47 | getEndpoint() { 48 | return this.providerService.serviceUrl 49 | } 50 | } 51 | 52 | export class MembershipSubdomainProviderStrategy extends ProviderConnectionStrategy { 53 | /** @type {string} */ #domain 54 | /** @type {string} */ #subdomain 55 | 56 | constructor(providerService, subdomain) { 57 | super(providerService) 58 | this.#domain = `${subdomain}.dotters.network` 59 | } 60 | 61 | async before() { 62 | await setDNS(this.#domain, this.providerService.provider.member.serviceIpAddress) 63 | } 64 | 65 | getEndpoint() { 66 | return `wss://${this.#domain}/${this.providerService.service.chainId}` 67 | } 68 | 69 | async after() { 70 | await clearDNS(this.#domain, this.providerService.provider.member.serviceIpAddress) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /data/migrations/20230415190244-create-member-service.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('member_service', memberServiceModel.definition) 3 | .sequelize 4 | .query( 5 | "CREATE TABLE `member_service` ( \ 6 | `id` int(11) NOT NULL AUTO_INCREMENT, \ 7 | `memberId` varchar(128) NOT NULL, \ 8 | `serviceId` varchar(128) NOT NULL, \ 9 | `serviceUrl` varchar(256) NOT NULL, \ 10 | `status` enum('active','inactive') NOT NULL, \ 11 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 12 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp(), \ 13 | PRIMARY KEY (`id`) \ 14 | )" 15 | ) 16 | .then(() => 17 | // UNIQUE KEY `u_member_service_member_service` (`memberId`,`serviceId`), \ 18 | queryInterface.addConstraint('member_service', { 19 | type: 'UNIQUE', 20 | name: 'u_member_service_member_service', 21 | fields: ['memberId', 'serviceId'], 22 | }) 23 | ) 24 | .then(() => 25 | // CONSTRAINT `fk_member_service_member` FOREIGN KEY (`memberId`) REFERENCES `member` (`id`), \ 26 | queryInterface.addConstraint('member_service', { 27 | type: 'FOREIGN KEY', 28 | name: 'fk_member_service_member', 29 | fields: ['memberId'], 30 | references: { 31 | table: 'member', 32 | field: 'id', 33 | }, 34 | onUpdate: 'RESTRICT', 35 | onDelete: 'RESTRICT', 36 | }) 37 | ) 38 | .then(() => 39 | // CONSTRAINT `fk_member_service_service` FOREIGN KEY (`serviceId`) REFERENCES `service` (`id`)' 40 | // KEY `fk_member_service_service` (`serviceId`), \ 41 | queryInterface.addConstraint('member_service', { 42 | type: 'FOREIGN KEY', 43 | name: 'fk_member_service_service', 44 | fields: ['serviceId'], 45 | references: { 46 | table: 'service', 47 | field: 'id', 48 | }, 49 | onUpdate: 'RESTRICT', 50 | onDelete: 'RESTRICT', 51 | }) 52 | ) 53 | } 54 | 55 | async function down({ context: queryInterface }) { 56 | await queryInterface.dropTable('member_service') 57 | } 58 | 59 | export { up, down } 60 | -------------------------------------------------------------------------------- /frontend/src/components/MonitorTable.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | -------------------------------------------------------------------------------- /data/migrations/20230928181621-remove-external-membership-type.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.sequelize.query( 8 | ` 9 | DELETE FROM member 10 | WHERE membershipType = 'external' 11 | `, 12 | { transaction } 13 | ) 14 | 15 | await queryInterface.changeColumn( 16 | 'member', 17 | 'membershipType', 18 | { 19 | type: DataTypes.ENUM('hobbyist', 'professional'), 20 | allowNull: false, 21 | }, 22 | { transaction } 23 | ) 24 | 25 | await transaction.commit() 26 | } catch (err) { 27 | await transaction.rollback() 28 | throw err 29 | } 30 | } 31 | 32 | async function down({ context: queryInterface }) { 33 | const transaction = await queryInterface.sequelize.transaction() 34 | 35 | try { 36 | await queryInterface.changeColumn( 37 | 'member', 38 | 'membershipType', 39 | { 40 | type: DataTypes.ENUM('hobbyist', 'professional', 'external'), 41 | allowNull: false, 42 | }, 43 | { transaction } 44 | ) 45 | 46 | // Recover external members from providers table 47 | await queryInterface.sequelize.query( 48 | ` 49 | INSERT INTO member (id, providerId, websiteUrl, logoUrl, serviceIpAddress, monitorUrl, membershipType, 50 | membershipLevelId, membershipLevelTimestamp, status, 51 | region, latitude, longitude, 52 | createdAt, updatedAt) 53 | SELECT (provider.id, provider.id, provider.websiteUrl, provider.logoUrl, '', '', 'external', 54 | 1, FLOOR(UNIX_TIMESTAMP(provider.createdAt)), 'active', 55 | provider.region, provider.longitude, provider.latitude, 56 | provider.createdAt, provider.updatedAt) 57 | FROM provider 58 | WHERE id IN ( 59 | SELECT provider.id 60 | FROM provider LEFT JOIN member 61 | ON member.id = provider.id AND member.providerId = NULL 62 | ) 63 | `, 64 | { transaction } 65 | ) 66 | 67 | await transaction.commit() 68 | } catch (err) { 69 | await transaction.rollback() 70 | throw err 71 | } 72 | } 73 | 74 | export { up, down } 75 | -------------------------------------------------------------------------------- /frontend/src/components/MonitorList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 72 | -------------------------------------------------------------------------------- /workers/f-update-memberships.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { DataStore } from '../data/data-store.js' 3 | import config from '../config/index.js' 4 | import { ProvidersAggregateRoot } from '../domain/providers.aggregate.js' 5 | import { ServiceEntity } from '../domain/service.entity.js' 6 | import { Logger } from '../lib/utils.js' 7 | 8 | const logger = new Logger('worker:updateMemberships') 9 | 10 | /** 11 | * Intializes {@link DataStore} and injects it to the process 12 | * @param {(ds: DataStore) => Promise} callback 13 | */ 14 | async function withDatastore(callback) { 15 | await callback( 16 | new DataStore({ 17 | pruning: config.pruning, 18 | }) 19 | ) 20 | } 21 | 22 | /** 23 | * Fetches the lists of members contained on some config JSON files and updates those lists 24 | */ 25 | export async function updateMemberships() { 26 | await withDatastore(async (ds) => { 27 | logger.log('Starting update of memberships list') 28 | 29 | try { 30 | const { data: membersList } = await axios.get(config.providers.members) 31 | const { data: externalsList } = await axios.get(config.providers.external) 32 | 33 | const services = (await ds.Service.findAll({ where: { type: 'rpc' } })).map( 34 | (service) => new ServiceEntity(service) 35 | ) 36 | const providers = ProvidersAggregateRoot.fromConfig( 37 | { 38 | ...membersList.members, 39 | ...externalsList.providers, 40 | }, 41 | services 42 | ) 43 | 44 | await providers.providers.reduce(async (thenable, provider) => { 45 | await thenable 46 | 47 | await ds.Provider.upsert(provider.toRecord()) 48 | if (provider.member) { 49 | await ds.Member.upsert(provider.member.toRecord()) 50 | } 51 | }, Promise.resolve()) 52 | 53 | await providers.providerServices.reduce(async (thenable, providerService) => { 54 | await thenable 55 | await ds.ProviderService.upsert(providerService.toRecord()) 56 | }, Promise.resolve()) 57 | 58 | // TODO: Include a check to deactivate services for providers that weren't upserted. 59 | // Check with @dcolley 60 | } catch (err) { 61 | logger.error(`updateMemberships`, err) 62 | } finally { 63 | logger.log('Finished process') 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /data/migrations/20230415193015-create-member-service-node.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('member_service_node', memberServiceNodeModel.definition) 3 | .sequelize 4 | .query( 5 | 'CREATE TABLE `member_service_node` ( \ 6 | `peerId` varchar(64) NOT NULL, \ 7 | `serviceId` varchar(128) NOT NULL, \ 8 | `memberId` varchar(128) NOT NULL, \ 9 | `memberServiceId` int(11) NOT NULL, \ 10 | `name` varchar(128) DEFAULT NULL, \ 11 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 12 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp(), \ 13 | PRIMARY KEY (`peerId`) \ 14 | )' 15 | ) 16 | .then(() => 17 | // KEY `fk_member_service_node_service` (`serviceId`), \ 18 | // CONSTRAINT `fk_member_service_node_service` FOREIGN KEY (`serviceId`) REFERENCES `service` (`id`) \ 19 | queryInterface.addConstraint('member_service_node', { 20 | type: 'FOREIGN KEY', 21 | name: 'fk_member_service_node_service', 22 | fields: ['serviceId'], 23 | references: { 24 | table: 'service', 25 | field: 'id', 26 | }, 27 | onUpdate: 'RESTRICT', 28 | onDelete: 'RESTRICT', 29 | }) 30 | ) 31 | .then(() => 32 | // KEY `fk_member_service_node_member` (`memberId`), \ 33 | // CONSTRAINT `fk_member_service_node_member` FOREIGN KEY (`memberId`) REFERENCES `member` (`id`), \ 34 | queryInterface.addConstraint('member_service_node', { 35 | type: 'FOREIGN KEY', 36 | name: 'fk_member_service_node_member', 37 | fields: ['memberId'], 38 | references: { 39 | table: 'member', 40 | field: 'id', 41 | }, 42 | onUpdate: 'RESTRICT', 43 | onDelete: 'RESTRICT', 44 | }) 45 | ) 46 | .then(() => 47 | queryInterface.addConstraint('member_service_node', { 48 | type: 'FOREIGN KEY', 49 | name: 'fk_member_service_node_member_service', 50 | fields: ['memberServiceId'], 51 | references: { 52 | table: 'member_service', 53 | field: 'id', 54 | }, 55 | onUpdate: 'RESTRICT', 56 | onDelete: 'RESTRICT', 57 | }) 58 | ) 59 | } 60 | 61 | async function down({ context: queryInterface }) { 62 | await queryInterface.dropTable('member_service_node') 63 | } 64 | 65 | export { up, down } 66 | -------------------------------------------------------------------------------- /lib/utils/logger.js: -------------------------------------------------------------------------------- 1 | import { Chalk } from 'chalk' 2 | import { makeTaggedTemplate } from 'chalk-template' 3 | 4 | export class Logger { 5 | /** @type {String} */ #component 6 | /** @type {(template: TemplateStringsArray, ...placeholders: unknown[]) => string} */ #template 7 | 8 | static logger = new Logger() 9 | 10 | /** 11 | * Initializes the logger 12 | * @param {String} component 13 | */ 14 | constructor(component = '') { 15 | this.#component = component 16 | this.#template = makeTaggedTemplate(new Chalk()) 17 | } 18 | 19 | #header(level, color) { 20 | let template = this.#template 21 | let component = this.#component 22 | return template`{bg${color}.underline.bold ${level}${ 23 | component !== '' ? `[${component}]` : '' 24 | }:}` 25 | } 26 | 27 | /** 28 | * @param {String} color 29 | * @param {String} message 30 | * @returns 31 | */ 32 | #message(level, color, message) { 33 | let template = this.#template 34 | return template`${this.#header(level, color)} ${message}` 35 | } 36 | 37 | /** 38 | * @param {string} message 39 | * @param {...any} optionalParams 40 | */ 41 | info(message, ...optionalParams) { 42 | console.debug(this.#message('debug', 'Blue', message), ...optionalParams) 43 | } 44 | 45 | /** 46 | * @param {string} message 47 | * @param {...any} optionalParams 48 | */ 49 | assert(message, ...optionalParams) { 50 | console.assert(this.#message('debug', 'Magenta', message), ...optionalParams) 51 | } 52 | 53 | /** 54 | * @param {string} message 55 | * @param {...any} optionalParams 56 | */ 57 | debug(message, ...optionalParams) { 58 | console.debug(this.#message('debug', 'Black', message), ...optionalParams) 59 | } 60 | 61 | /** 62 | * @param {string} message 63 | * @param {...any} optionalParams 64 | */ 65 | log(message, ...optionalParams) { 66 | console.log(this.#message('log', 'Gray', message), ...optionalParams) 67 | } 68 | 69 | /** 70 | * @param {string} message 71 | * @param {...any} optionalParams 72 | */ 73 | warn(message, ...optionalParams) { 74 | console.warn(this.#message('warn', 'Yellow', message), ...optionalParams) 75 | } 76 | 77 | /** 78 | * @param {string} message 79 | * @param {...any} optionalParams 80 | */ 81 | error(message, ...optionalParams) { 82 | console.error(this.#message('error', 'Red', message), ...optionalParams) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /boot-nodes/README.md: -------------------------------------------------------------------------------- 1 | # tests for boot nodes 2 | 3 | ## Dependencies 4 | 5 | ```bash 6 | git clone https://github.com/paritytech/polkadot 7 | # build the executable 8 | cargo build --release 9 | 10 | git clone https://github.com/paritytech/cumulus 11 | # build the executable 12 | cargo build --release --bin polkadot-parachain 13 | 14 | git clone https://github.com/encointer/encointer-parachain 15 | # build the executable 16 | cargo build --release 17 | ``` 18 | 19 | 20 | ```js 21 | let commands = { 22 | polkadot: { exec: '/path/to/polkadot', params: '--chain ${CHAIN} --tmp --name "IBP Bootnode test" --reserved-only --reserved-nodes ${RESERVED_NODE}' }, 23 | // kusama: '/path/to/polkadot', 24 | // westend: '/path/to/polkadot', 25 | parachain: { exec: '/path/to/polkadot-parachain', params: [] }, 26 | encointer: { exec: '/path/to/encointer-collator', params: '--chain encointer-kusama --tmp --reserved-only --reserved-nodes ${RESERVED_NODE}' }, 27 | } 28 | 29 | let bootNodes = { 30 | polkadot: { 31 | exec: 'polkadot', 32 | endpoints: { 33 | metaspan: ["/dns/boot-polkadot.metaspan.io/tcp/13012/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF"], 34 | metaspan: ["/dns/boot-polkadot.metaspan.io/tcp/13012/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF"] 35 | } 36 | }, 37 | kusama: { 38 | exec: 'polkadot', 39 | endpoints: { 40 | metaspan: ["/dns/boot-kusama.metas"] 41 | } 42 | }, 43 | westend: { 44 | exec: 'polkadot', 45 | endpoints: { 46 | metaspan: [] 47 | } 48 | }, 49 | 'encointer-kusama': { 50 | exec: 'encointer', 51 | endpoints: { 52 | metaspan: ["/dns/boot.metaspan.io/tcp/26072/p2p/12D3KooWPtjFu99oadjbtbK33iir1jdYVdkEEs3GYV6nswJzwx8W", 53 | "/dns/boot.metaspan.io/tcp/26076/wss/p2p/12D3KooWPtjFu99oadjbtbK33iir1jdYVdkEEs3GYV6nswJzwx8W"], 54 | stakeplus: ["/dns/boot.stake.plus/tcp/36333/p2p/12D3KooWNFFdJFV21haDiSdPJ1EnGmv6pa2TgB81Cvu7Y96hjTAu"] 55 | } 56 | }, 57 | } 58 | ``` 59 | 60 | ```sh 61 | polkadot --chain polkadot --tmp \ 62 | --name "Bootnode testnode" \ 63 | --reserved-only \ 64 | --reserved-nodes "/dns/boot-polkadot.metaspan.io/tcp/13012/p2p/12D3KooWRjHFApinuqSBjoaDjQHvxwubQSpEVy5hrgC9Smvh92WF" \ 65 | --no-hardware-benchmarks 66 | ``` 67 | 68 | ## Encointer 69 | 70 | ```sh 71 | ./encointer-collator --chain encointer-kusama --tmp \ 72 | --reserved-only \ 73 | --reserved-nodes "/dns/boot.stake.plus/tcp/36333/p2p/12D3KooWNFFdJFV21haDiSdPJ1EnGmv6pa2TgB81Cvu7Y96hjTAu" 74 | ``` 75 | -------------------------------------------------------------------------------- /frontend/src/store/modules/libp2p.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import Vue from 'vue' 3 | import { Module } from 'vuex' 4 | import { IState as IRootState } from '../index' 5 | 6 | // eslint-disable-next-line 7 | interface IState { 8 | // list: any[] 9 | // offset: number 10 | // limit: number 11 | // pagination: any 12 | // healthCheck: any 13 | // healthChecks: any[] 14 | } 15 | 16 | // Vue.use(Vuex) 17 | 18 | const libp2p: Module = { 19 | namespaced: true, 20 | state: { 21 | // list: [], 22 | // offset: 0, 23 | // limit: 15, 24 | // healthCheck: {}, 25 | // pagination: { pages: [] } 26 | // // healthChecks: [] 27 | }, 28 | mutations: { 29 | // SET_LIST (state: IState, list: any[]) { 30 | // state.list = list 31 | // }, 32 | // SET_PAGINATION (state: IState, value: any) { 33 | // state.pagination = value 34 | // }, 35 | // SET_OFFSET (state: IState, value: number) { 36 | // state.offset = value 37 | // }, 38 | // SET_LIMIT (state: IState, value: number) { 39 | // state.limit = value 40 | // }, 41 | // SET_HEALTH_CHECK (state: IState, value: any) { 42 | // console.debug('SET_HEALTH_CHECK()', value) 43 | // state.healthCheck = value 44 | // } 45 | }, 46 | actions: { 47 | // async getList ({ state, commit, dispatch }: any, { offset, limit }: any) { 48 | // if (offset) { commit('SET_OFFSET', offset) } 49 | // if (limit) { commit('SET_LIMIT', limit) } 50 | // const res = await axios.get('/api/healthCheck', { params: { offset: offset || state.offset, limit: limit || state.limit } }) 51 | // commit('SET_LIST', res.data.models) 52 | // commit('SET_PAGINATION', res.data.pagination) 53 | // dispatch('setLocalMonitorId', res.data.localMonitorId, { root: true }) 54 | // }, 55 | // async setHealthCheck ({ state, commit }: any, monitorId: string) { 56 | // const res = await axios.get(`/api/healthCheck/${monitorId}`) 57 | // commit('SET_HEALTH_CHECK', res.data.model) 58 | // } 59 | peerDiscovery({ commit }: any, event: any) { 60 | console.debug('peerDiscovery()', event.detail.id.toString()) 61 | }, 62 | peerConnect({ commit }: any, event: any) { 63 | console.debug('peerConnect()', event.detail.remotePeer.toString()) 64 | }, 65 | peerDisconnect({ commit }: any, event: any) { 66 | console.debug('peerDisconnect()', event) 67 | }, 68 | pubsubMessage({ commit }: any, message: any) { 69 | console.debug('peerDisconnect()', message) 70 | }, 71 | }, 72 | } 73 | 74 | export default libp2p 75 | -------------------------------------------------------------------------------- /frontend/src/components/SideNav.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 90 | -------------------------------------------------------------------------------- /docker/DOCKER.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | builing the docker images 4 | 5 | ## Configure docker (desktop) 6 | 7 | Settings > Docker Engine : Add insecure registries: 8 | ```json 9 | { 10 | "insecure-registries": ["192.168.1.2:5002"] 11 | } 12 | ``` 13 | 14 | ## Build & deploy process 15 | ```bash 16 | # docker login -u docker -p d0ck3r localhost:5050 17 | REGISTRY=docker.metaspan.io 18 | docker login -u docker -p d0ck3r ${REGISTRY} 19 | 20 | # https://stackoverflow.com/questions/53416685/docker-compose-tagging-and-pushing 21 | 22 | COMPOSE_DOCKER_CLI_BUILD=1 23 | DOCKER_BUILDKIT=1 24 | # if building on mac, set this to linux/amd64 25 | DOCKER_DEFAULT_PLATFORM=linux/amd64 26 | # docker-compose build 27 | 28 | docker compose build # --pull # if you want to pull the latest images 29 | docker compose push 30 | docker compose buildx build --pull --push 31 | 32 | 33 | ## Services to build 34 | 35 | - ibp-redis 36 | docker compose buildx build --platform linux/amd64 -t ${REGISTRY}/ibp/ibp-redis -f Dockerfile.ibp.services --push ../. 37 | 38 | - ibp-datastore 39 | - ibp-datastore-init 40 | - ibp-monitor-api 41 | - ibp-monitor-server 42 | - ibp-monitor-workers 43 | - ibp-monitor-frontend 44 | 45 | 46 | 47 | # these can't be run together 48 | # --load: load the image locally, this should make subsequent builds faster 49 | # --push: push the image to the registry 50 | 51 | docker buildx build --platform linux/amd64 -t ${REGISTRY}/ibp/subledgr-api -f Dockerfile.ibp.services --push ../. 52 | # docker build --platform linux/amd64 -t ${REGISTRY}/subledgr/subledgr-api -f Dockerfile.api --push ../. 53 | # docker tag subledgr/subledgr-api ${REGISTRY}/subledgr/subledgr-api 54 | # docker push ${REGISTRY}/subledgr/subledgr-api 55 | 56 | docker buildx build --platform linux/amd64 -t ${REGISTRY}/subledgr/subledgr-fe -f Dockerfile.frontend --push ../. 57 | # docker tag subledgr/subledgr-fe ${REGISTRY}/subledgr/subledgr-fe 58 | # docker push ${REGISTRY}/subledgr/subledgr-fe 59 | 60 | # docker build --platform linux/amd64 -t subledgr/subledgr-datastore -f Dockerfile.postgres ../. 61 | # docker tag subledgr/subledgr-datastore ${REGISTRY}/subledgr/subledgr-datastore 62 | # docker push ${REGISTRY}/subledgr/subledgr-datastore 63 | 64 | # Updates in portainer 65 | # for each service (in the stack), open the service and 66 | # > Click Update the service 67 | # > Select Re-pull image 68 | # > Click Update 69 | # Double-check the environment & secrets are still in place 70 | 71 | ``` 72 | 73 | ## GraphQL Express 74 | ```bash 75 | docker build -t subledgr-api -f Dockerfile.graphql-express ../ 76 | ``` 77 | 78 | ## Setup the local registry 79 | 80 | https://www.blackvoid.club/private-docker-registry-with-portainer/ 81 | 82 | -------------------------------------------------------------------------------- /data/migrations/20230928172000-create-provider.js: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.createTable( 8 | 'provider', 9 | { 10 | id: { 11 | type: DataTypes.STRING(128), 12 | allowNull: false, 13 | primaryKey: true, 14 | }, 15 | name: { 16 | type: DataTypes.STRING(128), 17 | allowNull: false, 18 | }, 19 | websiteUrl: { 20 | type: DataTypes.STRING(256), 21 | allowNull: true, 22 | }, 23 | logoUrl: { 24 | type: DataTypes.STRING(256), 25 | allowNull: true, 26 | }, 27 | status: { 28 | type: DataTypes.ENUM('active', 'pending'), 29 | allowNull: false, 30 | }, 31 | region: { 32 | type: DataTypes.ENUM( 33 | '', 34 | 'africa', 35 | 'asia', 36 | 'central_america', 37 | 'europe', 38 | 'middle_east', 39 | 'north_america', 40 | 'oceania' 41 | ), 42 | allowNull: true, 43 | }, 44 | latitude: { 45 | type: DataTypes.FLOAT, 46 | allowNull: false, 47 | }, 48 | longitude: { 49 | type: DataTypes.FLOAT, 50 | allowNull: false, 51 | }, 52 | createdAt: { 53 | type: DataTypes.DATE, 54 | allowNull: false, 55 | defaultValue: Sequelize.fn('now'), 56 | }, 57 | updatedAt: { 58 | type: DataTypes.DATE, 59 | allowNull: false, 60 | defaultValue: Sequelize.fn('now'), 61 | }, 62 | }, 63 | { 64 | timestamps: true, 65 | createdAt: true, 66 | updatedAt: true, 67 | defaultScope: { 68 | attributes: { 69 | exclude: [], 70 | }, 71 | order: [['id', 'ASC']], 72 | }, 73 | transaction, 74 | } 75 | ) 76 | 77 | await queryInterface.sequelize.query( 78 | ` 79 | INSERT INTO provider 80 | (id, name, websiteUrl, logoUrl, status, region, latitude, longitude, createdAt, updatedAt) 81 | SELECT id, name, websiteUrl, logoUrl, status, region, latitude, longitude, createdAt, updatedAt 82 | FROM member 83 | `, 84 | { transaction } 85 | ) 86 | 87 | await transaction.commit() 88 | } catch (err) { 89 | await transaction.rollback() 90 | throw err 91 | } 92 | } 93 | 94 | async function down({ context: queryInterface }) { 95 | await queryInterface.dropTable('provider') 96 | } 97 | 98 | export { up, down } 99 | -------------------------------------------------------------------------------- /domain/provider.entity.js: -------------------------------------------------------------------------------- 1 | import { MemberEntity } from './member.entity.js' 2 | 3 | /** 4 | * Represents an entity that might (or not) be a member of the IBP network, 5 | * and that hosts nodes for the services provided by the network 6 | */ 7 | export class ProviderEntity { 8 | /** @type {String} */ id 9 | /** @type {String} */ name 10 | /** @type {String} */ websiteUrl 11 | /** @type {String} */ logoUrl 12 | 13 | /** @type {'active' | 'pending'} */ status 14 | 15 | /** @type {'' | 'africa' | 'asia' | 'central_america' | 'europe' | 'middle_east' | 'north_america' | 'oceania'} */ 16 | region 17 | 18 | /** @type {Number} */ latitude 19 | /** @type {Number} */ longitude 20 | 21 | /** @type {MemberEntity} */ member 22 | 23 | /** 24 | * @param {ProviderEntity} initializator 25 | * @param {MemberEntity} member 26 | */ 27 | constructor({ id, name, websiteUrl, logoUrl, status, region, latitude, longitude }, member) { 28 | this.id = id 29 | this.name = name 30 | this.websiteUrl = websiteUrl 31 | this.logoUrl = logoUrl 32 | this.status = status 33 | this.region = region 34 | this.latitude = latitude 35 | this.longitude = longitude 36 | this.member = member 37 | } 38 | 39 | /** 40 | * Takes a config object as input (typically from a JSON file) and 41 | * turns it into a member entity 42 | * @param {Record} configObject 43 | * @returns {ProviderEntity} 44 | */ 45 | static fromConfig(configObject, isMember) { 46 | const { 47 | id, 48 | name = configObject.id, 49 | website = null, 50 | logo = null, 51 | active = '0', 52 | region = '', 53 | latitude = '0.0000', 54 | longitude = '0.0000', 55 | } = configObject 56 | 57 | return new ProviderEntity( 58 | { 59 | id, 60 | name, 61 | websiteUrl: website, 62 | logoUrl: logo, 63 | status: !!Number(active) ? 'active' : 'pending', 64 | region, 65 | latitude: Number(latitude), 66 | longitude: Number(longitude), 67 | }, 68 | isMember ? MemberEntity.fromConfig(configObject) : undefined 69 | ) 70 | } 71 | 72 | /** 73 | * Takes a Member object from the data store, and converts it into a 74 | * {@link ProviderEntity} 75 | * @param {Record} configObject 76 | * @returns {ProviderEntity} 77 | */ 78 | static fromDataStore(record) { 79 | if (record.member) { 80 | // Eager-loaded as Provider>Member 81 | let { member, ...provider } = record 82 | return new ProviderEntity(provider, new MemberEntity(member)) 83 | } else { 84 | // Eager-loaded as Provider>Member 85 | let { provider, ...member } = record 86 | return new ProviderEntity(provider, new MemberEntity(member)) 87 | } 88 | } 89 | 90 | toRecord() { 91 | return this 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /frontend/src/components/ServiceTable.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 82 | -------------------------------------------------------------------------------- /frontend/public/image/prometheus-logo-orange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /frontend/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 83 | 84 | 93 | -------------------------------------------------------------------------------- /workers.js: -------------------------------------------------------------------------------- 1 | import cfg from './config/index.js' 2 | 3 | import express from 'express' 4 | import { Queue, Worker } from 'bullmq' 5 | import * as pkg1 from '@bull-board/express' 6 | const { ExpressAdapter } = pkg1 7 | import * as pkg2 from '@bull-board/api' 8 | const { createBullBoard } = pkg2 9 | import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js' 10 | 11 | import { asyncForeach } from './lib/utils.js' 12 | import { checkService } from './workers/f-check-service.js' 13 | import { updateMemberships } from './workers/f-update-memberships.js' 14 | 15 | console.debug('cfg.redis', cfg.redis) 16 | 17 | const qOpts = { 18 | connection: cfg.redis, 19 | } 20 | 21 | async function onError(job, err) { 22 | const errStr = `ERROR: ${job}: ` + typeof err === 'string' ? err : JSON.stringify(err) 23 | // await axios.get('http://192.168.1.2:1880/sendToTelegram?text='+ errStr) 24 | console.log(errStr) 25 | } 26 | 27 | async function onFailed(job, event) { 28 | const errStr = `FAILED: ${job}: ` + typeof event === 'string' ? event : JSON.stringify(event) 29 | // await axios.get('http://192.168.1.2:1880/sendToTelegram?text='+ errStr) 30 | console.log(errStr) 31 | } 32 | 33 | const q_checkService = new Queue('checkService', qOpts) 34 | const q_updateMemberships = new Queue('updateMemberships', qOpts) 35 | 36 | const workers = [ 37 | new Worker('checkService', checkService, { ...qOpts, concurrency: 1 }), 38 | new Worker('updateMemberships', updateMemberships, qOpts), 39 | ] 40 | 41 | const jobs = workers.map((w) => w.name) 42 | 43 | // Call updateMemberships repeteadely 44 | q_updateMemberships.add( 45 | 'updateMemberships', 46 | {}, 47 | { 48 | repeat: { 49 | pattern: '0 0 * * *', 50 | }, 51 | } 52 | ) 53 | 54 | // handle all error/failed 55 | workers.forEach((worker) => { 56 | const job = worker.name 57 | worker.on('error', (err) => onError(job, err)) 58 | worker.on('failed', (event) => onFailed(job, event)) 59 | }) 60 | 61 | async function clearQueue(jobname) { 62 | let qname = eval(`q_${jobname}`) 63 | await qname.pause() 64 | // // Removes all jobs that are waiting or delayed, but not active, completed or failed 65 | // await qname.drain() 66 | // Completely obliterates a queue and all of its contents. 67 | await qname.resume() 68 | } 69 | 70 | ;(async () => { 71 | // on startup, drain the queues and start again 72 | async function clearQueues() { 73 | await asyncForeach(jobs, clearQueue) 74 | } 75 | 76 | await clearQueues() 77 | 78 | const serverAdapter = new ExpressAdapter() 79 | serverAdapter.setBasePath('/admin/queues') 80 | // const queueMQ = new QueueMQ() 81 | const { setQueues, replaceQueues } = createBullBoard({ 82 | queues: [ 83 | new BullMQAdapter(q_checkService, { readOnlyMode: false }), 84 | new BullMQAdapter(q_updateMemberships, { readOnlyMode: false }), 85 | ], 86 | serverAdapter: serverAdapter, 87 | }) 88 | const app = express() 89 | app.use('/admin/queues', serverAdapter.getRouter()) 90 | app.listen(3000, () => { 91 | console.log('Running on 3000...') 92 | console.log('For the UI, open http://localhost:3000/admin/queues') 93 | }) 94 | })() 95 | -------------------------------------------------------------------------------- /data/migrations/20230928184941-remove-member-redundant-fields.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.removeColumn('member', 'name', { transaction }) 8 | await queryInterface.removeColumn('member', 'websiteUrl', { transaction }) 9 | await queryInterface.removeColumn('member', 'logoUrl', { transaction }) 10 | await queryInterface.removeColumn('member', 'region', { transaction }) 11 | await queryInterface.removeColumn('member', 'latitude', { transaction }) 12 | await queryInterface.removeColumn('member', 'longitude', { transaction }) 13 | 14 | await transaction.commit() 15 | } catch (err) { 16 | await transaction.rollback() 17 | throw err 18 | } 19 | } 20 | 21 | async function down({ context: queryInterface }) { 22 | const transaction = await queryInterface.sequelize.transaction() 23 | 24 | try { 25 | await queryInterface.addColumn('member', 'name', { 26 | type: DataTypes.STRING(128), 27 | allowNull: true, 28 | after: 'providerId', 29 | 30 | transaction, 31 | }) 32 | await queryInterface.addColumn('member', 'websiteUrl', { 33 | type: DataTypes.STRING(256), 34 | allowNull: true, 35 | after: 'name', 36 | 37 | transaction, 38 | }) 39 | await queryInterface.addColumn('member', 'logoUrl', { 40 | type: DataTypes.STRING(256), 41 | allowNull: true, 42 | after: 'websiteUrl', 43 | 44 | transaction, 45 | }) 46 | await queryInterface.addColumn('member', 'region', { 47 | type: DataTypes.ENUM( 48 | '', 49 | 'africa', 50 | 'asia', 51 | 'central_america', 52 | 'europe', 53 | 'middle_east', 54 | 'north_america', 55 | 'oceania' 56 | ), 57 | allowNull: true, 58 | after: 'status', 59 | 60 | transaction, 61 | }) 62 | await queryInterface.addColumn('member', 'latitude', { 63 | type: DataTypes.FLOAT, 64 | allowNull: false, 65 | after: 'region', 66 | 67 | transaction, 68 | }) 69 | await queryInterface.addColumn('member', 'longitude', { 70 | type: DataTypes.FLOAT, 71 | allowNull: false, 72 | after: 'latitude', 73 | 74 | transaction, 75 | }) 76 | 77 | // Copy information back to members from providers table 78 | await queryInterface.sequelize.query( 79 | ` 80 | UPDATE member m 81 | INNER JOIN provider p 82 | ON m.providerId = p.id 83 | SET 84 | name = provider.name, 85 | websiteUrl = provider.websiteUrl, 86 | logoUrl = provider.logoUrl, 87 | region = provider.region, 88 | longitude = provider.longitude, 89 | latitude = provider.latitude, 90 | `, 91 | { transaction } 92 | ) 93 | 94 | await transaction.commit() 95 | } catch (err) { 96 | await transaction.rollback() 97 | throw err 98 | } 99 | } 100 | 101 | export { up, down } 102 | -------------------------------------------------------------------------------- /frontend/src/components/ServiceList.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 91 | 92 | 97 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | // Composables 2 | import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router' 3 | 4 | import Home from '@/components/Home.vue' 5 | import Config from '@/components/Config.vue' 6 | import Services from '@/components/Services.vue' 7 | import Members from '@/components/Members.vue' 8 | import Member from '@/components/Member.vue' 9 | import Service from '@/components/Service.vue' 10 | import Monitors from '@/components/Monitors.vue' 11 | import Monitor from '@/components/Monitor.vue' 12 | import Checks from '@/components/Checks.vue' 13 | import Check from '@/components/Check.vue' 14 | import Message from '@/components/Message.vue' 15 | import Status from '@/components/Status.vue' 16 | 17 | const routes: RouteRecordRaw[] = [ 18 | // { 19 | // path: '/', 20 | // component: () => import('@/layouts/default/Default.vue'), 21 | // children: [ 22 | // { 23 | // path: '', 24 | // name: 'Home', 25 | // // route level code-splitting 26 | // // this generates a separate chunk (about.[hash].js) for this route 27 | // // which is lazy-loaded when the route is visited. 28 | // component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'), 29 | // }, 30 | // ], 31 | // }, 32 | { 33 | path: '/', 34 | name: 'home', 35 | component: Home, 36 | }, 37 | { 38 | path: '/config', 39 | name: 'config', 40 | component: Config, 41 | }, 42 | { 43 | path: '/member', 44 | name: 'Members', 45 | component: Members, 46 | }, 47 | { 48 | path: '/member/:memberId', 49 | name: 'Member', 50 | component: Member, 51 | }, 52 | { 53 | path: '/service', 54 | name: 'Services', 55 | component: Services, 56 | }, 57 | { 58 | path: '/service/:serviceId', 59 | name: 'Service', 60 | component: Service, 61 | props: true, 62 | }, 63 | { path: '/monitor', name: 'Monitors', component: Monitors }, 64 | { path: '/monitor/:monitorId', name: 'Monitor', component: Monitor, props: true }, 65 | { 66 | path: '/healthCheck', 67 | name: 'Checks', 68 | component: Checks, 69 | }, 70 | { 71 | path: '/healthCheck/non-members', 72 | name: 'NonMemberChecks', 73 | component: Checks, 74 | }, 75 | { 76 | path: '/healthCheck/:id', 77 | name: 'Check', 78 | component: Check, 79 | props: true, 80 | }, 81 | { 82 | path: '/sign', 83 | name: 'Sign', 84 | component: Message, 85 | }, 86 | { 87 | path: '/status', 88 | name: 'Status', 89 | component: Status, 90 | }, 91 | // { 92 | // path: '/about', 93 | // name: 'about', 94 | // // route level code-splitting 95 | // // this generates a separate chunk (about.[hash].js) for this route 96 | // // which is lazy-loaded when the route is visited. 97 | // component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue') 98 | // } 99 | ] 100 | 101 | const router = createRouter({ 102 | history: createWebHistory(process.env.BASE_URL), 103 | routes, 104 | }) 105 | 106 | export default router 107 | -------------------------------------------------------------------------------- /data/migrations/20230928185952-rename-memberId-columns-to-providerId.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction() 5 | 6 | try { 7 | await queryInterface.addColumn('health_check', 'providerId', { 8 | type: DataTypes.STRING(128), 9 | after: 'serviceId', 10 | allowNull: false, 11 | 12 | transaction, 13 | }) 14 | await queryInterface.changeColumn('health_check', 'memberId', { 15 | type: DataTypes.STRING(128), 16 | after: 'providerId', 17 | allowNull: true, 18 | 19 | transaction, 20 | }) 21 | 22 | await queryInterface.renameColumn('member_service_node', 'memberId', 'providerId', { 23 | transaction, 24 | }) 25 | await queryInterface.renameColumn('member_service', 'memberId', 'providerId', { transaction }) 26 | 27 | await queryInterface.addConstraint('health_check', { 28 | type: 'FOREIGN KEY', 29 | name: 'fk_health_check_provider', 30 | fields: ['providerId'], 31 | references: { 32 | table: 'provider', 33 | field: 'id', 34 | }, 35 | onUpdate: 'RESTRICT', 36 | onDelete: 'RESTRICT', 37 | 38 | transaction, 39 | }) 40 | await queryInterface.addConstraint('member_service_node', { 41 | type: 'FOREIGN KEY', 42 | name: 'fk_member_service_node_provider', 43 | fields: ['providerId'], 44 | references: { 45 | table: 'provider', 46 | field: 'id', 47 | }, 48 | onUpdate: 'RESTRICT', 49 | onDelete: 'RESTRICT', 50 | 51 | transaction, 52 | }) 53 | await queryInterface.addConstraint('member_service', { 54 | type: 'FOREIGN KEY', 55 | name: 'fk_member_service_provider', 56 | fields: ['providerId'], 57 | references: { 58 | table: 'provider', 59 | field: 'id', 60 | }, 61 | onUpdate: 'RESTRICT', 62 | onDelete: 'RESTRICT', 63 | 64 | transaction, 65 | }) 66 | 67 | await transaction.commit() 68 | } catch (err) { 69 | await transaction.rollback() 70 | throw err 71 | } 72 | } 73 | 74 | async function down({ context: queryInterface }) { 75 | const transaction = await queryInterface.sequelize.transaction() 76 | 77 | try { 78 | await queryInterface.removeConstraint('health_check', 'fk_health_check_provider', { 79 | transaction, 80 | }) 81 | await queryInterface.removeConstraint( 82 | 'member_service_node', 83 | 'fk_member_service_node_provider', 84 | { 85 | transaction, 86 | } 87 | ) 88 | await queryInterface.removeConstraint('member_service', 'fk_member_service_provider', { 89 | transaction, 90 | }) 91 | 92 | await queryInterface.renameColumn('health_check', 'providerId', 'memberId', { transaction }) 93 | await queryInterface.renameColumn('member_service_node', 'providerId', 'memberId', { 94 | transaction, 95 | }) 96 | await queryInterface.renameColumn('member_service', 'providerId', 'memberId', { transaction }) 97 | 98 | await transaction.commit() 99 | } catch (err) { 100 | await transaction.rollback() 101 | throw err 102 | } 103 | } 104 | 105 | export { up, down } 106 | -------------------------------------------------------------------------------- /data/migrations/20230415202600-create-health-check.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface// .createTable('health_check', healthCheckModel.definition) 3 | .sequelize 4 | .query( 5 | "CREATE TABLE `health_check` ( \ 6 | `id` int(11) NOT NULL AUTO_INCREMENT, \ 7 | `monitorId` varchar(64) NOT NULL, \ 8 | `serviceId` varchar(128) NOT NULL, \ 9 | `memberId` varchar(128) NOT NULL, \ 10 | `peerId` varchar(64) DEFAULT NULL, \ 11 | `source` enum('check','gossip') NOT NULL, \ 12 | `type` enum('service_check','system_health','best_block','bootnode_check') NOT NULL, \ 13 | `status` enum('error','warning','success') NOT NULL, \ 14 | `responseTimeMs` int(11) DEFAULT NULL, \ 15 | `record` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`record`)), \ 16 | `createdAt` datetime NOT NULL DEFAULT current_timestamp(), \ 17 | PRIMARY KEY (`id`) \ 18 | )" 19 | ) 20 | // KEY `health_check_created_at` (`createdAt`) \ 21 | .then(() => 22 | // CONSTRAINT `fk_health_check_monitor` FOREIGN KEY (`monitorId`) REFERENCES `monitor` (`id`), \ 23 | // KEY `fk_health_check_monitor` (`monitorId`), \ 24 | queryInterface.addConstraint('health_check', { 25 | type: 'FOREIGN KEY', 26 | name: 'fk_health_check_monitor', 27 | fields: ['monitorId'], 28 | references: { 29 | table: 'monitor', 30 | field: 'id', 31 | }, 32 | onUpdate: 'RESTRICT', 33 | onDelete: 'RESTRICT', 34 | }) 35 | ) 36 | .then(() => 37 | // CONSTRAINT `fk_health_check_service` FOREIGN KEY (`serviceId`) REFERENCES `service` (`id`) 38 | // KEY `fk_health_check_service` (`serviceId`), \ 39 | queryInterface.addConstraint('health_check', { 40 | type: 'FOREIGN KEY', 41 | name: 'fk_health_check_service', 42 | fields: ['serviceId'], 43 | references: { 44 | table: 'service', 45 | field: 'id', 46 | }, 47 | onUpdate: 'RESTRICT', 48 | onDelete: 'RESTRICT', 49 | }) 50 | ) 51 | .then(() => 52 | // CONSTRAINT `fk_health_check_member` FOREIGN KEY (`memberId`) REFERENCES `member` (`id`), \ 53 | // KEY `fk_health_check_member` (`memberId`), \ 54 | queryInterface.addConstraint('health_check', { 55 | type: 'FOREIGN KEY', 56 | name: 'fk_health_check_member', 57 | fields: ['memberId'], 58 | references: { 59 | table: 'member', 60 | field: 'id', 61 | }, 62 | onUpdate: 'RESTRICT', 63 | onDelete: 'RESTRICT', 64 | }) 65 | ) 66 | .then(() => 67 | // CONSTRAINT `fk_health_check_member_service_node` FOREIGN KEY (`peerId`) REFERENCES `member_service_node` (`peerId`), \ 68 | // KEY `fk_health_check_member_service_node` (`peerId`), \ 69 | queryInterface.addConstraint('health_check', { 70 | type: 'FOREIGN KEY', 71 | name: 'fk_health_check_member_service_node', 72 | fields: ['peerId'], 73 | references: { 74 | table: 'member_service_node', 75 | field: 'peerId', 76 | }, 77 | onUpdate: 'RESTRICT', 78 | onDelete: 'RESTRICT', 79 | }) 80 | ) 81 | } 82 | 83 | async function down({ context: queryInterface }) { 84 | await queryInterface.dropTable('health_check') 85 | } 86 | 87 | export { up, down } 88 | -------------------------------------------------------------------------------- /data/migrations/20230411123348-insert-chains.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('chain', [ 3 | { 4 | id: 'westend', 5 | genesisHash: 'e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', 6 | name: 'Westend', 7 | relayChainId: null, 8 | logoUrl: null, 9 | }, 10 | { 11 | id: 'kusama', 12 | genesisHash: 'b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', 13 | name: 'Kusama', 14 | relayChainId: null, 15 | logoUrl: 'https://parachains.info/images/kusama.png', 16 | }, 17 | { 18 | id: 'polkadot', 19 | genesisHash: '91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', 20 | name: 'Polkadot', 21 | relayChainId: null, 22 | logoUrl: 'https://parachains.info/images/polkadot.png', 23 | }, 24 | { 25 | id: 'westmint', 26 | genesisHash: '67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9', 27 | name: 'Westmint', 28 | relayChainId: 'westend', 29 | logoUrl: 'https://parachains.info/images/parachains/1623939400_statemine_logo.png', 30 | }, 31 | { 32 | id: 'collectives-westend', 33 | genesisHash: '713daf193a6301583ff467be736da27ef0a72711b248927ba413f573d2b38e44', 34 | name: 'Collectives Westend', 35 | relayChainId: 'westend', 36 | logoUrl: 'https://parachains.info/images/parachains/1664976722_collectives_logo.png', 37 | }, 38 | { 39 | id: 'statemine', 40 | genesisHash: '48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a', 41 | name: 'Statemine', 42 | relayChainId: 'kusama', 43 | logoUrl: 'https://parachains.info/images/parachains/1623939400_statemine_logo.png', 44 | }, 45 | { 46 | id: 'bridgehub-kusama', 47 | genesisHash: '00dcb981df86429de8bbacf9803401f09485366c44efbf53af9ecfab03adc7e5', 48 | name: 'Bridge Hub Kusama', 49 | relayChainId: 'kusama', 50 | logoUrl: 'https://parachains.info/images/parachains/1677333455_bridgehub_kusama.svg', 51 | }, 52 | { 53 | id: 'encointer-kusama', 54 | genesisHash: '7dd99936c1e9e6d1ce7d90eb6f33bea8393b4bf87677d675aa63c9cb3e8c5b5b', 55 | name: 'Encointer Kusama', 56 | relayChainId: 'kusama', 57 | logoUrl: 'https://parachains.info/images/parachains/1625163231_encointer_logo.png', 58 | }, 59 | { 60 | id: 'statemint', 61 | genesisHash: '68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f', 62 | name: 'Statemint', 63 | relayChainId: 'polkadot', 64 | logoUrl: 'https://parachains.info/images/parachains/1623939400_statemine_logo.png', 65 | }, 66 | { 67 | id: 'collectives-polkadot', 68 | genesisHash: '46ee89aa2eedd13e988962630ec9fb7565964cf5023bb351f2b6b25c1b68b0b2', 69 | name: 'Collectives Polkadot', 70 | relayChainId: 'polkadot', 71 | logoUrl: 'https://parachains.info/images/parachains/1664976722_collectives_logo.png', 72 | }, 73 | { 74 | id: 'bridgehub-polkadot', 75 | genesisHash: 'dcf691b5a3fbe24adc99ddc959c0561b973e329b1aef4c4b22e7bb2ddecb4464', 76 | name: 'Bridge Hub Polkadot', 77 | relayChainId: 'polkadot', 78 | logoUrl: 'https://parachains.info/images/parachains/1677333524_bridgehub_new.svg', 79 | }, 80 | ]) 81 | } 82 | 83 | async function down({ context: queryInterface }) { 84 | await queryInterface.bulkDelete('chain', null, {}) 85 | } 86 | 87 | export { up, down } 88 | -------------------------------------------------------------------------------- /frontend/src/components/Monitor.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 101 | -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from 'vue' 2 | import { createStore, Store } from 'vuex' 3 | import axios from 'axios' 4 | 5 | import geoDnsPool from './modules/geo-dns-pool' 6 | import member from './modules/member' 7 | import service from './modules/service' 8 | import monitor from './modules/monitor' 9 | import healthCheck from './modules/health-check' 10 | import status from './modules/status' 11 | import libp2p from './modules/libp2p' 12 | 13 | export interface IState { 14 | apiVersion: string 15 | config: Record 16 | showSideBar: boolean 17 | monitorCount: number 18 | memberCount: number 19 | serviceCount: number 20 | checkCount: number 21 | regions: Record 22 | packageVersion: string 23 | dateTimeFormat: string 24 | localMonitorId: string 25 | } 26 | export const key: InjectionKey> = Symbol('$store') 27 | 28 | export const store = createStore({ 29 | state: { 30 | apiVersion: '', 31 | config: {}, 32 | showSideBar: false, 33 | localMonitorId: '', 34 | monitorCount: 0, 35 | memberCount: 0, 36 | serviceCount: 0, 37 | checkCount: 0, 38 | regions: { 39 | africa: { name: 'Africa' }, 40 | central_america: { name: 'Central America' }, 41 | europe: { name: 'Europe' }, 42 | north_america: { name: 'North America' }, 43 | south_america: { name: 'South America' }, 44 | middle_east: { name: 'Middle East' }, 45 | oceania: { name: 'Oceania' }, 46 | asia: { name: 'Asia' }, 47 | }, 48 | packageVersion: process.env.PACKAGE_VERSION || '0', 49 | dateTimeFormat: 'DD/MM/YYYY HH:mm:ss', 50 | }, 51 | getters: {}, 52 | mutations: { 53 | SET_LOCAL_MONITOR_ID(state: IState, value: string) { 54 | state.localMonitorId = value 55 | }, 56 | SET_HOME( 57 | state: IState, 58 | { version, config, localMonitorId, memberCount, monitorCount, serviceCount, checkCount } 59 | ) { 60 | state.apiVersion = version 61 | state.config = config 62 | state.localMonitorId = localMonitorId 63 | state.monitorCount = monitorCount 64 | state.memberCount = memberCount 65 | state.serviceCount = serviceCount 66 | state.checkCount = checkCount 67 | }, 68 | SET_CONFIG(state: IState, config: any) { 69 | state.config = config 70 | }, 71 | SET_SIDE_BAR(state: IState, visible: boolean) { 72 | state.showSideBar = visible 73 | }, 74 | }, 75 | actions: { 76 | async init({ dispatch, commit }) { 77 | dispatch('member/getList', {}, { root: true }) 78 | dispatch('service/getList', {}, { root: true }) 79 | const res = await axios.get('/api/home') 80 | commit('SET_HOME', res.data) 81 | }, 82 | setLocalMonitorId({ commit }: any, value: string) { 83 | commit('SET_LOCAL_MONITOR_ID', value) 84 | }, 85 | async getHome({ commit }) { 86 | const res = await axios.get('/api/home') 87 | commit('SET_HOME', res.data) 88 | }, 89 | async getConfig({ commit }) { 90 | const res = await axios.get('/api/config') 91 | commit('SET_CONFIG', res.data) 92 | }, 93 | setSideBar({ commit }, visible: boolean) { 94 | commit('SET_SIDE_BAR', visible) 95 | }, 96 | toggleSideBar({ state, commit }) { 97 | commit('SET_SIDE_BAR', !state.showSideBar) 98 | }, 99 | }, 100 | modules: { 101 | geoDnsPool, 102 | member, 103 | service, 104 | monitor, 105 | healthCheck, 106 | libp2p, 107 | status, 108 | }, 109 | }) 110 | -------------------------------------------------------------------------------- /domain/providers.aggregate.js: -------------------------------------------------------------------------------- 1 | import { ServiceEntity } from './service.entity.js' 2 | import { ProviderEntity } from './provider.entity.js' 3 | import { ProviderServiceEntity } from './provider-service.entity.js' 4 | 5 | export class ProvidersAggregateRoot { 6 | /** @type {ProviderEntity[]}} */ providers = [] 7 | /** @type {ProviderServiceEntity[]}} */ providerServices = [] 8 | 9 | /** 10 | * Takes a providers list from a config object and retrieves a list of aggregate {@link ProviderServiceEntity}s 11 | * @param {Record} config 12 | * @param {ServiceEntity[]} services 13 | */ 14 | static fromConfig(config, services) { 15 | let root = new ProvidersAggregateRoot() 16 | 17 | Object.entries(config).forEach(([id, providerConfig]) => { 18 | const provider = ProviderEntity.fromConfig( 19 | { id, ...providerConfig }, 20 | 'membership' in providerConfig 21 | ) 22 | 23 | Object.entries(providerConfig.endpoints || {}).forEach((endpointConfig) => { 24 | root.providerServices.push( 25 | ProviderServiceEntity.fromConfig(endpointConfig, services, provider) 26 | ) 27 | }) 28 | 29 | root.providers.push(provider) 30 | }) 31 | 32 | return root 33 | } 34 | 35 | /** 36 | * Creates an aggregate based on the 37 | * @param {ProviderEntity[]} providers 38 | * @param {ServiceEntity[]} services 39 | * @param {{ providerId: string, serviceId: string, serviceUrl: string, status: string }[]} providerServices 40 | */ 41 | static fromDataStore(providers, services, providerServices, filter = () => true) { 42 | const root = new ProvidersAggregateRoot() 43 | 44 | root.providers = providers 45 | root.providerServices = providerServices 46 | .map((providerService) => { 47 | return new ProviderServiceEntity({ 48 | provider: providers.find((provider) => provider.id === providerService.providerId), 49 | service: services.find((service) => service.id === providerService.serviceId), 50 | serviceUrl: providerService.serviceUrl, 51 | status: providerService.status, 52 | }) 53 | }) 54 | .filter(filter) 55 | 56 | return root 57 | } 58 | 59 | /** 60 | * Creates an providers aggregate root based on the mere cross product of the providers and services lists 61 | * @param {ProviderEntity[]} providers 62 | * @param {ServiceEntity[]} services 63 | * @param {(provider: ProviderServiceEntity) => bool} filter 64 | */ 65 | static crossProduct(providers, services, filter = () => true) { 66 | const root = new ProvidersAggregateRoot() 67 | 68 | root.providers = providers 69 | root.providerServices = providers.flatMap((provider) => { 70 | return services 71 | .map((service) => { 72 | return new ProviderServiceEntity({ 73 | provider, 74 | service, 75 | status: service.status, 76 | }) 77 | }) 78 | .filter(filter) 79 | }) 80 | 81 | return root 82 | } 83 | 84 | /** 85 | * Merges two or more providers' aggregate roots 86 | * @param {ProvidersAggregateRoot[]} other 87 | */ 88 | concat(...other) { 89 | const root = new ProvidersAggregateRoot() 90 | 91 | return [this, ...other].reduce((prev, other) => { 92 | prev.providers = Array.from(new Set(prev.providers.concat(other.providers))) 93 | prev.providerServices = Array.from( 94 | new Set(prev.providerServices.concat(other.providerServices)) 95 | ) 96 | 97 | return prev 98 | }, root) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/components/CheckList.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 115 | -------------------------------------------------------------------------------- /workers/f-check-service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Job } from 'bullmq' 4 | import { ApiPromise, WsProvider } from '@polkadot/api' 5 | 6 | import cfg from '../config/index.js' 7 | 8 | import { ProviderServiceEntity } from '../domain/provider-service.entity.js' 9 | 10 | import { HealthChecker, retry, asyncCallWithTimeout } from '../lib/check-service/index.js' 11 | import { Logger } from '../lib/utils.js' 12 | import { SECOND_AS_MILLISECONDS } from '../lib/consts.js' 13 | 14 | const logger = new Logger('worker:checkService') 15 | 16 | /** 17 | * 18 | * @param {Job} job 19 | * @param {Object} params 20 | * @param {HealthChecker} params.healthChecker 21 | * @param {ProviderServiceEntity} params.providerService 22 | */ 23 | async function performCheck(job, { healthChecker, providerService }) { 24 | job.log(`checkService: ${providerService}`) 25 | 26 | /** @type {WsProvider?} */ let provider 27 | /** @type {ApiPromise?} */ let api 28 | try { 29 | provider = await healthChecker.buildProvider() 30 | 31 | job.log('connecting to provider...') 32 | await asyncCallWithTimeout(healthChecker.connectProvider(provider), cfg.checkTimeout) 33 | job.log('provider is ready!') 34 | 35 | job.updateProgress(25) 36 | 37 | api = await healthChecker.buildApi(provider) 38 | 39 | job.log('connecting to api...') 40 | await asyncCallWithTimeout(healthChecker.connectApi(api), cfg.checkTimeout) 41 | job.log('api is ready...') 42 | 43 | job.updateProgress(60) 44 | 45 | logger.log('getting stats from provider / api...') 46 | job.log('getting stats from provider / api...') 47 | const result = await healthChecker.tryCheck(api, cfg.performance?.sla) 48 | 49 | job.updateProgress(100) 50 | 51 | await api?.disconnect() 52 | await provider?.disconnect() 53 | await healthChecker.connectionStrategy.after() 54 | 55 | return result 56 | } catch (err) { 57 | await api?.disconnect() 58 | await provider?.disconnect() 59 | await healthChecker.connectionStrategy.after() 60 | 61 | logger.error(err) 62 | job.log('>> got error for ', healthChecker.connectionStrategy.getEndpoint()) 63 | job.log(err.toString()) 64 | 65 | throw err 66 | } 67 | } 68 | 69 | /** 70 | * Similar to healthCheck-endpoint, but for IBP url at member.services_address 71 | * @param {Job} job 72 | * @returns 73 | */ 74 | export async function checkService(job) { 75 | /** @type {ProviderServiceEntity} */ 76 | const providerService = job.data.providerService 77 | const { 78 | service: { id: serviceId }, 79 | provider: { id: providerId }, 80 | } = providerService 81 | 82 | /** @type {string} */ 83 | const monitorId = job.data.monitorId 84 | 85 | logger.debug(`Checking ${serviceId} for ${providerId}`) 86 | const healthChecker = new HealthChecker({ 87 | monitorId, 88 | providerService, 89 | }) 90 | 91 | try { 92 | const result = await retry( 93 | () => 94 | performCheck(job, { 95 | healthChecker, 96 | providerService, 97 | }), 98 | job, 99 | 3, // retry 3 times 100 | 5 * SECOND_AS_MILLISECONDS 101 | ) 102 | 103 | logger.log(`Done checking ${serviceId} for ${providerId}`) 104 | job.log(`[checkService] Done checking ${serviceId} for ${providerId}`) 105 | 106 | return result 107 | } catch (err) { 108 | logger.warn('WE GOT AN ERROR AFTER RETRIES') 109 | logger.error(err) 110 | job.log('WE GOT AN ERROR AFTER RETRIES --------------') 111 | job.log(err) 112 | job.log(err.toString()) 113 | 114 | logger.log(`Done checking ${serviceId} for ${providerId} with error`) 115 | job.log(`[checkService] Done checking ${serviceId} for ${providerId} with error`) 116 | 117 | return healthChecker.errorCheck() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /frontend/src/components/Check.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 114 | -------------------------------------------------------------------------------- /data/migrations/20230628090200-drop-column-id.js: -------------------------------------------------------------------------------- 1 | import { DataTypes } from 'sequelize' 2 | 3 | async function up({ context: queryInterface }) { 4 | const transaction = await queryInterface.sequelize.transaction(); 5 | try { 6 | // Remove the foreign key constraint 7 | console.log('Removing the foreign key constraint') 8 | await queryInterface 9 | // .removeConstraint('member_service_node', 'member_service_node_ibfk_3') 10 | .sequelize.query(`ALTER TABLE member_service_node DROP CONSTRAINT IF EXISTS member_service_node_ibfk_3;`, { transaction }) 11 | .then(async () => { 12 | await queryInterface.sequelize.query(`ALTER TABLE member_service_node DROP CONSTRAINT IF EXISTS fk_member_service_node_member_service;`, { transaction }) 13 | }) 14 | .then(async () => { 15 | console.log('Dropping column memberServiceId from table member_service_node') 16 | // await queryInterface.removeColumn('member_service_node', 'memberServiceId', { transaction }) 17 | await queryInterface.sequelize.query(`ALTER TABLE member_service_node DROP COLUMN IF EXISTS memberServiceId;`, { transaction }) 18 | }) 19 | .then(async () => { 20 | console.log('Removing the AUTO_INCREMENT property from the id column') 21 | await queryInterface.changeColumn('member_service', 'id', { 22 | type: DataTypes.INTEGER, 23 | allowNull: true, 24 | autoIncrement: false, 25 | primaryKey: false, 26 | }, { transaction }) 27 | }) 28 | // Remove the primary key constraint on column id 29 | .then(async () => { 30 | console.log('Removing the primary key constraint on column id') 31 | await queryInterface.removeConstraint('member_service', 'PRIMARY', { transaction }) 32 | }) 33 | // Populate a temporary table with the distinct records based on memberId and serviceId 34 | .then(async () => { 35 | console.log('Populating a temporary table with the distinct records based on memberId and serviceId') 36 | await queryInterface.sequelize.query(` 37 | CREATE TEMPORARY TABLE temp_member_service AS 38 | SELECT * FROM ( 39 | SELECT *, 40 | ROW_NUMBER() OVER (PARTITION BY memberId, serviceId ORDER BY updatedAt DESC) AS rn 41 | FROM member_service 42 | ) t 43 | WHERE rn = 1; 44 | `, { transaction }) 45 | }) 46 | // Delete the records in the member_service table that do not exist in the temporary table 47 | .then(async () => { 48 | console.log('Deleting the records in the member_service table that do not exist in the temporary table') 49 | await queryInterface.sequelize.query(` 50 | DELETE FROM member_service 51 | WHERE (memberId, serviceId, updatedAt) NOT IN ( 52 | SELECT memberId, serviceId, updatedAt FROM temp_member_service 53 | ); 54 | `, { transaction }) 55 | }) 56 | // Drop column id 57 | .then(async () => { 58 | console.log('Dropping column id from table member_service') 59 | await queryInterface.removeColumn('member_service', 'id', { transaction }) 60 | }) 61 | // Create the new PK constraint 62 | .then(async () => { 63 | console.log('Creating the new PK constraint') 64 | await queryInterface.addConstraint('member_service', { 65 | type: 'primary key', 66 | fields: ['memberId', 'serviceId'], 67 | name: 'member_service_pk', 68 | }, { transaction }) 69 | }) 70 | await transaction.commit(); 71 | 72 | } catch (err) { 73 | // If there is an error, rollback the transaction 74 | await transaction.rollback(); 75 | throw err; 76 | } 77 | 78 | } 79 | 80 | async function down({ context: queryInterface }) { 81 | // In this case, the down migration is hard to implement because we cannot undo the deletion of rows and removal of id column. 82 | // Therefore, it's better to backup your database before running the up migration. 83 | // throw new Error('This migration cannot be undone'); 84 | } 85 | 86 | export { up, down } 87 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | name: ibp-stack 3 | 4 | # PORTS 5 | # - 3000 - bullmq admin, http 6 | # - 3306 - mariadb, tcp 7 | # - 6379 - redis, tcp 8 | # - 30000 - libp2p, gossip, tcp 9 | # - 30001 - frontend, http 10 | # - 30002 - api, http 11 | 12 | services: 13 | ibp-redis: 14 | container_name: ibp-redis 15 | image: redis 16 | restart: unless-stopped 17 | ports: 18 | - '6379:6379' 19 | ibp-datastore: 20 | container_name: ibp-datastore 21 | image: mariadb:10.11.2 22 | restart: unless-stopped 23 | environment: 24 | - MARIADB_ROOT_PASSWORD=thisIsASecret 25 | - MARIADB_USER=ibp_monitor 26 | - MARIADB_PASSWORD=ibp_monitor 27 | - MARIADB_DATABASE=ibp_monitor 28 | ports: 29 | - '3306:3306' 30 | volumes: 31 | - ../data/mariadb:/var/lib/mysql 32 | wait-for-datastore-ready: 33 | container_name: wait-for-datastore-ready 34 | image: jwilder/dockerize:0.6.1 35 | depends_on: 36 | - ibp-datastore 37 | command: 'dockerize -wait=tcp://ibp-datastore:3306 -timeout 60s' 38 | ibp-datastore-init: 39 | container_name: ibp-datastore-init 40 | image: ibp-monitor-services 41 | pull_policy: never 42 | build: &ibp-monitor-services 43 | context: ../. 44 | dockerfile: docker/Dockerfile.ibp.services 45 | restart: "no" 46 | depends_on: 47 | wait-for-datastore-ready: 48 | condition: service_completed_successfully 49 | command: > 50 | bash -c "cd data 51 | && node migrate.js" 52 | ibp-monitor-api: 53 | container_name: ibp-monitor-api 54 | build: *ibp-monitor-services 55 | restart: on-failure 56 | depends_on: 57 | ibp-datastore-init: 58 | condition: service_completed_successfully 59 | ports: 60 | - '30002:30002' 61 | command: > 62 | bash -c "node api.js" 63 | volumes: 64 | # - ibp-keys:/home/ibp/keys 65 | - ../keys:/home/ibp/keys 66 | - ../config:/home/ibp/config 67 | ibp-monitor-server: 68 | container_name: ibp-monitor-server 69 | restart: on-failure 70 | build: *ibp-monitor-services 71 | depends_on: 72 | ibp-datastore-init: 73 | condition: service_completed_successfully 74 | ports: 75 | - '30000:30000' # p2p port 76 | environment: 77 | - P2P_PUBLIC_IP=${P2P_PUBLIC_IP} 78 | - P2P_PUBLIC_HOST=${P2P_PUBLIC_HOST} 79 | command: > 80 | bash -c "node server.js" 81 | volumes: 82 | # - ibp-keys:/home/ibp/keys 83 | - ../keys:/home/ibp/keys 84 | - ../config:/home/ibp/config 85 | wait-for-ibp-server-ready: 86 | container_name: wait-for-ibp-server-ready 87 | image: jwilder/dockerize:0.6.1 88 | depends_on: 89 | - ibp-monitor-server 90 | command: 'dockerize -wait=tcp://ibp-monitor-server:30000 -timeout 60s' 91 | ibp-monitor-workers: 92 | container_name: ibp-monitor-workers 93 | restart: on-failure 94 | build: *ibp-monitor-services 95 | depends_on: 96 | wait-for-ibp-server-ready: 97 | condition: service_completed_successfully 98 | ports: 99 | - '3000:3000' 100 | command: > 101 | bash -c "node workers.js" 102 | wait-for-ibp-api-ready: 103 | container_name: wait-for-ibp-api-ready 104 | image: jwilder/dockerize:0.6.1 105 | depends_on: 106 | - ibp-monitor-api 107 | command: 'dockerize -wait=tcp://ibp-monitor-api:30002 -timeout 60s' 108 | ibp-monitor-frontend: 109 | container_name: ibp-monitor-frontend 110 | image: ibp-monitor-frontend 111 | pull_policy: never 112 | build: &ibp-monitor-frontend 113 | context: ../. 114 | dockerfile: docker/Dockerfile.ibp.frontend 115 | restart: on-failure 116 | ports: 117 | - '30001:80' 118 | depends_on: 119 | wait-for-ibp-api-ready: 120 | condition: service_completed_successfully 121 | # volumes: 122 | # ibp-keys: 123 | # name: ibp-keys 124 | -------------------------------------------------------------------------------- /lib/prometheus-exporter.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | class PrometheusExporter { 4 | _ds = undefined 5 | 6 | constructor(datastore) { 7 | this._ds = datastore 8 | } 9 | 10 | // Older versions of MySQL store JSON in TEXT fields... 11 | _parseRecord(record = {}) { 12 | return typeof record === 'string' ? JSON.parse(record) : record 13 | } 14 | 15 | _calcAverage(checks = [], range = 10) { 16 | var ret = 0 17 | var chunk = checks.slice(0, range) 18 | // console.log(chunk) 19 | let count = chunk.length 20 | 21 | if (count > 0) { 22 | var sum = chunk 23 | .map((m) => { 24 | return this._parseRecord(m.record).performance || 0 25 | }) 26 | .reduce((prevVal, currVal, idx, arr) => { 27 | return prevVal + currVal 28 | }, 0) 29 | ret = sum / count 30 | } 31 | return ret 32 | } 33 | 34 | async export(serviceId) { 35 | console.debug('PrometheusExporter.export()', serviceId) 36 | let lines = [] 37 | const service = await this._ds.Service.findByPk(serviceId) 38 | if (!service) { 39 | lines.push(`# invalid serviceId: ${serviceId}`) 40 | } else { 41 | const errorCount = ( 42 | await this._ds.HealthCheck.findAll({ 43 | where: { serviceId, status: 'error' }, 44 | order: [['id', 'DESC']], 45 | }) 46 | ).length 47 | lines.push('# HELP ibp_service_error_count') 48 | lines.push('# TYPE ibp_service_error_count counter') 49 | lines.push(`ibp_service_error_count{serviceId="${serviceId}"} ${errorCount || 0}`) 50 | const checks = await this._ds.HealthCheck.findAll({ 51 | where: { serviceId }, 52 | order: [['id', 'DESC']], 53 | limit: 100, 54 | }) 55 | // averages: 10, 50, 100 56 | let check = checks[0] || {} 57 | const record = this._parseRecord(check.record || {}) 58 | lines.push('# HELP ibp_service_performance_1 performance for latest check') 59 | lines.push('# TYPE ibp_service_performance_1 gauge') 60 | lines.push( 61 | `ibp_service_performance_1{serviceId="${serviceId}", timestamp="${moment 62 | .utc(check.createdAt) 63 | .valueOf()}"} ${record?.performance || 0}` 64 | ) 65 | let avg = this._calcAverage(checks, 10) 66 | lines.push('# HELP ibp_service_performance_10 average performance over past 10 checks') 67 | lines.push('# TYPE ibp_service_performance_10 gauge') 68 | lines.push(`ibp_service_performance_10{serviceId="${serviceId}"} ${avg}`) 69 | avg = this._calcAverage(checks, 50) 70 | lines.push('# HELP ibp_service_performance_50 average performance over past 50 checks') 71 | lines.push('# TYPE ibp_service_performance_50 gauge') 72 | lines.push(`ibp_service_performance_50{serviceId="${serviceId}"} ${avg}`) 73 | avg = this._calcAverage(checks, 100) 74 | lines.push('# HELP ibp_service_performance_100 average performance over past 100 checks') 75 | lines.push('# TYPE ibp_service_performance_100 gauge') 76 | lines.push(`ibp_service_performance_100{serviceId="${serviceId}"} ${avg}`) 77 | 78 | // api.rpc.system.peers() is an unsafe RPC method 79 | // api.rpc.net.peerCount returns undefined... 80 | // TODO peerCount 81 | // lines.push('# HELP ibp_service_peer_count number of peers connected at latest check') 82 | // lines.push('# TYPE ibp_service_peer_count gauge') 83 | // lines.push(`ibp_service_peer_count{serviceId="${serviceId}"} ${record.peerCount || 0}`) 84 | 85 | // lines.push(`ibp_monitor{serviceId="${serviceId}"} 0`) 86 | // checks.forEach(hc => { 87 | // // console.debug(hc) 88 | // const record = this._parseRecord(hc.record) 89 | // lines.push(`ibp_service_performance{serviceId="${serviceId}", timestamp="${moment.utc(hc.createdAt).valueOf()}"} ${record?.performance || 0}`) 90 | // }); 91 | } 92 | // console.debug(lines) 93 | return lines.join('\n') 94 | } 95 | } 96 | 97 | export { PrometheusExporter } 98 | -------------------------------------------------------------------------------- /data/migrations/20230411123426-insert-services.js: -------------------------------------------------------------------------------- 1 | async function up({ context: queryInterface }) { 2 | await queryInterface.bulkInsert('service', [ 3 | // level 1 4 | { 5 | id: 'westend-bootnode', 6 | chainId: 'westend', 7 | type: 'bootnode', 8 | membershipLevelId: 1, 9 | status: 'active', 10 | }, 11 | { 12 | id: 'kusama-bootnode', 13 | chainId: 'kusama', 14 | type: 'bootnode', 15 | membershipLevelId: 1, 16 | status: 'active', 17 | }, 18 | { 19 | id: 'polkadot-bootnode', 20 | chainId: 'polkadot', 21 | type: 'bootnode', 22 | membershipLevelId: 1, 23 | status: 'active', 24 | }, 25 | // level 3 26 | { 27 | id: 'westend-rpc', 28 | chainId: 'westend', 29 | type: 'rpc', 30 | membershipLevelId: 3, 31 | status: 'active', 32 | }, 33 | { 34 | id: 'kusama-rpc', 35 | chainId: 'kusama', 36 | type: 'rpc', 37 | membershipLevelId: 3, 38 | status: 'active', 39 | }, 40 | { 41 | id: 'polkadot-rpc', 42 | chainId: 'polkadot', 43 | type: 'rpc', 44 | membershipLevelId: 3, 45 | status: 'active', 46 | }, 47 | // level 4 48 | { 49 | id: 'westmint-bootnode', 50 | chainId: 'westmint', 51 | type: 'bootnode', 52 | membershipLevelId: 4, 53 | status: 'active', 54 | }, 55 | { 56 | id: 'collectives-westend-bootnode', 57 | chainId: 'collectives-westend', 58 | type: 'bootnode', 59 | membershipLevelId: 4, 60 | status: 'active', 61 | }, 62 | { 63 | id: 'statemine-bootnode', 64 | chainId: 'statemine', 65 | type: 'bootnode', 66 | membershipLevelId: 4, 67 | status: 'active', 68 | }, 69 | { 70 | id: 'bridgehub-kusama-bootnode', 71 | chainId: 'bridgehub-kusama', 72 | type: 'bootnode', 73 | membershipLevelId: 4, 74 | status: 'active', 75 | }, 76 | { 77 | id: 'encointer-kusama-bootnode', 78 | chainId: 'encointer-kusama', 79 | type: 'bootnode', 80 | membershipLevelId: 4, 81 | status: 'active', 82 | }, 83 | { 84 | id: 'statemint-bootnode', 85 | chainId: 'statemint', 86 | type: 'bootnode', 87 | membershipLevelId: 4, 88 | status: 'active', 89 | }, 90 | { 91 | id: 'collectives-polkadot-bootnode', 92 | chainId: 'collectives-polkadot', 93 | type: 'bootnode', 94 | membershipLevelId: 4, 95 | status: 'active', 96 | }, 97 | // level 5 98 | { 99 | id: 'westmint-rpc', 100 | chainId: 'westmint', 101 | type: 'rpc', 102 | membershipLevelId: 5, 103 | status: 'active', 104 | }, 105 | { 106 | id: 'collectives-westend-rpc', 107 | chainId: 'collectives-westend', 108 | type: 'rpc', 109 | membershipLevelId: 5, 110 | status: 'active', 111 | }, 112 | { 113 | id: 'statemine-rpc', 114 | chainId: 'statemine', 115 | type: 'rpc', 116 | membershipLevelId: 5, 117 | status: 'active', 118 | }, 119 | { 120 | id: 'bridgehub-kusama-rpc', 121 | chainId: 'bridgehub-kusama', 122 | type: 'rpc', 123 | membershipLevelId: 5, 124 | status: 'active', 125 | }, 126 | { 127 | id: 'encointer-kusama-rpc', 128 | chainId: 'encointer-kusama', 129 | type: 'rpc', 130 | membershipLevelId: 5, 131 | status: 'active', 132 | }, 133 | { 134 | id: 'statemint-rpc', 135 | chainId: 'statemint', 136 | type: 'rpc', 137 | membershipLevelId: 5, 138 | status: 'active', 139 | }, 140 | { 141 | id: 'collectives-polkadot-rpc', 142 | chainId: 'collectives-polkadot', 143 | type: 'rpc', 144 | membershipLevelId: 5, 145 | status: 'active', 146 | }, 147 | ]) 148 | } 149 | 150 | async function down({ context: queryInterface }) { 151 | await queryInterface.bulkDelete('service', null, {}) 152 | } 153 | 154 | export { up, down } 155 | --------------------------------------------------------------------------------