├── .dockerignore ├── .lintstagedrc ├── images ├── logo.png └── banner.png ├── docs ├── images │ ├── logo.png │ └── banner.png ├── fonts │ ├── WorkSans-Bold.ttf │ ├── OpenSans-Regular.ttf │ └── Inconsolata-Regular.ttf ├── scripts │ ├── third-party │ │ ├── hljs-line-num.js │ │ ├── tocbot.min.js │ │ ├── Apache-License-2.0.txt │ │ └── hljs-line-num-original.js │ ├── resize.js │ ├── search.min.js │ ├── search.js │ └── core.min.js ├── data │ └── search.json ├── styles │ ├── clean-jsdoc-theme-dark.css │ └── clean-jsdoc-theme-light.css ├── config_db.js.html ├── index.js.html ├── Rewards_popSolUsd.js.html └── Delegator_utils.js.html ├── .husky ├── pre-commit └── commit-msg ├── .prettierrc ├── Dockerfile ├── .commitlintrc.json ├── .env.sample ├── src ├── logger │ ├── logger.js │ ├── utils.js │ ├── debugLogger.js │ └── productionLogger.js ├── models │ ├── Delegator.js │ ├── Transaction.js │ └── Reward.js ├── config │ ├── db.js │ └── axiosInstance.js ├── index.js ├── Rewards │ ├── popSolUsd.js │ ├── validator.js │ └── index.js ├── Delegator │ ├── utils.js │ └── delegatorCron.js ├── utils │ └── index.js └── repository │ └── network.repository.js ├── .eslintrc ├── docker-compose.yml ├── LICENSE ├── jsdoc.json ├── package.json ├── .gitignore ├── CONTRIBUTING.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .vscode -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": "npm run format-onCommit --" 3 | } -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/images/banner.png -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/docs/images/banner.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run jsdoc 5 | git add docs -------------------------------------------------------------------------------- /docs/fonts/WorkSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/docs/fonts/WorkSans-Bold.ttf -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/docs/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luganodes/Solana-Indexer/HEAD/docs/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | WORKDIR /app 4 | 5 | USER root 6 | 7 | COPY package*.json ./ 8 | RUN npm install 9 | COPY . . 10 | 11 | CMD ["npm", "start"] 12 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": ["@commitlint/config-conventional"], 4 | "rules": { 5 | "type-enum": [2, "always", ["ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style"]] 6 | } 7 | } -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DB_URI=mongodb://localhost:27017 2 | MONGO_USER= 3 | MONGO_PASSWORD= 4 | DB_NAME=solana_crons 5 | 6 | VALIDATOR_PUB_KEY= 7 | VALIDATOR_ID= 8 | START_EPOCH= 9 | 10 | SOLANA_ENDPOINT=https://api.mainnet-beta.solana.com -------------------------------------------------------------------------------- /src/logger/logger.js: -------------------------------------------------------------------------------- 1 | import DebugLogger from './debugLogger.js' 2 | import ProductionLogger from './productionLogger.js' 3 | import dotenv from 'dotenv' 4 | 5 | dotenv.config() 6 | 7 | const logger = 8 | process.env.NODE_ENV !== 'production' 9 | ? new DebugLogger() 10 | : new ProductionLogger() 11 | 12 | export default logger 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": ["prettier", "plugin:node/recommended"], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2020 14 | }, 15 | "plugins": ["prettier"], 16 | "rules":{ 17 | "prettier/prettier":"error" 18 | } 19 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | app: 4 | build: . 5 | environment: 6 | DB_URI: mongodb://mongo:27017 7 | MONGO_USER: ${MONGO_USER} 8 | MONGO_PASSWORD: ${MONGO_PASSWORD} 9 | VALIDATOR_PUB_KEY: ${VALIDATOR_PUB_KEY} 10 | VALIDATOR_ID: ${VALIDATOR_ID} 11 | START_EPOCH: ${START_EPOCH} 12 | depends_on: 13 | - mongo 14 | volumes: 15 | - .:/app 16 | - /app/node_modules # This volume is for caching node modules 17 | mongo: 18 | image: mongo:latest 19 | ports: 20 | - '27018:27017' 21 | environment: 22 | MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} 23 | MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} 24 | volumes: 25 | - mongodb_data:/data/db 26 | volumes: 27 | mongodb_data: 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luganodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/models/Delegator.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import logger from '../logger/logger.js' 3 | 4 | // Define the schema for a Delegator 5 | const delegatorSchema = new mongoose.Schema({ 6 | delegatorId: { 7 | type: String, 8 | required: [true, 'Delegator ID is required'], 9 | }, 10 | timestamp: { 11 | type: Number, 12 | required: [true, 'Timestamp is required'], 13 | }, 14 | unstaked: { 15 | type: Boolean, 16 | default: false, 17 | }, 18 | unstakedTimestamp: { 19 | type: Number, 20 | default: -1, 21 | }, 22 | unstakedEpoch: { 23 | type: Number, 24 | default: -1, 25 | }, 26 | apr: { 27 | type: Number, 28 | default: 0, 29 | }, 30 | stakedAmount: { 31 | type: Number, 32 | default: 0, 33 | }, 34 | activationEpoch: { 35 | type: Number, 36 | default: 0, 37 | }, 38 | }) 39 | 40 | // Compile the schema into a model or retrieve the existing model 41 | const Delegator = 42 | mongoose.models.Delegator || mongoose.model('Delegator', delegatorSchema) 43 | 44 | export default Delegator 45 | -------------------------------------------------------------------------------- /src/config/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import dotenv from 'dotenv' 3 | import logger from '../logger/logger.js' 4 | 5 | // Load environment variables from .env file 6 | dotenv.config() 7 | 8 | /** 9 | * Establishes a connection to the MongoDB using the provided configurations. 10 | * Connection configurations are sourced from environment variables. 11 | * 12 | * @returns {Promise} A promise object that resolves once the connection is established. 13 | * @throws {Error} - If the connection couldn't be established. 14 | */ 15 | export const connectDB = async () => { 16 | try { 17 | mongoose.set('strictQuery', false) 18 | const connection = await mongoose.connect(process.env.DB_URI, { 19 | useNewUrlParser: true, 20 | useUnifiedTopology: true, 21 | user: process.env.MONGO_USER, 22 | pass: process.env.MONGO_PASSWORD, 23 | dbName: process.env.DB_NAME, 24 | }) 25 | logger.info('Successfully connected to the database') 26 | return connection 27 | } catch (e) { 28 | logger.error(`Error connecting to the database: ${e.message}`) 29 | throw e 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { connectDB } from './config/db.js' 2 | import delegatorCron from './Delegator/delegatorCron.js' 3 | import rewardsCron from './Rewards/index.js' 4 | import validatorRewardsCron from './Rewards/validator.js' 5 | import logger from './logger/logger.js' 6 | 7 | /** 8 | * Initialize and run essential services. 9 | */ 10 | const cronHandles = [] 11 | const main = async () => { 12 | try { 13 | // Connect to the database 14 | await connectDB() 15 | 16 | // Start necessary cron jobs 17 | cronHandles.push(await delegatorCron()) 18 | cronHandles.push(await validatorRewardsCron()) 19 | cronHandles.push(await rewardsCron()) 20 | } catch (e) { 21 | logger.error(`Error in main initialization: ${e}`) 22 | } 23 | } 24 | 25 | main() 26 | 27 | process.on('SIGINT', async () => { 28 | try { 29 | for (const handle of cronHandles) { 30 | await handle.cancel() 31 | logger.info('Cron job successfully cancelled.') 32 | } 33 | // eslint-disable-next-line no-process-exit 34 | process.exit(0) 35 | } catch (e) { 36 | logger.error(`Error during graceful shutdown: ${e}`) 37 | // eslint-disable-next-line no-process-exit 38 | process.exit(1) // Exit with an error code 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/Rewards/popSolUsd.js: -------------------------------------------------------------------------------- 1 | import Reward from '../models/Reward.js' 2 | import logger from '../logger/logger.js' 3 | import { fetchSolanaPriceAtDate } from '../repository/network.repository.js' 4 | 5 | /** 6 | * Populate the solUsd field in all Reward model instances 7 | */ 8 | export const populateSolUsd = async () => { 9 | try { 10 | // Fetch all rewards from the database 11 | const rewards = await Reward.find() 12 | 13 | // Loop through each reward to populate solUsd 14 | for (let i = 0; i < rewards.length; i++) { 15 | // Introduce a delay of 1 second to avoid rate limiting 16 | await new Promise((resolve) => setTimeout(resolve, 1000)) 17 | 18 | // Fetch the current SOL to USD conversion rate 19 | const solUsd = await fetchSolanaPriceAtDate(rewards[i].timestamp) 20 | 21 | // Update the solUsd field for the current reward 22 | rewards[i].solUsd = solUsd 23 | await rewards[i].save() 24 | 25 | logger.info( 26 | `Successfully updated solUsd for reward ID: ${rewards[i]._id}` 27 | ) 28 | } 29 | 30 | logger.info('SOL-USD population task is DONE.') 31 | } catch (e) { 32 | logger.error(`An error occurred while populating SOL-USD: ${e.message}`) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/logger/utils.js: -------------------------------------------------------------------------------- 1 | import * as stackTrace from 'stack-trace' 2 | import { format } from 'winston' 3 | const { printf } = format 4 | 5 | const getFileAndLineNumber = () => { 6 | try { 7 | throw new Error('Dummy error to find stack trace') 8 | } catch (e) { 9 | const trace = stackTrace.parse(e) 10 | const cwd = process.cwd().split('src')[0] 11 | 12 | // index 2 should skip over this function and its immediate caller to get to the original log call 13 | const file = trace[2] 14 | .getFileName() 15 | .replace(cwd, '') 16 | .replace('file:///', '') 17 | const line = trace[2].getLineNumber() 18 | return { 19 | file, 20 | line, 21 | } 22 | } 23 | } 24 | 25 | const fileFormat = printf(({ level, message, timestamp, meta }) => { 26 | let fileInfo = '' 27 | if (meta) { 28 | const { file, line } = meta 29 | fileInfo = `${file}:${line}` 30 | } 31 | return `[${level}]\t[${timestamp}]\t${fileInfo}\t${message}` 32 | }) 33 | 34 | const consoleFormat = printf(({ level, message, meta }) => { 35 | let fileInfo = '' 36 | if (meta) { 37 | const { file, line } = meta 38 | fileInfo = `${file}:${line}` 39 | } 40 | return `[${level}]\t${fileInfo}\t${message}` 41 | }) 42 | 43 | export { getFileAndLineNumber, fileFormat, consoleFormat } 44 | -------------------------------------------------------------------------------- /src/logger/debugLogger.js: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston' 2 | const { combine, timestamp, colorize } = format 3 | import { getFileAndLineNumber, consoleFormat, fileFormat } from './utils.js' 4 | 5 | class DebugLogger { 6 | constructor() { 7 | this.logger = this.initLogger() 8 | } 9 | 10 | initLogger() { 11 | return createLogger({ 12 | level: 'debug', 13 | format: combine( 14 | timestamp({ format: 'YYYY-MM-DD HH:mm:ss [UTC]Z' }), 15 | fileFormat 16 | ), 17 | transports: [ 18 | new transports.Console({ 19 | format: combine(colorize(), consoleFormat), 20 | }), 21 | new transports.File({ 22 | filename: 'combined.log', 23 | level: 'info', 24 | }), 25 | new transports.File({ filename: 'error.log', level: 'error' }), 26 | ], 27 | }) 28 | } 29 | 30 | debug(message) { 31 | const fileInfo = getFileAndLineNumber() 32 | this.logger.debug(message, { meta: fileInfo }) 33 | } 34 | 35 | info(message) { 36 | const fileInfo = getFileAndLineNumber() 37 | this.logger.info(message, { meta: fileInfo }) 38 | } 39 | 40 | error(message) { 41 | const fileInfo = getFileAndLineNumber() 42 | this.logger.error(message, { meta: fileInfo }) 43 | } 44 | } 45 | 46 | export default DebugLogger 47 | -------------------------------------------------------------------------------- /src/models/Transaction.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import logger from '../logger/logger.js' 3 | 4 | // Define the schema for a Transaction 5 | const transactionSchema = new mongoose.Schema({ 6 | delegatorId: { 7 | type: String, 8 | required: [true, 'Delegator ID is required.'], 9 | }, 10 | timestamp: { 11 | type: Number, 12 | required: [true, 'Timestamp of the transaction is required.'], 13 | }, 14 | type: { 15 | type: String, 16 | required: [true, 'Transaction type is required.'], 17 | }, 18 | amount: { 19 | type: Number, 20 | required: [true, 'Transaction amount is required.'], 21 | }, 22 | solUsd: { 23 | type: Number, 24 | required: [ 25 | true, 26 | 'Solana price in USD at the time of transaction is required.', 27 | ], 28 | }, 29 | transactionCount: { 30 | type: Number, 31 | required: [true, 'Transaction count for the delegator is required.'], 32 | }, 33 | transactionHash: { 34 | type: String, 35 | required: [true, 'Transaction hash is required.'], 36 | }, 37 | fee: { 38 | type: Number, 39 | required: [true, 'Transaction fee is required.'], 40 | }, 41 | }) 42 | 43 | // Compile the schema into a model or retrieve the existing model 44 | const Transaction = 45 | mongoose.models.Transaction || 46 | mongoose.model('Transaction', transactionSchema) 47 | 48 | export default Transaction 49 | -------------------------------------------------------------------------------- /src/logger/productionLogger.js: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston' 2 | const { combine, timestamp, colorize } = format 3 | import { getFileAndLineNumber, consoleFormat, fileFormat } from './utils.js' 4 | 5 | class ProductionLogger { 6 | constructor() { 7 | this.logger = this.initLogger() 8 | } 9 | 10 | initLogger() { 11 | return createLogger({ 12 | level: 'info', 13 | format: combine( 14 | timestamp({ format: 'YYYY-MM-DD HH:mm:ss [UTC]Z' }), 15 | fileFormat 16 | ), 17 | transports: [ 18 | new transports.Console({ 19 | format: combine(colorize(), consoleFormat), 20 | }), 21 | new transports.File({ 22 | filename: 'combined.log', 23 | level: 'info', 24 | }), 25 | new transports.File({ filename: 'error.log', level: 'error' }), 26 | ], 27 | }) 28 | } 29 | 30 | debug(message) { 31 | const fileInfo = getFileAndLineNumber() 32 | this.logger.debug(message, { meta: fileInfo }) 33 | } 34 | 35 | info(message) { 36 | const fileInfo = getFileAndLineNumber() 37 | this.logger.info(message, { meta: fileInfo }) 38 | } 39 | 40 | error(message) { 41 | const fileInfo = getFileAndLineNumber() 42 | this.logger.error(message, { meta: fileInfo }) 43 | } 44 | } 45 | 46 | export default ProductionLogger 47 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "recurseDepth": 10, 4 | "source": { 5 | "include": ["src", "package.json", "README.md"], 6 | "includePattern": ".js$", 7 | "excludePattern": "(node_modules/|docs)" 8 | }, 9 | "templates": { 10 | "cleverLinks": true, 11 | "monospaceLinks": true, 12 | "default": { 13 | "staticFiles": { 14 | "include": ["./images"], 15 | "destination": "images" 16 | } 17 | } 18 | }, 19 | "opts": { 20 | "encoding": "utf8", 21 | "readme": "./README.md", 22 | "destination": "docs/", 23 | "recurse": true, 24 | "verbose": true, 25 | "template": "node_modules/clean-jsdoc-theme", 26 | "theme_opts": { 27 | "default_theme": "fallback-dark", 28 | "homepageTitle": "Documentation", 29 | "title": "Solana Indexer Documentation", 30 | "includeFilesListInHomepage": true, 31 | "meta": [ 32 | { 33 | "name": "author", 34 | "content": "Luganodes" 35 | }, 36 | { 37 | "name": "description", 38 | "content": "A fast, efficient, and open-source Solana blockchain indexer designed to query, analyze, and monitor on-chain data." 39 | } 40 | ] 41 | } 42 | }, 43 | "markdown": { 44 | "hardwrap": false, 45 | "idInHeadings": true 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/models/Reward.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import logger from '../logger/logger.js' 3 | 4 | // Define the schema for Rewards 5 | const rewardSchema = new mongoose.Schema({ 6 | delegatorId: { 7 | type: String, 8 | required: [true, 'Delegator ID is required.'], 9 | }, 10 | solUsd: { 11 | type: Number, 12 | default: 0, 13 | }, 14 | epochNum: { 15 | type: Number, 16 | required: [true, 'Epoch number is required.'], 17 | }, 18 | timestamp: { 19 | type: Number, 20 | required: [true, 'Timestamp is required.'], 21 | }, 22 | postBalance: { 23 | type: Number, 24 | required: [true, 'Post balance is required.'], 25 | }, 26 | postBalanceUsd: { 27 | type: Number, 28 | default: 0, 29 | }, 30 | userAction: { 31 | type: String, 32 | enum: ['WITHDRAW', 'REWARD'], 33 | }, 34 | reward: { 35 | type: Number, 36 | required: [true, 'Reward amount is required.'], 37 | }, 38 | rewardUsd: { 39 | type: Number, 40 | required: [true, 'Reward amount in USD is required.'], 41 | }, 42 | totalReward: { 43 | type: Number, 44 | required: [true, 'Total reward is required.'], 45 | }, 46 | totalRewardUsd: { 47 | type: Number, 48 | default: 0, 49 | }, 50 | pendingRewards: { 51 | type: Number, 52 | required: [true, 'Pending rewards are required.'], 53 | }, 54 | pendingRewardsUsd: { 55 | type: Number, 56 | default: 0, 57 | }, 58 | stakedAmount: { 59 | type: Number, 60 | required: [true, 'Staked amount is required.'], 61 | }, 62 | stakedAmountUsd: { 63 | type: Number, 64 | required: [true, 'Staked amount in USD is required.'], 65 | }, 66 | }) 67 | 68 | // Compile the schema into a model or retrieve the existing model 69 | const Reward = mongoose.models.Reward || mongoose.model('Reward', rewardSchema) 70 | 71 | export default Reward 72 | -------------------------------------------------------------------------------- /src/Delegator/utils.js: -------------------------------------------------------------------------------- 1 | import Transaction from '../models/Transaction.js' 2 | import logger from '../logger/logger.js' 3 | import { 4 | fetchSolanaPriceAtDate, 5 | getTransaction, 6 | getSignaturesForAddress, 7 | } from '../repository/network.repository.js' 8 | 9 | const VALIDATOR_PUB_KEY = process.env.VALIDATOR_PUB_KEY 10 | const LAMPORTS_PER_SOL = 1000000000 11 | 12 | /** 13 | * Create and persist a delegate transaction 14 | * @param {string} address - The delegator's public key 15 | * @param {number} stakedAmount - The amount of SOL the user staked 16 | */ 17 | export const createDelegateTransaction = async (address, stakedAmount) => { 18 | try { 19 | const data = await getSignaturesForAddress(address) 20 | const transactionSignatures = data.result 21 | 22 | for (const signature of transactionSignatures) { 23 | const transactionData = await getTransaction(signature.signature) 24 | if (!transactionData.result) continue 25 | const result = 26 | transactionData.result.transaction.message.accountKeys.filter( 27 | (key) => key === VALIDATOR_PUB_KEY 28 | ) 29 | 30 | if (result.length > 0) { 31 | const solUsd = await fetchSolanaPriceAtDate( 32 | transactionData.result.blockTime * 1000 33 | ) 34 | const transactionFee = 35 | transactionData.result.meta.fee / LAMPORTS_PER_SOL 36 | 37 | await Transaction.create({ 38 | delegatorId: address, 39 | timestamp: transactionData.result.blockTime * 1000, 40 | type: 'STAKE', 41 | amount: stakedAmount, 42 | solUsd, 43 | fee: transactionFee, 44 | transactionHash: signature.signature, 45 | transactionCount: transactionSignatures.length, 46 | }) 47 | logger.info(`Transaction created [${address}]`) 48 | } 49 | } 50 | } catch (e) { 51 | logger.error( 52 | `Error creating delegate transaction [${address}]: ${e.message}` 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-crons", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo 'no test configured'", 9 | "start": "node ./src/index", 10 | "dev": "nodemon ./src/index.js", 11 | "format": "prettier-eslint --write \"{,!(node_modules)/**/}*.{js,jsx}\"", 12 | "jsdoc": "rm -rf docs && jsdoc -c jsdoc.json && dir=docs/solana-crons/1.0.0 && mkdir -p $dir/images && mv $dir/*.png $dir/images && mv $dir/* docs && rm -r docs/solana-crons", 13 | "prepare": "husky install", 14 | "pre-commit": "lint-staged", 15 | "format-onCommit": "prettier-eslint --write \"*.{js,jsx}\"" 16 | }, 17 | "dependencies": { 18 | "@solana/web3.js": "^1.87.2", 19 | "@types/mongoose": "^5.11.97", 20 | "axios": "^1.1.3", 21 | "dotenv": "^16.0.3", 22 | "mongoose": "^6.7.2", 23 | "node-schedule": "^2.1.0", 24 | "stack-trace": "^1.0.0-pre2", 25 | "winston": "^3.10.0" 26 | }, 27 | "devDependencies": { 28 | "@commitlint/cli": "^17.4.2", 29 | "@commitlint/config-conventional": "^17.4.2", 30 | "clean-jsdoc-theme": "^4.2.10", 31 | "eslint": "^8.32.0", 32 | "eslint-config-node": "^4.1.0", 33 | "eslint-config-prettier": "^8.6.0", 34 | "eslint-config-standard": "^17.0.0", 35 | "eslint-plugin-import": "^2.26.0", 36 | "eslint-plugin-n": "^15.6.0", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-prettier": "^4.2.1", 39 | "eslint-plugin-promise": "^6.1.1", 40 | "husky": "^8.0.0", 41 | "jsdoc": "^4.0.2", 42 | "lint-staged": "^13.1.0", 43 | "nodemon": "^2.0.20", 44 | "prettier": "^2.8.3", 45 | "prettier-eslint-cli": "^7.1.0" 46 | }, 47 | "description": "Indexer for Solana", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+ssh://git@gitlab.com/luganodes/staking-dashboard/indexers-new/solana-crons.git" 51 | }, 52 | "author": "", 53 | "bugs": { 54 | "url": "https://gitlab.com/luganodes/staking-dashboard/indexers-new/solana-crons/issues" 55 | }, 56 | "homepage": "https://gitlab.com/luganodes/staking-dashboard/indexers-new/solana-crons#readme" 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # vscode settings 133 | .vscode 134 | 135 | # random files 136 | test.js 137 | .DS_Store 138 | 139 | #todo 140 | Todo -------------------------------------------------------------------------------- /docs/scripts/third-party/hljs-line-num.js: -------------------------------------------------------------------------------- 1 | !function(r,o){"use strict";var e,l="hljs-ln",s="hljs-ln-line",f="hljs-ln-code",c="hljs-ln-numbers",u="hljs-ln-n",h="data-line-number",n=/\r\n|\r|\n/g;function t(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var e=parseInt(t.dataset.lineNumber),o=parseInt(r.dataset.lineNumber);if(e==o)return n;var a,i=t.textContent,l=r.textContent;for(o{6}',[s,c,u,h,f,a+t.startFrom,0{1}',[l,o])}return e}function m(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r{1}\n',[n,0} 12 | */ 13 | const sleep = async (milliseconds) => { 14 | return new Promise((resolve) => { 15 | setTimeout(() => { 16 | resolve() 17 | }, milliseconds) 18 | }) 19 | } 20 | 21 | const request = { 22 | /** 23 | * Sends a GET request to the given URL. 24 | * Implements exponential retry on failure up to a max of 5 minutes. 25 | * @param {string} url - The endpoint URL. 26 | * @param {Object} params - Optional parameters for the request. 27 | * @param {number} retryDelay - Initial delay for the retry. Defaults to 5 seconds. 28 | * @returns {Promise} - Response data from the request. 29 | * @throws {Error} - If the maximum retry limit is exceeded. 30 | */ 31 | GET: async (url, params = {}, retryDelay = INITIAL_DELAY) => { 32 | try { 33 | console.info(url, params) 34 | if (retryDelay !== INITIAL_DELAY) { 35 | logger.debug( 36 | `GET request to ${url} in ${retryDelay / 1000} seconds` 37 | ) 38 | await sleep(retryDelay) 39 | } 40 | const response = await axios.get(url, params) 41 | return response.data 42 | } catch (e) { 43 | if (retryDelay <= MAX_RETRY_DELAY) 44 | return await request.GET(url, params, retryDelay * 2) 45 | const info = { 46 | url, 47 | error: e.message, 48 | } 49 | logger.error('GET request failed', info) 50 | throw e 51 | } 52 | }, 53 | 54 | /** 55 | * Sends a POST request to the given URL. 56 | * Implements exponential retry on failure up to a max of 5 minutes. 57 | * @param {string} url - The endpoint URL. 58 | * @param {Object} body - Body of the request. 59 | * @param {number} retryDelay - Initial delay for the retry. Defaults to 5 seconds. 60 | * @returns {Promise} - Response data from the request. 61 | * @throws {Error} - If the maximum retry limit is exceeded. 62 | */ 63 | POST: async (url, body, retryDelay = INITIAL_DELAY) => { 64 | try { 65 | console.log(url, body) 66 | if (retryDelay !== INITIAL_DELAY) { 67 | logger.debug( 68 | `POST request to ${url} in ${retryDelay / 1000} seconds` 69 | ) 70 | await sleep(retryDelay) 71 | } 72 | const response = await axios.post(url, body) 73 | return response.data 74 | } catch (e) { 75 | if (retryDelay <= MAX_RETRY_DELAY) 76 | return await request.POST(url, body, retryDelay * 2) 77 | 78 | const info = { 79 | url, 80 | body, 81 | error: e.message, 82 | } 83 | logger.error('POST request failed', info) 84 | throw e 85 | } 86 | }, 87 | } 88 | 89 | export default request 90 | -------------------------------------------------------------------------------- /docs/scripts/search.min.js: -------------------------------------------------------------------------------- 1 | const searchId="LiBfqbJVcV",searchHash="#"+searchId,searchContainer=document.querySelector("#PkfLWpAbet"),searchWrapper=document.querySelector("#iCxFxjkHbP"),searchCloseButton=document.querySelector("#VjLlGakifb"),searchInput=document.querySelector("#vpcKVYIppa"),resultBox=document.querySelector("#fWwVHRuDuN");function showResultText(e){resultBox.innerHTML=`${e}`}function hideSearch(){window.location.hash===searchHash&&history.go(-1),window.onhashchange=null,searchContainer&&(searchContainer.style.display="none")}function listenCloseKey(e){"Escape"===e.key&&(hideSearch(),window.removeEventListener("keyup",listenCloseKey))}function showSearch(){try{hideMobileMenu()}catch(e){console.error(e)}window.onhashchange=hideSearch,window.location.hash!==searchHash&&history.pushState(null,null,searchHash),searchContainer&&(searchContainer.style.display="flex",window.addEventListener("keyup",listenCloseKey)),searchInput&&searchInput.focus()}async function fetchAllData(){var{hostname:e,protocol:t,port:n}=location,t=t+"//"+e+(""!==n?":"+n:"")+baseURL,e=new URL("data/search.json",t);const a=await fetch(e);n=(await a.json()).list;return n}function onClickSearchItem(t){const n=t.currentTarget;if(n){const a=n.getAttribute("href")||"";t=a.split("#")[1]||"";let e=document.getElementById(t);e||(t=decodeURI(t),e=document.getElementById(t)),e&&setTimeout(function(){bringElementIntoView(e)},100)}}function buildSearchResult(e){let t="";var n=/(<([^>]+)>)/gi;for(const s of e){const{title:c="",description:i=""}=s.item;var a=s.item.link.replace('.*/,""),o=c.replace(n,""),r=i.replace(n,"");t+=` 2 | 3 |
${o}
4 |
${r||"No description available."}
5 |
6 | `}return t}function getSearchResult(e,t,n){var t={...{shouldSort:!0,threshold:.4,location:0,distance:100,maxPatternLength:32,minMatchCharLength:1,keys:t}},a=Fuse.createIndex(t.keys,e);const o=new Fuse(e,t,a),r=o.search(n);return 20{o=null,a||t.apply(this,e)},n),a&&!o&&t.apply(this,e)}}let searchData;async function search(e){e=e.target.value;if(resultBox)if(e){if(!searchData){showResultText("Loading...");try{searchData=await fetchAllData()}catch(e){return console.log(e),void showResultText("Failed to load result.")}}e=getSearchResult(searchData,["title","description"],e);e.length?resultBox.innerHTML=buildSearchResult(e):showResultText("No result found! Try some different combination.")}else showResultText("Type anything to view search result");else console.error("Search result container not found")}function onDomContentLoaded(){const e=document.querySelectorAll(".search-button");var t=debounce(search,300);searchCloseButton&&searchCloseButton.addEventListener("click",hideSearch),e&&e.forEach(function(e){e.addEventListener("click",showSearch)}),searchContainer&&searchContainer.addEventListener("click",hideSearch),searchWrapper&&searchWrapper.addEventListener("click",function(e){e.stopPropagation()}),searchInput&&searchInput.addEventListener("keyup",t),window.location.hash===searchHash&&showSearch()}window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",function(){window.location.hash===searchHash&&showSearch()}); -------------------------------------------------------------------------------- /src/Delegator/delegatorCron.js: -------------------------------------------------------------------------------- 1 | import Delegator from '../models/Delegator.js' 2 | import schedule from 'node-schedule' 3 | import { fetchLatestEpoch } from '../repository/network.repository.js' 4 | import { findAPRValue, findDelegators } from '../utils/index.js' 5 | import { createDelegateTransaction } from './utils.js' 6 | import logger from '../logger/logger.js' 7 | import Transaction from '../models/Transaction.js' 8 | 9 | /** 10 | * Scheduled job to manage delegators. 11 | */ 12 | const delegatorCron = async () => { 13 | logger.info('Delegator cron started') 14 | 15 | // Schedule a job to run every 30 minutes 16 | return schedule.scheduleJob('*/30 * * * *', async () => { 17 | await delegatorJob() 18 | }) 19 | } 20 | 21 | /** 22 | * Job to create new delegators and update already populated ones. 23 | */ 24 | const delegatorJob = async () => { 25 | try { 26 | const delegators = await findDelegators() 27 | const latestEpoch = await fetchLatestEpoch() 28 | console.table(delegators) 29 | 30 | // Loop over each delegator create or update their entry 31 | for (const delegator of delegators) { 32 | await processDelegator(delegator, latestEpoch) 33 | } 34 | await processUnstaking(delegators, latestEpoch) 35 | 36 | logger.info('Delegator cron job successfully executed') 37 | } catch (e) { 38 | logger.error( 39 | `Delegator cron job failed [${new Date().getTime()}]: ${e.message}` 40 | ) 41 | } 42 | } 43 | 44 | /** 45 | * Process individual delegator. 46 | */ 47 | const processDelegator = async (delegator, latestEpoch) => { 48 | const { pubkey, activationEpoch, deactivationEpoch, stake } = delegator 49 | 50 | const storedDelegator = await Delegator.findOne({ 51 | delegatorId: pubkey, 52 | }) 53 | 54 | if (!storedDelegator) { 55 | const unstaked = latestEpoch >= deactivationEpoch 56 | const apr = unstaked ? 0 : await findAPRValue(pubkey, latestEpoch) 57 | await Delegator.create({ 58 | delegatorId: pubkey, 59 | timestamp: new Date().getTime(), 60 | unstaked, 61 | apr, 62 | stakedAmount: stake, 63 | activationEpoch, 64 | unstakedEpoch: deactivationEpoch, 65 | }) 66 | 67 | logger.info(`Created delegator: ${pubkey}`) 68 | await createDelegateTransaction(pubkey, stake) 69 | return 70 | } 71 | 72 | if (!(await Transaction.findOne({ delegatorId: pubkey }))) 73 | await createDelegateTransaction(pubkey, stake) 74 | 75 | if (latestEpoch > deactivationEpoch) { 76 | if ( 77 | storedDelegator.unstaked && 78 | storedDelegator.unstakedEpoch === deactivationEpoch 79 | ) 80 | return 81 | storedDelegator.unstaked = true 82 | storedDelegator.unstakedEpoch = deactivationEpoch 83 | await storedDelegator.save() 84 | 85 | logger.info(`Unstaked delegator: ${pubkey}`) 86 | return 87 | } 88 | 89 | const apr = await findAPRValue(pubkey, latestEpoch) 90 | storedDelegator.apr = apr 91 | await storedDelegator.save() 92 | logger.info(`APR updated for delegator: ${pubkey}`) 93 | } 94 | 95 | /** 96 | * Handle unstaking based on epoch conditions. 97 | */ 98 | const processUnstaking = async (delegators, latestEpoch) => { 99 | const delegatorPubKeys = delegators.map((delegator) => delegator.pubkey) 100 | 101 | await Delegator.updateMany( 102 | { delegatorId: { $nin: delegatorPubKeys } }, 103 | { 104 | unstaked: true, 105 | unstakedTimestamp: new Date().getTime(), 106 | unstakedEpoch: latestEpoch - 1, 107 | } 108 | ) 109 | logger.info(`Unstaking processed for delegators removed from the API`) 110 | } 111 | 112 | export default delegatorCron 113 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains certain rules and guidelines that developers are expected to follow, while contributing to any repository. 4 | 5 | --- 6 | ## 1. Commit Messages 7 | 8 | * Use the `-m` flag only for minor changes. The message following the `-m` flag must be of the below format : 9 | > ` ` 10 | 11 | :white_check_mark: __Examples of valid messages:__ 12 | * Added serialisers.py for users app 13 | * Updated utils/validator.js file 14 | * Changed functionality of authentication process 15 | 16 | :x: __Examples of invalid messages:__ 17 | * Idk why this is not working 18 | * Only ui bug fixes left 19 | * All changes done, ready for production :)) 20 | 21 | * Before opening a PR, make sure you squash all your commits into one single commit using `git rebase` (squash). Instead of having 50 commits that describe 1 feature implementation, there must be one commit that describes everything that has been done so far. You can read up about it [here](https://www.internalpointers.com/post/squash-commits-into-one-git). 22 | > NOTE: While squashing your commits to write a new one, do not make use of `-m` flag. In this case, a vim editor window shall open. Write a title for the commit within 50-70 characters, leave a line and add an understandable description. 23 | 24 | ## 2. Issues 25 | * Issues __MUST__ be opened any time any of the following events occur : 26 | 1. You encounter an issue such that a major (50 lines of code or above) portion of the code needs to be changed/added. 27 | 2. You want feature enhancements 28 | 3. You encounter bugs 29 | 4. Code refactoring is required 30 | 5. Test coverage should be increased 31 | * __Open issues with the given template only.__ 32 | * Feel free to label the issues appropriately. 33 | * Do not remove the headings (questions in bold) while opening an issue with the given template. Simply append to it. 34 | 35 | 36 | ## 3. Branches and PRs 37 | 38 | * No commits must be made to the `master` branch directly. The `master` branch shall only consist of the working code. 39 | * Developers are expected to work on feature branches, and upon successful development and testing, a PR (pull request) must be opened to merge with master. 40 | * A branch must be named as either as the feature being implemented, or the issue being fixed. 41 | 42 | :white_check_mark: __Examples of valid brach names:__ 43 | * #8123 (issue number) 44 | * OAuth (feature) 45 | * questionsUtils (functionality of the questions) 46 | 47 | :x: __Examples of invalid branch names__: 48 | * ziyan-testing 49 | * attemptToFixAuth 50 | * SomethingRandom 51 | 52 | 53 | ## 4. Discussion Ethics 54 | 55 | * Developers should be clear and concise while commenting on issues or PR reviews. If needed, one should provide visual reference or a code snippet for everyone involved to properly grasp the issue. 56 | * Everyone should be respectful of everyone's opinion. Any harsh/disrespectful language is __STRICTLY__ prohibited and will not be tolerated under any circumstances. 57 | 58 | ## 5. Coding Ethics 59 | 60 | * Developers are highly encouraged to use comments wherever necessary and make the code self documented. 61 | * The project structure should be neat and organised. All folders and files should be organised semantically according to their functionality. 62 | * The name of the folders and files should not be too long but should be as self explanatory as possible. 63 | * Documentation shall __STRICTLY__ have gender neutral terms. Instead of using "he/him" or "she/her", one should use "they/them" or "the user". 64 | 65 | ## 6. Coding Style Guidelines 66 | 67 | Developers should aim to write clean, maintainable, scalable and testable code. If your code is not testable, that means, it's time to refactor it. The following guidelines might come in handy for this: 68 | 69 | * Python: [Hitchiker's Guide to Python](https://docs.python-guide.org/writing/style/), [Google](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) 70 | * GoLang: [Effective-Go](https://golang.org/doc/effective_go.html) 71 | * Django: [Django-Styleguide](https://github.com/HackSoftware/Django-Styleguide) 72 | * JavaScript: [Airbnb](https://github.com/airbnb/javascript) 73 | * React.JS: [Airbnb](https://github.com/airbnb/javascript/tree/master/react) 74 | * Flutter/Dart: [Effective-Dart](https://dart.dev/guides/language/effective-dart) 75 | * Kotlin: [Kotlin Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html) 76 | * Swift: [Swift Style Guide](https://github.com/github/swift-style-guide), [Google](https://google.github.io/swift/) 77 | * Docker: [Dev Best Practices](https://docs.docker.com/develop/), [Dockerfile Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) -------------------------------------------------------------------------------- /src/Rewards/validator.js: -------------------------------------------------------------------------------- 1 | import Reward from '../models/Reward.js' 2 | import { setTimestampFormat } from '../utils/index.js' 3 | import schedule from 'node-schedule' 4 | import { 5 | getBlockTime, 6 | fetchLatestEpoch, 7 | getInflationReward, 8 | fetchSolanaPriceAtDate, 9 | } from '../repository/network.repository.js' 10 | import logger from '../logger/logger.js' 11 | import dotenv from 'dotenv' 12 | 13 | // Load environment variables from .env file 14 | dotenv.config() 15 | 16 | const LAMPORTS_PER_SOL = 1000000000 17 | 18 | const VALIDATOR_PUB_KEY = process.env.VALIDATOR_PUB_KEY 19 | const VALIDATOR_ID = process.env.VALIDATOR_ID 20 | const START_EPOCH = parseInt(process.env.START_EPOCH) 21 | 22 | /** 23 | * Initializes the function for rewards cron job. 24 | * @returns {Object} - A schedule job to run every day at 1am. 25 | */ 26 | const validatorRewardsCron = async () => { 27 | logger.info('Validator rewards cron started') 28 | 29 | // Schedule a daily job to run at 1am 30 | return schedule.scheduleJob('0 1 * * *', async () => { 31 | await validatorRewardsJob() 32 | }) 33 | } 34 | 35 | /** 36 | * Job to populate validator rewards from the start epoch to the latest epoch 37 | */ 38 | const validatorRewardsJob = async () => { 39 | try { 40 | // Get the latest validator reward's epoch number 41 | const latestReward = await Reward.findOne({ 42 | delegatorId: VALIDATOR_ID, 43 | }).sort({ epochNum: -1, timestamp: -1 }) 44 | 45 | // Set initial epoch to the epoch where validator became active 46 | let currentEpoch = START_EPOCH 47 | if (latestReward) { 48 | currentEpoch = latestReward.epochNum + 1 49 | } 50 | // Get the latest epoch info 51 | const latestEpoch = await fetchLatestEpoch() 52 | 53 | // Loop through all epochs starting from the current one 54 | for (; currentEpoch <= latestEpoch; currentEpoch++) { 55 | if (latestEpoch === currentEpoch) { 56 | logger.info(`Reached latest Epoch: ${latestEpoch}`) 57 | break 58 | } 59 | // Fetch the validator's reward for the specific epoch 60 | const data = await getInflationReward( 61 | [VALIDATOR_PUB_KEY], 62 | currentEpoch 63 | ) 64 | const rewards = data.result[0] 65 | if (rewards) { 66 | await processReward(rewards, currentEpoch) 67 | } else { 68 | logger.info('no rewards for epoch:', currentEpoch) 69 | } 70 | } 71 | } catch (e) { 72 | logger.error(`Validator rewards cron job failed: ${e.message}`) 73 | } 74 | } 75 | 76 | /** 77 | * Processes reward information and stores it in the database. 78 | * @param {Object} rewards - The reward object containing information about rewards. 79 | * @param {number} rewards.effectiveSlot - The effective slot number for the reward. 80 | * @param {number} rewards.amount - The amount of the reward in lamports. 81 | * @param {number} rewards.postBalance - The post balance in lamports after receiving the reward. 82 | * @param {number} rewards.epoch - The epoch number for the reward. 83 | * @param {number} epoch - The epoch number being processed. 84 | * @returns {Promise} A promise that resolves once the reward has been processed. 85 | */ 86 | const processReward = async (rewards, epoch) => { 87 | const blockTime = await getBlockTime(rewards.effectiveSlot) 88 | const timestamp = setTimestampFormat( 89 | new Date(blockTime * 1000) // convert to milliseconds 90 | ) 91 | const solUsd = await fetchSolanaPriceAtDate(timestamp) 92 | 93 | const { postBalance } = rewards 94 | const postBalanceUsd = (postBalance / LAMPORTS_PER_SOL) * solUsd 95 | 96 | const reward = rewards.amount 97 | const rewardUsd = (rewards.amount / LAMPORTS_PER_SOL) * solUsd 98 | 99 | let totalReward = rewards.amount 100 | let pendingRewards = rewards.amount 101 | 102 | // Get the previous reward 103 | const previousReward = await Reward.findOne({ 104 | delegatorId: VALIDATOR_ID, 105 | }) 106 | .sort({ timestamp: -1 }) 107 | .exec() 108 | // If previous reward exists, add it to the current reward 109 | if (previousReward) { 110 | totalReward += previousReward.totalReward 111 | pendingRewards += previousReward.pendingRewards 112 | } 113 | const totalRewardUsd = (totalReward / LAMPORTS_PER_SOL) * solUsd 114 | const pendingRewardsUsd = (pendingRewards / LAMPORTS_PER_SOL) * solUsd 115 | 116 | await Reward.create({ 117 | delegatorId: VALIDATOR_ID, 118 | epochNum: rewards.epoch, 119 | solUsd, 120 | timestamp, 121 | postBalance, 122 | postBalanceUsd, 123 | userAction: 'REWARD', 124 | reward, 125 | rewardUsd, 126 | totalReward, 127 | totalRewardUsd, 128 | pendingRewards, 129 | pendingRewardsUsd, 130 | stakedAmount: -1, 131 | stakedAmountUsd: -1, 132 | }) 133 | 134 | logger.info(`processed reward for epoch [${epoch}]`) 135 | } 136 | 137 | export default validatorRewardsCron 138 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import logger from '../logger/logger.js' 2 | import Reward from '../models/Reward.js' 3 | import { 4 | getProgramAccounts, 5 | getAccountInfo, 6 | } from '../repository/network.repository.js' 7 | import dotenv from 'dotenv' 8 | 9 | // Load environment variables from .env file 10 | dotenv.config() 11 | 12 | const VALIDATOR_PUB_KEY = process.env.VALIDATOR_PUB_KEY 13 | 14 | /** 15 | * Helper function to create a JSON-RPC request body. 16 | * @param {string} method - JSON-RPC method name. 17 | * @param {Array} [params] - Optional parameters for the method. 18 | * @returns {Object} Formatted JSON-RPC request body. 19 | */ 20 | export const populateBody = (method, params = null) => { 21 | const body = { 22 | jsonrpc: '2.0', 23 | id: 1, 24 | method, 25 | } 26 | if (params) { 27 | body.params = params 28 | } 29 | return body 30 | } 31 | 32 | /** 33 | * Sets the timestamp format to UTC 00:00:00. 34 | * @param {Date} currentDate - The date object to format. 35 | * @returns {number} The formatted timestamp in milliseconds. 36 | */ 37 | export const setTimestampFormat = (currentDate) => { 38 | try { 39 | currentDate.setUTCHours(0) 40 | currentDate.setUTCMinutes(0) 41 | currentDate.setUTCSeconds(0) 42 | currentDate.setUTCMilliseconds(0) 43 | return currentDate.getTime() 44 | } catch (e) { 45 | logger.error(`Failed to set timestamp format: ${e.message}`) 46 | throw e 47 | } 48 | } 49 | 50 | /** 51 | * Fetches the inflation reward for a given delegatorId and epoch. 52 | * @param {string} delegatorId - The public key of the delegator. 53 | * @param {number} epoch - The epoch number. 54 | * @returns {Promise<{reward: number, postBalance: number}>} The reward and post balance. 55 | */ 56 | const findRewards = async (delegatorId, epoch) => { 57 | try { 58 | const data = await Reward.findOne({ 59 | delegatorId, 60 | epochNum: epoch, 61 | duplicate: false, 62 | }) 63 | if (!data) return { reward: 0, postBalance: 0 } 64 | const { reward, postBalance } = data 65 | return { 66 | reward, 67 | postBalance, 68 | } 69 | } catch (e) { 70 | logger.error(`Failed to fetch rewards: ${e.message}`) 71 | throw e 72 | } 73 | } 74 | 75 | /** 76 | * Calculates and returns the APR value for a given delegator ID based on the rewards from the last month until the latestEpoch. 77 | * @param {string} delegatorId - The ID of the delegator. 78 | * @param {number} latestEpoch - The latest epoch number. 79 | * @returns {Promise} The APR value. 80 | */ 81 | export const findAPRValue = async (delegatorId, latestEpoch) => { 82 | try { 83 | const currentDate = new Date() 84 | const lastMonthDate = new Date( 85 | currentDate.setMonth(currentDate.getMonth() - 1) 86 | ) 87 | 88 | const previousRewards = await Reward.find({ 89 | delegatorId, 90 | timestamp: { $gte: lastMonthDate }, 91 | }).sort({ timestamp: 1 }) 92 | 93 | if (!previousRewards.length) return 0 94 | const startEpoch = rewards[rewards.length - 1].epochNum 95 | const numEpochs = latestEpoch - startEpoch + 1 96 | 97 | const rewardsPromises = [] 98 | for (let epoch = startEpoch; epoch < latestEpoch; epoch++) { 99 | rewardsPromises.push(findRewards(delegatorId, epoch)) 100 | } 101 | const rewards = await Promise.all(rewardsPromises) 102 | 103 | let totalAmount = 0, 104 | totalPostBalance = 0 105 | rewards.forEach((reward, index) => { 106 | if (index !== 0) totalAmount += reward.reward 107 | if (index !== rewards.length - 1) 108 | totalPostBalance += reward.postBalance 109 | }) 110 | 111 | const apr = (totalAmount / totalPostBalance) * (numEpochs * 12) * 100 112 | return isNaN(apr) ? 0 : apr 113 | } catch (e) { 114 | logger.error(`Failed to calculate APR value: ${e.message}`) 115 | throw e 116 | } 117 | } 118 | 119 | /** 120 | * Fetches stake information for a given public key. 121 | * @param {string} pubkey - The public key to fetch stake info for. 122 | * @returns {Promise<{pubkey: string, activationEpoch: number, deactivationEpoch: number, stake: number}|undefined>} The stake information or undefined if not found. 123 | */ 124 | const findStakeInfo = async (pubkey) => { 125 | try { 126 | const data = await getAccountInfo(pubkey) 127 | 128 | if (data && data.result) { 129 | const { delegation } = data.result.value.data.parsed.info.stake 130 | const { activationEpoch, deactivationEpoch, stake } = delegation 131 | return { 132 | pubkey, 133 | activationEpoch: parseInt(activationEpoch), 134 | deactivationEpoch: parseInt(deactivationEpoch), 135 | stake: parseFloat(stake), 136 | } 137 | } 138 | } catch (e) { 139 | logger.error(`Failed to fetch stake info [${pubkey}]: ${e.message}`) 140 | throw e 141 | } 142 | } 143 | 144 | /** 145 | * Fetches and returns active delegators for a given validator. 146 | * @param {string} [validatorId=VALIDATOR_PUB_KEY] - The public key of the validator. 147 | * @returns {Promise} An array of active delegators. 148 | */ 149 | export const findDelegators = async (validatorId = VALIDATOR_PUB_KEY) => { 150 | try { 151 | const data = await getProgramAccounts(validatorId) 152 | 153 | if (data && data.result) { 154 | const delegators = data.result.map((obj) => obj.pubkey) 155 | const activeDelegatorChecks = delegators.map((delegator) => 156 | findStakeInfo(delegator) 157 | ) 158 | const activeDelegators = ( 159 | await Promise.all(activeDelegatorChecks) 160 | ).filter(Boolean) 161 | return activeDelegators 162 | } 163 | } catch (e) { 164 | logger.error(`Failed to retrieve program accounts: ${e.message}`) 165 | throw e 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /docs/data/search.json: -------------------------------------------------------------------------------- 1 | {"list":[{"title":"LAMPORTS_PER_SOL","link":"LAMPORTS_PER_SOL","description":"

Important Notes on Delegation and Rewards:

\n
    \n
  • \n

    Activation Epoch:\nThe epoch when a delegation is activated.

    \n
  • \n
  • \n

    Reward Beginning Epoch:\nRewards start accruing one epoch after the Activation Epoch.

    \n
  • \n
  • \n

    Withdrawal Implications:\nUpon withdrawal, the 'postBalance' will be less than the balance from the previous reward.

    \n
  • \n
"},{"title":"connectDB","link":"connectDB","description":"

Establishes a connection to the MongoDB using the provided configurations.\nConnection configurations are sourced from environment variables.

"},{"title":"convertSolUsd","link":"convertSolUsd","description":"

Calculates the USD value of the post balance, reward, and staked amount.

"},{"title":"createDelegateTransaction","link":"createDelegateTransaction","description":"

Create and persist a delegate transaction

"},{"title":"cronHandles","link":"cronHandles","description":"

Initialize and run essential services.

"},{"title":"delegatorCron","link":"delegatorCron","description":"

Scheduled job to manage delegators.

"},{"title":"delegatorJob","link":"delegatorJob","description":"

Job to create new delegators and update already populated ones.

"},{"title":"fetchLatestEpoch","link":"fetchLatestEpoch","description":"

Fetch the current epoch number from the Solana network.

"},{"title":"fetchSolanaPriceAtDate","link":"fetchSolanaPriceAtDate","description":"

Fetch the Solana price at a specific date.

"},{"title":"findAPRValue","link":"findAPRValue","description":"

Calculates and returns the APR value for a given delegator ID based on the rewards from the last month until the latestEpoch.

"},{"title":"findDelegators","link":"findDelegators","description":"

Fetches and returns active delegators for a given validator.

"},{"title":"findRewards","link":"findRewards","description":"

Fetches the inflation reward for a given delegatorId and epoch.

"},{"title":"findStakeInfo","link":"findStakeInfo","description":"

Fetches stake information for a given public key.

"},{"title":"getAccountInfo","link":"getAccountInfo","description":"

Get information about a specific account.

"},{"title":"getBlockTime","link":"getBlockTime","description":"

Fetch the block time of a given effective slot.

"},{"title":"getInflationReward","link":"getInflationReward","description":"

Fetch the inflation rewards of delegators for a specific epoch.

"},{"title":"getProgramAccounts","link":"getProgramAccounts","description":"

Get the accounts associated with a given program.

"},{"title":"getSignaturesForAddress","link":"getSignaturesForAddress","description":"

Fetch transaction signatures related to an address.

"},{"title":"getTransaction","link":"getTransaction","description":"

Fetch details of a transaction by its signature.

"},{"title":"initializeRewardData","link":"initializeRewardData","description":"

Initializes reward data for a delegator based on the reward amount and staked SOL.

"},{"title":"isRewardValidForEpoch","link":"isRewardValidForEpoch","description":"

Checks if the reward is valid for the specified epoch based on activation and deactivation epochs.

"},{"title":"populateBody","link":"populateBody","description":"

Helper function to create a JSON-RPC request body.

"},{"title":"populateSolUsd","link":"populateSolUsd","description":"

Populate the solUsd field in all Reward model instances

"},{"title":"processDelegator","link":"processDelegator","description":"

Process individual delegator.

"},{"title":"processReward","link":"processReward","description":"

Processes reward information and stores it in the database.

"},{"title":"processUnstaking","link":"processUnstaking","description":"

Handle unstaking based on epoch conditions.

"},{"title":"request.GET","link":"GET","description":"

Sends a GET request to the given URL.\nImplements exponential retry on failure up to a max of 5 minutes.

"},{"title":"request.POST","link":"POST","description":"

Sends a POST request to the given URL.\nImplements exponential retry on failure up to a max of 5 minutes.

"},{"title":"rewardsCron","link":"rewardsCron","description":"

Initializes the function for rewards cron job.

"},{"title":"rewardsJob","link":"rewardsJob","description":"

Job to populate rewards from the start epoch to the latest epoch for all delegators

"},{"title":"setTimestampFormat","link":"setTimestampFormat","description":"

Sets the timestamp format to UTC 00:00:00.

"},{"title":"sleep","link":"sleep","description":"

Makes a delay for a given amount of time.

"},{"title":"validatorRewardsCron","link":"validatorRewardsCron","description":"

Initializes the function for rewards cron job.

"},{"title":"validatorRewardsJob","link":"validatorRewardsJob","description":"

Job to populate validator rewards from the start epoch to the latest epoch

"}]} -------------------------------------------------------------------------------- /src/repository/network.repository.js: -------------------------------------------------------------------------------- 1 | import request from '../config/axiosInstance.js' 2 | import { populateBody } from '../utils/index.js' 3 | import logger from '../logger/logger.js' 4 | import dotenv from 'dotenv' 5 | 6 | // Load environment variables from .env file 7 | dotenv.config() 8 | 9 | const SOLANA_ENDPOINT = process.env.SOLANA_ENDPOINT 10 | const EXCHANGE_URL = 'https://api.coingecko.com/api/v3' 11 | const STAKE_PROGRAM_ID = 'Stake11111111111111111111111111111111111111' 12 | 13 | /** 14 | * Fetch the current epoch number from the Solana network. 15 | * @returns {Promise} The current epoch number, or undefined if an error occurs. 16 | * @throws {Error} - If the network request failed. 17 | */ 18 | export const fetchLatestEpoch = async () => { 19 | try { 20 | const body = populateBody('getEpochInfo') 21 | const data = await request.POST(SOLANA_ENDPOINT, body) 22 | return data.result.epoch 23 | } catch (e) { 24 | logger.error(`Failed to fetch latest epoch number: ${e.message}`) 25 | throw e 26 | } 27 | } 28 | 29 | /** 30 | * Get the accounts associated with a given program. 31 | * @param {string} validatorId - The public key of the validator. 32 | * @returns {Promise} An object containing the accounts associated with the program, or undefined if an error occurs. 33 | * @throws {Error} - If the network request failed. 34 | */ 35 | export const getProgramAccounts = async (validatorId) => { 36 | try { 37 | const params = [ 38 | STAKE_PROGRAM_ID, 39 | { 40 | commitment: 'confirmed', 41 | encoding: 'base64', 42 | dataSize: 200, 43 | filters: [ 44 | { 45 | memcmp: { 46 | offset: 124, 47 | bytes: validatorId, 48 | }, 49 | }, 50 | ], 51 | }, 52 | ] 53 | const data = await request.POST( 54 | SOLANA_ENDPOINT, 55 | populateBody('getProgramAccounts', params) 56 | ) 57 | return data 58 | } catch (e) { 59 | logger.error(`Failed to fetch program accounts: ${e.message}`) 60 | throw e 61 | } 62 | } 63 | 64 | /** 65 | * Get information about a specific account. 66 | * @param {string} pubkey - The public key of the account to query. 67 | * @returns {Promise} An object containing the account information, or undefined if an error occurs. 68 | * @throws {Error} - If the network request failed. 69 | */ 70 | export const getAccountInfo = async (pubkey) => { 71 | try { 72 | const data = await request.POST( 73 | SOLANA_ENDPOINT, 74 | populateBody('getAccountInfo', [pubkey, { encoding: 'jsonParsed' }]) 75 | ) 76 | return data 77 | } catch (e) { 78 | logger.error(`Failed to fetch account info [${pubkey}]: ${e.message}`) 79 | throw e 80 | } 81 | } 82 | 83 | /** 84 | * Fetch the Solana price at a specific date. 85 | * @param {string} timestamp - The timestamp (in ISO 8601 format) for which to fetch the price. 86 | * @returns {Promise} The price of Solana in USD at the specified date, or undefined if an error occurs. 87 | * @throws {Error} - If the network request failed. 88 | */ 89 | export const fetchSolanaPriceAtDate = async (timestamp) => { 90 | try { 91 | const d = new Date(timestamp) 92 | const date = d.getUTCDate() 93 | const month = d.getUTCMonth() + 1 94 | const year = d.getUTCFullYear() 95 | const dateParam = `${date}-${month}-${year}` 96 | 97 | const url = `${EXCHANGE_URL}/coins/solana/history` 98 | const data = await request.GET(url, { 99 | params: { localization: false, date: dateParam }, 100 | }) 101 | return data.market_data.current_price.usd 102 | } catch (e) { 103 | logger.error(`Failed to fetch Solana price: ${e.message}`) 104 | throw e 105 | } 106 | } 107 | 108 | /** 109 | * Fetch the inflation rewards of delegators for a specific epoch. 110 | * @param {string[]} delegatorPubKeys - An array of public keys of the delegators. 111 | * @param {number} epoch - The epoch number for which to fetch the rewards. 112 | * @returns {Promise} An object containing the rewards information, or undefined if an error occurs. 113 | * @throws {Error} - If the network request failed. 114 | */ 115 | export const getInflationReward = async (delegatorPubKeys, epoch) => { 116 | try { 117 | const body = populateBody('getInflationReward', [ 118 | delegatorPubKeys, 119 | { epoch }, 120 | ]) 121 | const data = await request.POST(SOLANA_ENDPOINT, body) 122 | return data 123 | } catch (e) { 124 | logger.error( 125 | `Failed to fetch delegator rewards for epoch: ${epoch} [${delegatorPubKeys.join( 126 | ', ' 127 | )}]: ${e.message}` 128 | ) 129 | throw e 130 | } 131 | } 132 | 133 | /** 134 | * Fetch the block time of a given effective slot. 135 | * @param {number} effectiveSlot - The effective slot number. 136 | * @returns {Promise} The block time in Unix timestamp format, or undefined if an error occurs. 137 | * @throws {Error} - If the network request failed. 138 | */ 139 | export const getBlockTime = async (effectiveSlot) => { 140 | try { 141 | const body = populateBody('getBlockTime', [effectiveSlot]) 142 | const { result } = await request.POST(SOLANA_ENDPOINT, body) 143 | if (!result) logger.error('Block time not found') 144 | return result 145 | } catch (e) { 146 | logger.error(`Failed to fetch block time: ${e.message}`) 147 | throw e 148 | } 149 | } 150 | 151 | /** 152 | * Fetch transaction signatures related to an address. 153 | * @param {string} address - The public key of the account. 154 | * @returns {Promise} An object containing the transaction signatures, or undefined if an error occurs. 155 | * @throws {Error} - If the network request failed. 156 | */ 157 | export const getSignaturesForAddress = async (address) => { 158 | try { 159 | const body = populateBody('getSignaturesForAddress', [address]) 160 | const data = await request.POST(SOLANA_ENDPOINT, body) 161 | return data 162 | } catch (e) { 163 | logger.error( 164 | `Failed to fetch transaction signatures [${address}]: ${e.message}` 165 | ) 166 | throw e 167 | } 168 | } 169 | 170 | /** 171 | * Fetch details of a transaction by its signature. 172 | * @param {string} signature - The transaction signature. 173 | * @returns {Promise} An object containing the transaction details, or undefined if an error occurs. 174 | * @throws {Error} - If the network request failed. 175 | */ 176 | export const getTransaction = async (signature) => { 177 | try { 178 | const body = populateBody('getTransaction', [signature, 'json']) 179 | const data = await request.POST(SOLANA_ENDPOINT, body) 180 | return data 181 | } catch (e) { 182 | logger.error( 183 | `Failed to fetch transaction details [${signature}]: ${e.message}` 184 | ) 185 | throw e 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /docs/styles/clean-jsdoc-theme-dark.css: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background: #ffce76; 3 | color: #222; 4 | } 5 | 6 | body { 7 | background-color: #1a1a1a; 8 | color: #fff; 9 | } 10 | 11 | a, 12 | a:active { 13 | color: #00bbff; 14 | } 15 | 16 | hr { 17 | color: #222; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | color: #fff; 27 | } 28 | 29 | .sidebar { 30 | background-color: #222; 31 | color: #999; 32 | } 33 | 34 | .sidebar-title { 35 | color: #999; 36 | } 37 | 38 | .sidebar-section-title { 39 | color: #999; 40 | } 41 | 42 | .sidebar-section-title:hover { 43 | background: #252525; 44 | } 45 | 46 | .with-arrow { 47 | fill: #999; 48 | } 49 | 50 | .sidebar-section-children-container { 51 | background: #292929; 52 | } 53 | 54 | .sidebar-section-children a:hover { 55 | background: #2c2c2c; 56 | } 57 | 58 | .sidebar-section-children a { 59 | color: #fff; 60 | } 61 | 62 | .navbar-container { 63 | background: #1a1a1a; 64 | } 65 | 66 | .icon-button svg, 67 | .navbar-item a { 68 | color: #999; 69 | fill: #999; 70 | } 71 | 72 | .font-size-tooltip .icon-button svg { 73 | fill: #fff; 74 | } 75 | 76 | .font-size-tooltip .icon-button.disabled { 77 | background: #999; 78 | } 79 | 80 | .icon-button:hover { 81 | background: #333; 82 | } 83 | 84 | .icon-button:active { 85 | background: #444; 86 | } 87 | 88 | .navbar-item a:active { 89 | color: #aaa; 90 | background-color: #222; 91 | } 92 | 93 | .navbar-item:hover { 94 | background: #202020; 95 | } 96 | 97 | .footer { 98 | background: #222; 99 | color: #999; 100 | } 101 | 102 | .footer a { 103 | color: #999; 104 | } 105 | 106 | .toc-link { 107 | color: #777; 108 | transition: color 0.3s; 109 | font-size: 0.875rem; 110 | } 111 | 112 | .toc-link.is-active-link { 113 | color: #fff; 114 | } 115 | 116 | .has-anchor .link-anchor { 117 | color: #555; 118 | } 119 | 120 | .has-anchor .link-anchor:hover { 121 | color: #888; 122 | } 123 | 124 | tt, 125 | code, 126 | kbd, 127 | samp { 128 | background: #333; 129 | } 130 | 131 | .signature-attributes { 132 | color: #aaa; 133 | } 134 | 135 | .ancestors { 136 | color: #999; 137 | } 138 | 139 | .ancestors a { 140 | color: #999 !important; 141 | } 142 | 143 | .important { 144 | color: #c51313; 145 | } 146 | 147 | .type-signature { 148 | color: #00918e; 149 | } 150 | 151 | .name, 152 | .name a { 153 | color: #f7f7f7; 154 | } 155 | 156 | .details { 157 | background: #222; 158 | color: #fff; 159 | } 160 | 161 | .prettyprint { 162 | background: #222; 163 | } 164 | 165 | .member-item-container strong, 166 | .method-member-container strong { 167 | color: #fff; 168 | } 169 | 170 | .pre-top-bar-container { 171 | background: #292929; 172 | } 173 | 174 | .prettyprint.source, 175 | .prettyprint code { 176 | background-color: #222; 177 | color: #c9d1d9; 178 | } 179 | 180 | .pre-div { 181 | background-color: #222; 182 | } 183 | 184 | .hljs .hljs-ln-numbers { 185 | color: #777; 186 | } 187 | 188 | .hljs .selected { 189 | background: #444; 190 | } 191 | 192 | .hljs .selected .hljs-ln-numbers { 193 | color: #eee; 194 | } 195 | 196 | /* stylelint-enable */ 197 | 198 | table .name, 199 | .params .name, 200 | .props .name, 201 | .name code { 202 | color: #fff; 203 | } 204 | 205 | table td, 206 | .params td { 207 | background-color: #292929; 208 | } 209 | 210 | table thead th, 211 | .params thead th, 212 | .props thead th { 213 | background-color: #222; 214 | color: #fff; 215 | } 216 | 217 | /* stylelint-disable */ 218 | table .params thead tr, 219 | .params .params thead tr, 220 | .props .props thead tr { 221 | background-color: #222; 222 | color: #fff; 223 | } 224 | 225 | .disabled { 226 | color: #aaaaaa; 227 | } 228 | 229 | .code-lang-name { 230 | color: #ff8a00; 231 | } 232 | 233 | .tooltip { 234 | background: #ffce76; 235 | color: #222; 236 | } 237 | 238 | /* code */ 239 | .hljs-comment { 240 | color: #8b949e; 241 | } 242 | 243 | .hljs-doctag, 244 | .hljs-keyword, 245 | .hljs-template-tag, 246 | .hljs-variable.language_ { 247 | color: #ff7b72; 248 | } 249 | 250 | .hljs-template-variable, 251 | .hljs-type { 252 | color: #30ac7c; 253 | } 254 | 255 | .hljs-meta, 256 | .hljs-string, 257 | .hljs-regexp { 258 | color: #a5d6ff; 259 | } 260 | 261 | .hljs-title.class_, 262 | .hljs-title { 263 | color: #ffa657; 264 | } 265 | 266 | .hljs-title.class_.inherited__, 267 | .hljs-title.function_ { 268 | color: #d2a8ff; 269 | } 270 | 271 | .hljs-attr, 272 | .hljs-attribute, 273 | .hljs-literal, 274 | .hljs-meta, 275 | .hljs-number, 276 | .hljs-operator, 277 | .hljs-selector-attr, 278 | .hljs-selector-class, 279 | .hljs-selector-id, 280 | .hljs-variable { 281 | color: #79c0ff; 282 | } 283 | 284 | .hljs-meta .hljs-string, 285 | .hljs-regexp, 286 | .hljs-string { 287 | color: #a5d6ff; 288 | } 289 | 290 | .hljs-built_in, 291 | .hljs-symbol { 292 | color: #ffa657; 293 | } 294 | 295 | .hljs-code, 296 | .hljs-comment, 297 | .hljs-formula { 298 | color: #8b949e; 299 | } 300 | 301 | .hljs-name, 302 | .hljs-quote, 303 | .hljs-selector-pseudo, 304 | .hljs-selector-tag { 305 | color: #7ee787; 306 | } 307 | 308 | .hljs-subst { 309 | color: #c9d1d9; 310 | } 311 | 312 | .hljs-section { 313 | color: #1f6feb; 314 | font-weight: 700; 315 | } 316 | 317 | .hljs-bullet { 318 | color: #f2cc60; 319 | } 320 | 321 | .hljs-emphasis { 322 | color: #c9d1d9; 323 | font-style: italic; 324 | } 325 | 326 | .hljs-strong { 327 | color: #c9d1d9; 328 | font-weight: 700; 329 | } 330 | 331 | /* code end*/ 332 | 333 | blockquote { 334 | background: #222; 335 | color: #fff; 336 | } 337 | 338 | .search-container { 339 | background: rgba(255, 255, 255, 0.1); 340 | } 341 | 342 | .icon-button.search-close-button svg { 343 | fill: #a00; 344 | } 345 | 346 | .search-container .wrapper { 347 | background: #222; 348 | } 349 | 350 | .search-result-c { 351 | color: #666; 352 | } 353 | 354 | .search-box-c { 355 | fill: #333; 356 | } 357 | 358 | .search-input { 359 | background: #333; 360 | color: #fff; 361 | } 362 | 363 | .search-box-c svg { 364 | fill: #fff; 365 | } 366 | 367 | .search-result-item { 368 | background: #333; 369 | } 370 | 371 | .search-result-item:hover { 372 | background: #444; 373 | } 374 | 375 | .search-result-item:active { 376 | background: #555; 377 | } 378 | 379 | .search-result-item-title { 380 | color: #fff; 381 | } 382 | 383 | .search-result-item-p { 384 | color: #aaa; 385 | } 386 | 387 | .mobile-menu-icon-container .icon-button { 388 | background: #333; 389 | } 390 | 391 | .mobile-sidebar-container { 392 | background: #1a1a1a; 393 | } 394 | 395 | .mobile-sidebar-wrapper { 396 | background: #222; 397 | } 398 | 399 | /* scroll bar */ 400 | ::-webkit-scrollbar-track { 401 | background: #333; 402 | } 403 | 404 | ::-webkit-scrollbar-thumb { 405 | background: #555; 406 | outline: 0.06125rem solid #555; 407 | } 408 | -------------------------------------------------------------------------------- /docs/styles/clean-jsdoc-theme-light.css: -------------------------------------------------------------------------------- 1 | .light ::selection { 2 | background: #ffce76; 3 | color: #1d1919; 4 | } 5 | 6 | body.light { 7 | background-color: #fff; 8 | color: #111; 9 | } 10 | 11 | .light a, 12 | .light a:active { 13 | color: #007bff; 14 | } 15 | 16 | .light hr { 17 | color: #f7f7f7; 18 | } 19 | 20 | .light h1, 21 | .light h2, 22 | .light h3, 23 | .light h4, 24 | .light h5, 25 | .light h6 { 26 | color: #111; 27 | } 28 | 29 | .light .sidebar { 30 | background-color: #f7f7f7; 31 | color: #222; 32 | } 33 | 34 | .light .sidebar-title { 35 | color: #222; 36 | } 37 | 38 | .light .sidebar-section-title { 39 | color: #222; 40 | } 41 | 42 | .light .sidebar-section-title:hover { 43 | background: #eee; 44 | } 45 | 46 | .light .with-arrow { 47 | fill: #111; 48 | } 49 | 50 | .light .sidebar-section-children-container { 51 | background: #eee; 52 | } 53 | 54 | .light .sidebar-section-children a:hover { 55 | background: #e0e0e0; 56 | } 57 | 58 | .light .sidebar-section-children a { 59 | color: #111; 60 | } 61 | 62 | .light .navbar-container { 63 | background: #fff; 64 | } 65 | 66 | .light .icon-button svg, 67 | .light .navbar-item a { 68 | color: #222; 69 | fill: #222; 70 | } 71 | 72 | .light .tippy-box { 73 | background: #eee; 74 | color: #111; 75 | } 76 | 77 | .light .tippy-arrow { 78 | color: #f1f1f1; 79 | } 80 | 81 | .light .font-size-tooltip .icon-button svg { 82 | fill: #111; 83 | } 84 | 85 | .light .font-size-tooltip .icon-button.disabled svg { 86 | fill: #999; 87 | } 88 | 89 | .light .icon-button:hover { 90 | background: #ddd; 91 | } 92 | 93 | .light .icon-button:active { 94 | background: #ccc; 95 | } 96 | 97 | .light .navbar-item a:active { 98 | color: #333; 99 | background-color: #eee; 100 | } 101 | 102 | .light .navbar-item:hover { 103 | background: #f7f7f7; 104 | } 105 | 106 | .light .footer { 107 | background: #f7f7f7; 108 | color: #111; 109 | } 110 | 111 | .light .footer a { 112 | color: #111; 113 | } 114 | 115 | .light .toc-link { 116 | color: #999; 117 | transition: color 0.3s; 118 | font-size: 0.875rem; 119 | } 120 | 121 | .light .toc-link.is-active-link { 122 | color: #111; 123 | } 124 | 125 | .light .has-anchor .link-anchor { 126 | color: #ddd; 127 | } 128 | 129 | .light .has-anchor .link-anchor:hover { 130 | color: #ccc; 131 | } 132 | 133 | .light .signature-attributes { 134 | color: #aaa; 135 | } 136 | 137 | .light .ancestors { 138 | color: #999; 139 | } 140 | 141 | .light .ancestors a { 142 | color: #999 !important; 143 | } 144 | 145 | .light .important { 146 | color: #ee1313; 147 | } 148 | 149 | .light .type-signature { 150 | color: #00918e; 151 | } 152 | 153 | .light .name, 154 | .light .name a { 155 | color: #293a80; 156 | } 157 | 158 | .light .details { 159 | background: #f9f9f9; 160 | color: #101010; 161 | } 162 | 163 | .light .member-item-container strong, 164 | .light .method-member-container strong { 165 | color: #000; 166 | } 167 | 168 | .light .prettyprint { 169 | background: #f7f7f7; 170 | } 171 | 172 | .light .pre-div { 173 | background: #f7f7f7; 174 | } 175 | 176 | .light .hljs .hljs-ln-numbers { 177 | color: #aaa; 178 | } 179 | 180 | .light .hljs .selected { 181 | background: #ccc; 182 | } 183 | 184 | .light table.hljs-ln td { 185 | background: none; 186 | } 187 | 188 | .light .hljs .selected .hljs-ln-numbers { 189 | color: #444; 190 | } 191 | 192 | .light .pre-top-bar-container { 193 | background-color: #eee; 194 | } 195 | 196 | .light .prettyprint code { 197 | background-color: #f7f7f7; 198 | } 199 | 200 | .light table .name, 201 | .light .params .name, 202 | .light .props .name, 203 | .light .name code { 204 | color: #4d4e53; 205 | } 206 | 207 | .light table td, 208 | .light .params td { 209 | background: #f7f7f7; 210 | } 211 | 212 | .light table thead th, 213 | .light .params thead th, 214 | .light .props thead th { 215 | background-color: #eee; 216 | color: #111; 217 | } 218 | 219 | /* stylelint-disable */ 220 | .light table .params thead tr, 221 | .light .params .params thead tr, 222 | .light .props .props thead tr { 223 | background-color: #eee; 224 | color: #111; 225 | } 226 | 227 | .light .disabled { 228 | color: #454545; 229 | } 230 | 231 | .light .code-lang-name { 232 | color: #ff0000; 233 | } 234 | 235 | .light .tooltip { 236 | background: #ffce76; 237 | color: #000; 238 | } 239 | 240 | /* Code */ 241 | 242 | .light .hljs-comment, 243 | .light .hljs-quote { 244 | color: #a0a1a7; 245 | } 246 | 247 | .light .hljs-doctag, 248 | .light .hljs-keyword, 249 | .light .hljs-formula { 250 | color: #a626a4; 251 | } 252 | 253 | .light .hljs-section, 254 | .light .hljs-name, 255 | .light .hljs-selector-tag, 256 | .light .hljs-deletion, 257 | .light .hljs-subst { 258 | color: #e45649; 259 | } 260 | 261 | .light .hljs-literal { 262 | color: #0184bb; 263 | } 264 | 265 | .light .hljs-string, 266 | .light .hljs-regexp, 267 | .light .hljs-addition, 268 | .light .hljs-attribute, 269 | .light .hljs-meta .hljs-string { 270 | color: #50a14f; 271 | } 272 | 273 | .light .hljs-attr, 274 | .light .hljs-variable, 275 | .light .hljs-template-variable, 276 | .light .hljs-type, 277 | .light .hljs-selector-class, 278 | .light .hljs-selector-attr, 279 | .light .hljs-selector-pseudo, 280 | .light .hljs-number { 281 | color: #986801; 282 | } 283 | 284 | .light .hljs-symbol, 285 | .light .hljs-bullet, 286 | .light .hljs-link, 287 | .light .hljs-meta, 288 | .light .hljs-selector-id, 289 | .light .hljs-title { 290 | color: #4078f2; 291 | } 292 | 293 | .light .hljs-built_in, 294 | .light .hljs-title.class_, 295 | .light .hljs-class .hljs-title { 296 | color: #c18401; 297 | } 298 | 299 | .light .hljs-emphasis { 300 | font-style: italic; 301 | } 302 | 303 | .light .hljs-strong { 304 | font-weight: bold; 305 | } 306 | 307 | .light .hljs-link { 308 | text-decoration: underline; 309 | } 310 | 311 | /* Code Ends */ 312 | 313 | .light blockquote { 314 | background: #eee; 315 | color: #111; 316 | } 317 | 318 | .light code { 319 | background: #ddd; 320 | color: #000; 321 | } 322 | 323 | .light .search-container { 324 | background: rgba(0, 0, 0, 0.1); 325 | } 326 | 327 | .light .search-close-button svg { 328 | fill: #f00; 329 | } 330 | 331 | .light .search-container .wrapper { 332 | background: #eee; 333 | } 334 | 335 | .light .search-result-c { 336 | color: #aaa; 337 | } 338 | 339 | .light .search-box-c svg { 340 | fill: #333; 341 | } 342 | 343 | .light .search-input { 344 | background: #f7f7f7; 345 | color: #111; 346 | } 347 | 348 | .light .search-result-item { 349 | background: #f7f7f7; 350 | } 351 | 352 | .light .search-result-item:hover { 353 | background: #e9e9e9; 354 | } 355 | 356 | .light .search-result-item:active { 357 | background: #f7f7f7; 358 | } 359 | 360 | .light .search-result-item-title { 361 | color: #111; 362 | } 363 | 364 | .light .search-result-item-p { 365 | color: #aaa; 366 | } 367 | 368 | .light .mobile-menu-icon-container .icon-button { 369 | background: #e5e5e5; 370 | } 371 | 372 | .light .mobile-sidebar-container { 373 | background: #fff; 374 | } 375 | 376 | .light .mobile-sidebar-wrapper { 377 | background: #f7f7f7; 378 | } 379 | 380 | /* scroll bar */ 381 | .light ::-webkit-scrollbar-track { 382 | background: #ddd; 383 | } 384 | 385 | .light ::-webkit-scrollbar-thumb { 386 | background: #aaa; 387 | outline: 0.06125rem solid #aaa; 388 | } 389 | -------------------------------------------------------------------------------- /docs/scripts/third-party/tocbot.min.js: -------------------------------------------------------------------------------- 1 | var defaultOptions={ignoreSelector:".js-toc-ignore",linkClass:"toc-link",extraLinkClasses:"",activeLinkClass:"is-active-link",listClass:"toc-list",extraListClasses:"",isCollapsedClass:"is-collapsed",collapsibleClass:"is-collapsible",listItemClass:"toc-list-item",activeListItemClass:"is-active-li",collapseDepth:0,scrollSmooth:!0,scrollSmoothDuration:420,scrollSmoothOffset:0,scrollEndCallback:function(e){},throttleTimeout:50,positionFixedSelector:null,positionFixedClass:"is-position-fixed",fixedSidebarOffset:"auto",includeHtml:!1,includeTitleTags:!1,orderedList:!0,scrollContainer:null,skipRendering:!1,headingLabelCallback:!1,ignoreHiddenElements:!1,headingObjectCallback:null,basePath:"",disableTocScrollSync:!1};function ParseContent(r){var t=[].reduce;function a(e){return e[e.length-1]}function c(e){if(!(e instanceof window.HTMLElement))return e;if(r.ignoreHiddenElements&&(!e.offsetHeight||!e.offsetParent))return null;var t=e.getAttribute("data-heading-label")||(r.headingLabelCallback?String(r.headingLabelCallback(e.textContent)):e.textContent.trim()),t={id:e.id,children:[],nodeName:e.nodeName,headingLevel:+e.nodeName.toUpperCase().replace("H",""),textContent:t};return r.includeHtml&&(t.childNodes=e.childNodes),r.headingObjectCallback?r.headingObjectCallback(t,e):t}return{nestHeadingsArray:function(e){return t.call(e,function(e,t){t=c(t);if(t){for(var l=e.nest,n=(t=c(t)).headingLevel,o=l,s=a(o),i=n-(s?s.headingLevel:0);0=r.collapseDepth&&(t.isCollapsed=!0),o.push(t)}return e},{nest:[]})},selectHeadings:function(e,t){var l=t;r.ignoreSelector&&(l=t.split(",").map(function(e){return e.trim()+":not("+r.ignoreSelector+")"}));try{return e.querySelectorAll(l)}catch(e){return console.warn("Headers not found with selector: "+l),null}}}}function BuildHtml(i){var r,n=[].forEach,a=[].some,c=document.body,d=document.querySelector(i.contentSelector),u=!0,m=" ";function o(e,t){var l,t=t.appendChild(function(e){var t=document.createElement("li"),l=document.createElement("a");i.listItemClass&&t.setAttribute("class",i.listItemClass);i.onClick&&(l.onclick=i.onClick);i.includeTitleTags&&l.setAttribute("title",e.textContent);i.includeHtml&&e.childNodes.length?n.call(e.childNodes,function(e){l.appendChild(e.cloneNode(!0))}):l.textContent=e.textContent;return l.setAttribute("href",i.basePath+"#"+e.id),l.setAttribute("class",i.linkClass+m+"node-name--"+e.nodeName+m+i.extraLinkClasses),t.appendChild(l),t}(e));e.children.length&&(l=s(e.isCollapsed),e.children.forEach(function(e){o(e,l)}),t.appendChild(l))}function s(e){var t=i.orderedList?"ol":"ul",t=document.createElement(t),l=i.listClass+m+i.extraListClasses;return e&&(l=(l+=m+i.collapsibleClass)+(m+i.isCollapsedClass)),t.setAttribute("class",l),t}function f(e){var t=[].forEach,l=r.querySelectorAll("."+i.linkClass),l=(t.call(l,function(e){e.className=e.className.split(m+i.activeLinkClass).join("")}),r.querySelectorAll("."+i.listItemClass)),l=(t.call(l,function(e){e.className=e.className.split(m+i.activeListItemClass).join("")}),r.querySelector("."+i.linkClass+".node-name--"+e.nodeName+'[href="'+i.basePath+"#"+e.id.replace(/([ #;&,.+*~':"!^$[\]()=>|/@])/g,"\\$1")+'"]')),e=(l&&-1===l.className.indexOf(i.activeLinkClass)&&(l.className+=m+i.activeLinkClass),l&&l.parentNode),e=(e&&-1===e.className.indexOf(i.activeListItemClass)&&(e.className+=m+i.activeListItemClass),r.querySelectorAll("."+i.listClass+"."+i.collapsibleClass));t.call(e,function(e){-1===e.className.indexOf(i.isCollapsedClass)&&(e.className+=m+i.isCollapsedClass)}),l&&l.nextSibling&&-1!==l.nextSibling.className.indexOf(i.isCollapsedClass)&&(l.nextSibling.className=l.nextSibling.className.split(m+i.isCollapsedClass).join("")),function e(t){if(t&&-1!==t.className.indexOf(i.collapsibleClass)&&-1!==t.className.indexOf(i.isCollapsedClass))return t.className=t.className.split(m+i.isCollapsedClass).join(""),e(t.parentNode.parentNode);return t}(l&&l.parentNode.parentNode)}return{enableTocAnimation:function(){u=!0},disableTocAnimation:function(e){"string"==typeof(e=e.target||e.srcElement).className&&-1!==e.className.indexOf(i.linkClass)&&(u=!1)},render:function(e,t){var l=s(!1);if(t.forEach(function(e){o(e,l)}),null!==(r=e||r))return r.firstChild&&r.removeChild(r.firstChild),0===t.length?r:r.appendChild(l)},updateToc:function(e){n=i.scrollContainer&&document.querySelector(i.scrollContainer)?document.querySelector(i.scrollContainer).scrollTop:document.documentElement.scrollTop||c.scrollTop,i.positionFixedSelector&&(t=i.scrollContainer&&document.querySelector(i.scrollContainer)?document.querySelector(i.scrollContainer).scrollTop:document.documentElement.scrollTop||c.scrollTop,l=document.querySelector(i.positionFixedSelector),"auto"===i.fixedSidebarOffset&&(i.fixedSidebarOffset=r.offsetTop),t>i.fixedSidebarOffset?-1===l.className.indexOf(i.positionFixedClass)&&(l.className+=m+i.positionFixedClass):l.className=l.className.split(m+i.positionFixedClass).join(""));var n,t,l,o,s=e;u&&null!==r&&0l?(o=s[0===t?t:t-1],!0):t===s.length-1?(o=s[s.length-1],!0):void 0}),f(o))},updateListActiveElement:f}}function updateTocScroll(e){var t,l=e.tocElement||document.querySelector(e.tocSelector);l&&l.scrollHeight>l.clientHeight&&((e=l.querySelector("."+e.activeListItemClass))&&(t=l.getBoundingClientRect().top,l.scrollTop=e.offsetTop-t))}!function(e,t){"function"==typeof define&&define.amd?define([],t(e)):"object"==typeof exports?module.exports=t(e):e.tocbot=t(e)}("undefined"!=typeof global?global:this.window||this.global,function(e){"use strict";var l,a,c,d,u={},m={},f=!!(e&&e.document&&e.document.querySelector&&e.addEventListener);if("undefined"!=typeof window||f)return d=Object.prototype.hasOwnProperty,m.destroy=function(){var e=C(u);null!==e&&(u.skipRendering||e&&(e.innerHTML=""),u.scrollContainer&&document.querySelector(u.scrollContainer)?(document.querySelector(u.scrollContainer).removeEventListener("scroll",this._scrollListener,!1),document.querySelector(u.scrollContainer).removeEventListener("resize",this._scrollListener,!1)):(document.removeEventListener("scroll",this._scrollListener,!1),document.removeEventListener("resize",this._scrollListener,!1)))},m.init=function(e){if(f){u=function(){for(var e={},t=0;t${text}`; 13 | } 14 | 15 | function hideSearch() { 16 | // eslint-disable-next-line no-undef 17 | if (window.location.hash === searchHash) { 18 | // eslint-disable-next-line no-undef 19 | history.go(-1); 20 | } 21 | 22 | // eslint-disable-next-line no-undef 23 | window.onhashchange = null; 24 | 25 | if (searchContainer) { 26 | searchContainer.style.display = 'none'; 27 | } 28 | } 29 | 30 | function listenCloseKey(event) { 31 | if (event.key === 'Escape') { 32 | hideSearch(); 33 | // eslint-disable-next-line no-undef 34 | window.removeEventListener('keyup', listenCloseKey); 35 | } 36 | } 37 | 38 | function showSearch() { 39 | try { 40 | // Closing mobile menu before opening 41 | // search box. 42 | // It is defined in core.js 43 | // eslint-disable-next-line no-undef 44 | hideMobileMenu(); 45 | } catch (error) { 46 | console.error(error); 47 | } 48 | 49 | // eslint-disable-next-line no-undef 50 | window.onhashchange = hideSearch; 51 | 52 | // eslint-disable-next-line no-undef 53 | if (window.location.hash !== searchHash) { 54 | // eslint-disable-next-line no-undef 55 | history.pushState(null, null, searchHash); 56 | } 57 | 58 | if (searchContainer) { 59 | searchContainer.style.display = 'flex'; 60 | // eslint-disable-next-line no-undef 61 | window.addEventListener('keyup', listenCloseKey); 62 | } 63 | 64 | if (searchInput) { 65 | searchInput.focus(); 66 | } 67 | } 68 | 69 | async function fetchAllData() { 70 | // eslint-disable-next-line no-undef 71 | const { hostname, protocol, port } = location; 72 | 73 | // eslint-disable-next-line no-undef 74 | const base = protocol + '//' + hostname + (port !== '' ? ':' + port : '') + baseURL; 75 | // eslint-disable-next-line no-undef 76 | const url = new URL('data/search.json', base); 77 | const result = await fetch(url); 78 | const { list } = await result.json(); 79 | 80 | return list; 81 | } 82 | 83 | // eslint-disable-next-line no-unused-vars 84 | function onClickSearchItem(event) { 85 | const target = event.currentTarget; 86 | 87 | if (target) { 88 | const href = target.getAttribute('href') || ''; 89 | let elementId = href.split('#')[1] || ''; 90 | let element = document.getElementById(elementId); 91 | 92 | if (!element) { 93 | elementId = decodeURI(elementId); 94 | element = document.getElementById(elementId); 95 | } 96 | 97 | if (element) { 98 | setTimeout(function() { 99 | // eslint-disable-next-line no-undef 100 | bringElementIntoView(element); // defined in core.js 101 | }, 100); 102 | } 103 | } 104 | } 105 | 106 | function buildSearchResult(result) { 107 | let output = ''; 108 | const removeHTMLTagsRegExp = /(<([^>]+)>)/ig; 109 | 110 | for (const res of result) { 111 | const { title = '', description = '' } = res.item; 112 | 113 | const _link = res.item.link.replace('.*/, ''); 114 | const _title = title.replace(removeHTMLTagsRegExp, ""); 115 | const _description = description.replace(removeHTMLTagsRegExp, ""); 116 | 117 | output += ` 118 | 119 |
${_title}
120 |
${_description || 'No description available.'}
121 |
122 | `; 123 | } 124 | 125 | return output; 126 | } 127 | 128 | function getSearchResult(list, keys, searchKey) { 129 | const defaultOptions = { 130 | shouldSort: true, 131 | threshold: 0.4, 132 | location: 0, 133 | distance: 100, 134 | maxPatternLength: 32, 135 | minMatchCharLength: 1, 136 | keys: keys 137 | }; 138 | 139 | const options = { ...defaultOptions }; 140 | 141 | // eslint-disable-next-line no-undef 142 | const searchIndex = Fuse.createIndex(options.keys, list); 143 | 144 | // eslint-disable-next-line no-undef 145 | const fuse = new Fuse(list, options, searchIndex); 146 | 147 | const result = fuse.search(searchKey); 148 | 149 | if (result.length > 20) { 150 | return result.slice(0, 20); 151 | } 152 | 153 | return result; 154 | } 155 | 156 | function debounce(func, wait, immediate) { 157 | let timeout; 158 | 159 | return function() { 160 | const args = arguments; 161 | 162 | clearTimeout(timeout); 163 | timeout = setTimeout(() => { 164 | timeout = null; 165 | if (!immediate) { 166 | // eslint-disable-next-line consistent-this, no-invalid-this 167 | func.apply(this, args); 168 | } 169 | }, wait); 170 | 171 | if (immediate && !timeout) { 172 | // eslint-disable-next-line consistent-this, no-invalid-this 173 | func.apply(this, args); 174 | } 175 | }; 176 | } 177 | 178 | let searchData; 179 | 180 | async function search(event) { 181 | const value = event.target.value; 182 | const keys = ['title', 'description']; 183 | 184 | if (!resultBox) { 185 | console.error('Search result container not found'); 186 | 187 | return; 188 | } 189 | 190 | if (!value) { 191 | showResultText('Type anything to view search result'); 192 | 193 | return; 194 | } 195 | 196 | if (!searchData) { 197 | showResultText('Loading...'); 198 | 199 | try { 200 | // eslint-disable-next-line require-atomic-updates 201 | searchData = await fetchAllData(); 202 | } catch (e) { 203 | console.log(e); 204 | showResultText('Failed to load result.'); 205 | 206 | return; 207 | } 208 | } 209 | 210 | const result = getSearchResult(searchData, keys, value); 211 | 212 | if (!result.length) { 213 | showResultText('No result found! Try some different combination.'); 214 | 215 | return; 216 | } 217 | 218 | // eslint-disable-next-line require-atomic-updates 219 | resultBox.innerHTML = buildSearchResult(result); 220 | } 221 | 222 | function onDomContentLoaded() { 223 | const searchButton = document.querySelectorAll('.search-button'); 224 | const debouncedSearch = debounce(search, 300); 225 | 226 | if (searchCloseButton) { 227 | searchCloseButton.addEventListener('click', hideSearch); 228 | } 229 | 230 | if (searchButton) { 231 | searchButton.forEach(function(item) { 232 | item.addEventListener('click', showSearch); 233 | }); 234 | } 235 | 236 | if (searchContainer) { 237 | searchContainer.addEventListener('click', hideSearch); 238 | } 239 | 240 | if (searchWrapper) { 241 | searchWrapper.addEventListener('click', function(event) { 242 | event.stopPropagation(); 243 | }); 244 | } 245 | 246 | if (searchInput) { 247 | searchInput.addEventListener('keyup', debouncedSearch); 248 | } 249 | 250 | // eslint-disable-next-line no-undef 251 | if (window.location.hash === searchHash) { 252 | showSearch(); 253 | } 254 | } 255 | 256 | // eslint-disable-next-line no-undef 257 | window.addEventListener('DOMContentLoaded', onDomContentLoaded); 258 | 259 | // eslint-disable-next-line no-undef 260 | window.addEventListener('hashchange', function() { 261 | // eslint-disable-next-line no-undef 262 | if (window.location.hash === searchHash) { 263 | showSearch(); 264 | } 265 | }); 266 | -------------------------------------------------------------------------------- /docs/scripts/core.min.js: -------------------------------------------------------------------------------- 1 | var accordionLocalStorageKey="accordion-id",themeLocalStorageKey="theme",fontSizeLocalStorageKey="font-size",html=document.querySelector("html"),MAX_FONT_SIZE=30,MIN_FONT_SIZE=10,localStorage=window.localStorage;function getTheme(){var e=localStorage.getItem(themeLocalStorageKey);if(e)return e;switch(e=document.body.getAttribute("data-theme")){case"dark":case"light":return e;case"fallback-dark":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";case"fallback-light":return window.matchMedia("(prefers-color-scheme)").matches&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";default:return"dark"}}function localUpdateTheme(e){var t=document.body,o=document.querySelectorAll(".theme-svg-use"),n="dark"===e?"#light-theme-icon":"#dark-theme-icon";t.setAttribute("data-theme",e),t.classList.remove("dark","light"),t.classList.add(e),o.forEach(function(e){e.setAttribute("xlink:href",n)})}function updateTheme(e){localUpdateTheme(e),localStorage.setItem(themeLocalStorageKey,e)}function toggleTheme(){updateTheme("dark"===document.body.getAttribute("data-theme")?"light":"dark")}function setAccordionIdToLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));t[e]=e,localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function removeAccordionIdFromLocalStorage(e){var t=JSON.parse(localStorage.getItem(accordionLocalStorageKey));delete t[e],localStorage.setItem(accordionLocalStorageKey,JSON.stringify(t))}function getAccordionIdsFromLocalStorage(){return JSON.parse(localStorage.getItem(accordionLocalStorageKey))||{}}function toggleAccordion(e){"false"===e.getAttribute("data-isopen")?(e.setAttribute("data-isopen","true"),setAccordionIdToLocalStorage(e.id)):(e.setAttribute("data-isopen","false"),removeAccordionIdFromLocalStorage(e.id))}function initAccordion(){void 0!==localStorage.getItem(accordionLocalStorageKey)&&null!==localStorage.getItem(accordionLocalStorageKey)||localStorage.setItem(accordionLocalStorageKey,"{}");var e=document.querySelectorAll(".sidebar-section-title"),t=getAccordionIdsFromLocalStorage();e.forEach(function(e){e.addEventListener("click",function(){toggleAccordion(e)}),e.id in t&&toggleAccordion(e)})}function isSourcePage(){return Boolean(document.querySelector("#source-page"))}function bringElementIntoView(e,t=!0){var o,n,i,c;e&&(tocbotInstance&&setTimeout(()=>tocbotInstance.updateTocListActiveElement(e),60),o=document.querySelector(".navbar-container"),n=document.querySelector(".main-content"),i=e.getBoundingClientRect().top,c=16,o&&(c+=o.scrollHeight),n&&n.scrollBy(0,i-c),t&&history.pushState(null,null,"#"+e.id))}function bringLinkToView(e){e.preventDefault(),e.stopPropagation();var e=e.currentTarget.getAttribute("href");!e||(e=document.getElementById(e.slice(1)))&&bringElementIntoView(e)}function bringIdToViewOnMount(){var e,t;isSourcePage()||""!==(e=window.location.hash)&&((t=document.getElementById(e.slice(1)))||(e=decodeURI(e),t=document.getElementById(e.slice(1))),t&&bringElementIntoView(t,!1))}function createAnchorElement(e){var t=document.createElement("a");return t.textContent="#",t.href="#"+e,t.classList.add("link-anchor"),t.onclick=bringLinkToView,t}function addAnchor(){var e=document.querySelector(".main-content").querySelector("section");[e.querySelectorAll("h1"),e.querySelectorAll("h2"),e.querySelectorAll("h3"),e.querySelectorAll("h4")].forEach(function(e){e.forEach(function(e){var t=createAnchorElement(e.id);e.classList.add("has-anchor"),e.append(t)})})}function copy(e){const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}function showTooltip(e){var t=document.getElementById(e);t.classList.add("show-tooltip"),setTimeout(function(){t.classList.remove("show-tooltip")},3e3)}function copyFunction(e){var t=document.getElementById(e);copy((t.querySelector(".linenums")||t.querySelector("code")).innerText.trim().replace(/(^\t)/gm,"")),showTooltip("tooltip-"+e)}function hideTocOnSourcePage(){isSourcePage()&&(document.querySelector(".toc-container").style.display="none")}function getPreTopBar(e,t=""){e='";return'
'+('
'+t.toLocaleUpperCase()+"
")+e+"
"}function getPreDiv(){var e=document.createElement("div");return e.classList.add("pre-div"),e}function processAllPre(){var e=document.querySelectorAll("pre"),t=document.querySelector("#PeOAagUepe"),o=document.querySelector("#VuAckcnZhf"),n=0,i=0,c=(t&&(i=t.getBoundingClientRect().height),o&&(n=o.getBoundingClientRect().height),window.innerHeight-n-i-250);e.forEach(function(e,t){var o,n=e.parentNode;n&&"true"===n.getAttribute("data-skip-pre-process")||(n=getPreDiv(),o=getPreTopBar(t="ScDloZOMdL"+t,e.getAttribute("data-lang")||"code"),n.innerHTML=o,e.style.maxHeight=c+"px",e.id=t,e.classList.add("prettyprint"),e.parentNode.insertBefore(n,e),n.appendChild(e))})}function highlightAndBringLineIntoView(){var e=window.location.hash.replace("#line","");try{var t='[data-line-number="'+e+'"',o=document.querySelector(t);o.scrollIntoView(),o.parentNode.classList.add("selected")}catch(e){console.error(e)}}function getFontSize(){var e=16;try{e=Number.parseInt(html.style.fontSize.split("px")[0],10)}catch(e){console.log(e)}return e}function localUpdateFontSize(e){html.style.fontSize=e+"px";var t=document.querySelector("#b77a68a492f343baabea06fad81f651e");t&&(t.innerHTML=e)}function updateFontSize(e){localUpdateFontSize(e),localStorage.setItem(fontSizeLocalStorageKey,e)}function incrementFont(e){var t=getFontSize();t 3 | 8 |
9 | ${e} 10 |
11 | 16 | 21 | 22 | 23 | `}function initTooltip(){tippy(".theme-toggle",{content:"Toggle Theme",delay:500}),tippy(".search-button",{content:"Search",delay:500}),tippy(".font-size",{content:"Change font size",delay:500}),tippy(".codepen-button",{content:"Open code in CodePen",placement:"left"}),tippy(".copy-code",{content:"Copy this code",placement:"left"}),tippy(".font-size",{content:fontSizeTooltip(),trigger:"click",interactive:!0,allowHTML:!0,placement:"left"})}function fixTable(){for(const t of document.querySelectorAll("table")){if(t.classList.contains("hljs-ln"))return;var e=document.createElement("div");e.classList.add("table-div"),t.parentNode.insertBefore(e,t),e.appendChild(t)}}function hideMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.remove("show"),t&&t.setAttribute("data-isopen","false"),o&&o.setAttribute("xlink:href","#menu-icon")}function showMobileMenu(){var e=document.querySelector("#mobile-sidebar"),t=document.querySelector("#mobile-menu"),o=t.querySelector("use");e&&e.classList.add("show"),t&&t.setAttribute("data-isopen","true"),o&&o.setAttribute("xlink:href","#close-icon")}function onMobileMenuClick(){("true"===document.querySelector("#mobile-menu").getAttribute("data-isopen")?hideMobileMenu:showMobileMenu)()}function initMobileMenu(){var e=document.querySelector("#mobile-menu");e&&e.addEventListener("click",onMobileMenuClick)}function addHrefToSidebarTitle(){document.querySelectorAll(".sidebar-title-anchor").forEach(function(e){e.setAttribute("href",baseURL)})}function onDomContentLoaded(){var e=document.querySelectorAll(".theme-toggle");initMobileMenu(),e&&e.forEach(function(e){e.addEventListener("click",toggleTheme)}),hljs.addPlugin({"after:highlightElement":function(e){e.el.parentNode.setAttribute("data-lang","code")}}),hljs.highlightAll(),hljs.initLineNumbersOnLoad({singleLine:!0}),initAccordion(),addAnchor(),processAllPre(),hideTocOnSourcePage(),setTimeout(function(){bringIdToViewOnMount(),isSourcePage()&&highlightAndBringLineIntoView()},1e3),initTooltip(),fixTable(),addHrefToSidebarTitle()}updateTheme(getTheme()),function(){var e=getFontSize(),t=localStorage.getItem(fontSizeLocalStorageKey);t?(t=Number.parseInt(t,10))!==e&&updateFontSize(t):updateFontSize(e)}(),window.addEventListener("DOMContentLoaded",onDomContentLoaded),window.addEventListener("hashchange",e=>{e=new URL(e.newURL);""!==e.hash&&bringIdToViewOnMount(e.hash)}),window.addEventListener("storage",e=>{"undefined"!==e.newValue&&(initTooltip(),e.key===themeLocalStorageKey&&localUpdateTheme(e.newValue),e.key===fontSizeLocalStorageKey&&localUpdateFontSize(e.newValue))}); -------------------------------------------------------------------------------- /src/Rewards/index.js: -------------------------------------------------------------------------------- 1 | import Delegator from '../models/Delegator.js' 2 | import Reward from '../models/Reward.js' 3 | import { setTimestampFormat } from '../utils/index.js' 4 | import schedule from 'node-schedule' 5 | import { 6 | getBlockTime, 7 | fetchLatestEpoch, 8 | getInflationReward, 9 | fetchSolanaPriceAtDate, 10 | } from '../repository/network.repository.js' 11 | import logger from '../logger/logger.js' 12 | 13 | /** 14 | * Important Notes on Delegation and Rewards: 15 | * 16 | * - Activation Epoch: 17 | * The epoch when a delegation is activated. 18 | * 19 | * - Reward Beginning Epoch: 20 | * Rewards start accruing one epoch after the Activation Epoch. 21 | * 22 | * - Withdrawal Implications: 23 | * Upon withdrawal, the 'postBalance' will be less than the balance from the previous reward. 24 | * 25 | */ 26 | 27 | const LAMPORTS_PER_SOL = 1000000000 28 | const START_EPOCH = parseInt(process.env.START_EPOCH) 29 | 30 | /** 31 | * Initializes the function for rewards cron job. 32 | * @returns {Object} - A schedule job to run every day at 1am. 33 | */ 34 | const rewardsCron = async () => { 35 | logger.info('Rewards cron started') 36 | 37 | // Schedule a daily job to run at 1am 38 | return schedule.scheduleJob('0 1 * * *', async () => { 39 | await rewardsJob() 40 | }) 41 | } 42 | 43 | /** 44 | * Job to populate rewards from the start epoch to the latest epoch for all delegators 45 | */ 46 | const rewardsJob = async () => { 47 | try { 48 | const delegators = await Delegator.find({ unstaked: false }) 49 | const delegatorPubKeys = [] 50 | 51 | // populate pubKeys 52 | delegators.forEach((delegator) => { 53 | delegatorPubKeys.push(delegator.delegatorId) 54 | }) 55 | 56 | // finding the latest reward's epoch number 57 | const reward = await Reward.findOne({ 58 | delegatorId: { $in: delegatorPubKeys }, 59 | }).sort({ epochNum: -1, timestamp: -1 }) 60 | 61 | // initial epoch where validator became active 62 | let currentEpoch = START_EPOCH 63 | if (reward !== null) { 64 | currentEpoch = reward.epochNum + 1 65 | } 66 | const latestEpoch = await fetchLatestEpoch() 67 | 68 | for (; currentEpoch <= latestEpoch; currentEpoch++) { 69 | logger.info( 70 | `current epoch: ${currentEpoch}, latest epoch: ${latestEpoch}` 71 | ) 72 | if (currentEpoch === latestEpoch) { 73 | logger.info(`Reached latest Epoch: ${latestEpoch}`) 74 | break 75 | } 76 | const data = await getInflationReward( 77 | delegatorPubKeys, 78 | currentEpoch 79 | ) 80 | if (data.result.length > 0) { 81 | // loop through each delegator's reward 82 | for (let j = 0; j < data.result.length; j++) { 83 | const delegatorReward = data.result[j] 84 | if ( 85 | !isRewardValidForEpoch( 86 | delegatorReward, 87 | currentEpoch, 88 | delegators[j].activationEpoch, 89 | delegators[j].unstakedEpoch 90 | ) 91 | ) 92 | continue 93 | 94 | const blockTime = await getBlockTime( 95 | delegatorReward.effectiveSlot 96 | ) 97 | const timestamp = setTimestampFormat( 98 | new Date(blockTime * 1000) // seconds to milliseconds 99 | ) 100 | const solUsd = await fetchSolanaPriceAtDate(timestamp) 101 | 102 | const pubkey = delegatorPubKeys[j] 103 | 104 | const redundantReward = await Reward.findOne({ 105 | delegatorId: pubkey, 106 | timestamp, 107 | }) 108 | if (redundantReward) { 109 | await Reward.deleteOne({ 110 | delegatorId: pubkey, 111 | timestamp, 112 | }) 113 | } 114 | 115 | // initialization of reward props 116 | let { 117 | reward, 118 | rewardUsd, 119 | totalReward, 120 | totalRewardUsd, 121 | pendingRewards, 122 | pendingRewardsUsd, 123 | postBalance, 124 | postBalanceUsd, 125 | stakedAmount, 126 | stakedAmountUsd, 127 | } = await initializeRewardData( 128 | pubkey, 129 | delegatorReward, 130 | delegators[j].stakedAmount, 131 | solUsd 132 | ) 133 | const { epoch: epochNum } = delegatorReward 134 | 135 | await Reward.create({ 136 | delegatorId: pubkey, 137 | epochNum, 138 | solUsd, 139 | timestamp, 140 | userAction: 'REWARD', 141 | reward, 142 | rewardUsd, 143 | totalReward, 144 | totalRewardUsd, 145 | pendingRewards, 146 | pendingRewardsUsd, 147 | postBalance, 148 | postBalanceUsd, 149 | stakedAmount, 150 | stakedAmountUsd, 151 | }) 152 | } 153 | logger.info(`processed rewards for epoch [${currentEpoch}]`) 154 | } else { 155 | logger.info(`no rewards for epoch [${currentEpoch}]`) 156 | } 157 | } 158 | } catch (e) { 159 | console.log(e) 160 | logger.error(`Rewards cron job failed: ${e.message}`) 161 | const lastReward = await Reward.findOne().sort({ epochNum: -1 }).exec() 162 | const data = await Reward.deleteMany({ epochNum: lastReward.epochNum }) 163 | logger.info(`Deleted ${data.deletedCount} rewards`) 164 | console.info(JSON.stringify(data, null, 2)) 165 | } 166 | } 167 | 168 | /** 169 | * Checks if the reward is valid for the specified epoch based on activation and deactivation epochs. 170 | * @param {RewardOfDelegation|null} delegatorReward - The reward object containing information about the delegator's reward. 171 | * @param {number} epoch - The current epoch number being evaluated. 172 | * @param {number} activationEpoch - The epoch number when the delegator's stake was activated. 173 | * @param {number} deactivationEpoch - The epoch number when the delegator's stake was deactivated. 174 | * @returns {boolean} - Returns true if the reward is valid for the specified epoch, otherwise false. 175 | */ 176 | const isRewardValidForEpoch = ( 177 | delegatorReward, 178 | epoch, 179 | activationEpoch, 180 | deactivationEpoch 181 | ) => { 182 | return ( 183 | delegatorReward !== null && 184 | epoch > activationEpoch && 185 | epoch < deactivationEpoch 186 | ) 187 | } 188 | 189 | /** 190 | * Calculates the USD value of the post balance, reward, and staked amount. 191 | * @param {number} amount - Amount to be converted to USD. 192 | * @param {number} solUsd - Current value of 1 SOL in USD. 193 | * @returns {Object} - An object containing the USD values of the post balance, reward, and staked amount. 194 | */ 195 | const convertSolUsd = (amount, solUsd) => { 196 | return (amount / LAMPORTS_PER_SOL) * solUsd 197 | } 198 | 199 | /** 200 | * Initializes reward data for a delegator based on the reward amount and staked SOL. 201 | * @param {number} pubkey - The public key of the delegator. 202 | * @param {RewardOfDelegation} delegatorReward - The reward object containing the reward amount. 203 | * @param {number} stakedAmount - The amount staked in lamports. 204 | * @param {number} solUsd - Current value of 1 SOL in USD. 205 | * @returns {Object} - An object containing initialized reward data, including total days, total reward, and pending rewards in both SOL and USD. 206 | */ 207 | const initializeRewardData = async ( 208 | pubkey, 209 | delegatorReward, 210 | stakedAmount, 211 | solUsd 212 | ) => { 213 | const { amount: reward, postBalance } = delegatorReward 214 | let totalReward = reward, 215 | pendingRewards = reward 216 | 217 | const previousReward = await Reward.findOne({ 218 | delegatorId: pubkey, 219 | }) 220 | .sort({ epochNum: -1, timestamp: -1 }) 221 | .exec() 222 | if (previousReward) { 223 | totalReward += previousReward.totalReward 224 | pendingRewards += previousReward.pendingRewards 225 | } 226 | 227 | const rewardUsd = convertSolUsd(reward, solUsd) 228 | const totalRewardUsd = convertSolUsd(totalReward, solUsd) 229 | const pendingRewardsUsd = convertSolUsd(pendingRewards, solUsd) 230 | const postBalanceUsd = convertSolUsd(postBalance, solUsd) 231 | const stakedAmountUsd = convertSolUsd(stakedAmount, solUsd) 232 | 233 | return { 234 | reward, 235 | rewardUsd, 236 | totalReward, 237 | totalRewardUsd, 238 | pendingRewards, 239 | pendingRewardsUsd, 240 | postBalance, 241 | postBalanceUsd, 242 | stakedAmount, 243 | stakedAmountUsd, 244 | } 245 | } 246 | 247 | export default rewardsCron 248 | -------------------------------------------------------------------------------- /docs/scripts/third-party/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | Solana Indexer Logo 8 | 9 | 10 |

Solana Indexer

11 | 12 |

13 | A fast, efficient, and open-source Solana blockchain indexer designed to query, analyze, and monitor on-chain data. 14 |
15 |
16 | Report Bug 17 | · 18 | Request Feature 19 |

20 |
21 | 22 | 23 | 24 |
25 | Table of Contents 26 |
    27 |
  1. 28 | About The Project 29 | 32 |
  2. 33 |
  3. 34 | Getting Started 35 | 40 |
  4. 41 |
  5. Usage 42 | 46 |
  6. 47 |
  7. Schema Definition 48 | 53 |
  8. 54 |
  9. How It Works?
  10. 55 |
  11. Contributing
  12. 56 |
  13. License
  14. 57 |
  15. Contact
  16. 58 |
59 |
60 | 61 | 62 | 63 | ## About The Project 64 | 65 | [![Banner](./images/banner.png)](https://github.com/Luganodes/Solana-Indexer) 66 | 67 | Introducing our open-source Solana Indexer - a robust and scalable solution designed to help you effortlessly navigate the ever-growing Solana blockchain. Built with performance and usability in mind, our indexer rapidly fetches, processes, and stores on-chain data, making it instantly queryable for analytics, monitoring, or other applications. Whether you're a developer looking to integrate Solana data into your app, an enterprise in need of a reliable data backend, or simply a blockchain enthusiast wanting insights, our Solana Indexer is the tool you've been waiting for. Get started now and unlock the full potential of blockchain data. 68 | 69 | There are 3 crons setup: 70 | 71 | 1. Delegator Cron (runs every 30 minutes) 72 | 2. Rewards Cron (runs once every day at 1:00 am) 73 | 3. Validator Rewards Cron (runs everyday at 1:00 am) 74 | 75 |

(back to top)

76 | 77 | 78 | 79 | ### Built With 80 | 81 | - [![MongoDB](https://img.shields.io/badge/mongoDB-00684a?style=for-the-badge&logo=mongodb&logoColor=white)](https://www.mongodb.com/) 82 | - [![Node.js](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/en) 83 | - [![Docker](https://img.shields.io/badge/Docker-1D63ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) 84 | 85 |

(back to top)

86 | 87 | 88 | 89 | 90 | ## Getting Started 91 | 92 | To get a local copy up and running follow these simple steps. 93 | 94 | ### Prerequisites 95 | 96 | #### Node.js 97 | 98 | Node.js is required to run the Solana Indexer. Follow the installation guide for your platform [here](https://nodejs.org/en/download/package-manager). 99 | 100 | #### npm 101 | 102 | npm comes bundled with Node.js, but you can ensure you have the latest version by running the following command: 103 | 104 | ```sh 105 | npm install npm@latest -g 106 | ``` 107 | 108 | #### Docker (Optional) 109 | 110 | Docker is optional but recommended for containerized deployment. Follow the installation guide for your platform here. 111 | 112 | - For macOS: [Download Docker Desktop for Mac](https://docs.docker.com/desktop/mac/install/) 113 | - For Windows: [Download Docker Desktop for Windows](https://docs.docker.com/desktop/windows/install/) 114 | - For Linux: [Docker for Linux](https://docs.docker.com/engine/install/) 115 | 116 | ### Installation 117 | 118 | 1. Clone the repo 119 | ```sh 120 | git clone https://github.com/Luganodes/Solana-Indexer.git 121 | ``` 122 | 123 | 2. Change into the directory 124 | ```sh 125 | cd solana-indexer 126 | ``` 127 | 128 | 3. Install NPM packages (not required for Docker Run) 129 | ```sh 130 | npm i 131 | ``` 132 | 133 | 3. **Environment Variables** 134 | ```sh 135 | touch .env 136 | ``` 137 | **For running this project successfully you'll need to create a `.env` file like [`.env.sample`](ttps://github.com/Luganodes/Solana-Indexer/blob/main/.env.sample).** 138 | 139 | ### Environment Variables Description 140 | 141 | #### `DB_URI` 142 | - **Description:** 143 | Holds the connection string URI for the database, specifying the location of the database. 144 | - **Format:** 145 | ``` 146 | mongodb://: 147 | ``` 148 | - **Example:** 149 | ```env 150 | DB_URI=mongodb://localhost:27017 151 | ``` 152 | 153 | #### `DB_NAME` 154 | - **Description:** 155 | Holds the databse name 156 | - **Example:** 157 | ```env 158 | DB_NAME=solana_indexer 159 | ``` 160 | 161 | #### `MONGO_USER` 162 | - **Description:** 163 | Holds the username used to authenticate with the MongoDB database. 164 | - **Example:** 165 | ```env 166 | MONGO_USER=myuser 167 | ``` 168 | 169 | #### `MONGO_PASSWORD` 170 | - **Description:** 171 | Holds the password corresponding to `MONGO_USER`, used to authenticate with the MongoDB database. 172 | - **Example:** 173 | ```env 174 | MONGO_PASSWORD=mypassword 175 | ``` 176 | 177 | #### `VALIDATOR_PUB_KEY` 178 | - **Description:** 179 | Stores the public key of the validator, that's responsible for verifying and validating new transactions and adding them to the blockchain. 180 | - **Example:** 181 | ```env 182 | VALIDATOR_PUB_KEY=6aow5rTURdbhbeMDrFrbP2GR5vZjMEhktEy87iH1VGPs 183 | ``` 184 | 185 | #### `VALIDATOR_ID` 186 | - **Description:** 187 | Holds a unique identifier for the validator, allowing users or systems to reference a specific validator within the network or protocol. 188 | - **Assigned Value:** 189 | ```env 190 | VALIDATOR_ID=LGNS 191 | ``` 192 | 193 | #### `START_EPOCH` 194 | - **Description:** 195 | Represents the epoch at which the validator began validating blocks and distributing delegation rewards. 196 | - **Example:** 197 | ```env 198 | START_EPOCH=357 199 | ``` 200 | 201 | 202 | ### Notes 203 | - Store sensitive information like `MONGO_PASSWORD` securely, and avoid unnecessary exposure. 204 | - Do not hardcode environment variables directly in the code; use configuration files, environment files, or secure vaults. 205 | - Restart your application or system after changing environment variable values for the changes to take effect. 206 | 207 |

(back to top)

208 | 209 | 210 | 211 | 212 | ## Usage 213 | 214 | ### Local Run 215 | ```sh 216 | - npm start (for normal run) 217 | - npm run dev (for development run) 218 | - npm run format (to format your code) 219 | - npm run jsdoc (to generate documentation) 220 | ``` 221 | 222 | ### Docker Run 223 | To start the Docker container 224 | ```sh 225 | docker-compose up --build 226 | ``` 227 | To stop the Docker container 228 | ```sh 229 | docker-compose down 230 | ``` 231 | 232 |

(back to top)

233 | 234 | ## Schema Definition 235 | 236 | ### Delegator Schema 237 | 238 | | Field | Type | Required | Default | Description | 239 | |---------------------|---------|----------|---------|----------------------------------------------------| 240 | | `delegatorId` | String | Yes | - | Public key for the delegator | 241 | | `timestamp` | Number | Yes | - | Unix timestamp for when the delegator was created | 242 | | `unstaked` | Boolean | No | `false` | Indicates if the amount is unstaked | 243 | | `unstakedTimestamp` | Number | No | `-1` | Unix timestamp for when the amount was unstaked | 244 | | `unstakedEpoch` | Number | No | `-1` | Epoch when the unstaking occurred | 245 | | `apr` | Number | No | `0` | APR for the delegator | 246 | | `stakedAmount` | Number | No | `0` | The amount that is staked | 247 | | `activationEpoch` | Number | No | `0` | Epoch when the staking was activated | 248 | 249 | ### Reward Schema 250 | 251 | | Field | Type | Required | Default | Description | 252 | |--------------------|---------|----------|---------|----------------------------------------------------| 253 | | `delegatorId` | String | Yes | - | Public key of the delegator | 254 | | `solUsd` | Number | No | `0` | Current price of Solana in USD | 255 | | `epochNum` | Number | Yes | - | Epoch number for this reward | 256 | | `timestamp` | Number | Yes | - | Timestamp of the reward | 257 | | `postBalance` | Number | Yes | - | Post-reward balance | 258 | | `postBalanceUsd` | Number | No | `0` | Post-reward balance in USD | 259 | | `userAction` | String | No | - | Type of user action (`WITHDRAW`, `REWARD`) | 260 | | `reward` | Number | Yes | - | Reward amount | 261 | | `rewardUsd` | Number | Yes | - | Reward amount in USD | 262 | | `totalReward` | Number | Yes | - | Total rewards till the current epoch | 263 | | `totalRewardUsd` | Number | No | `0` | Total rewards in USD till the current epoch | 264 | | `pendingRewards` | Number | Yes | - | Pending rewards | 265 | | `pendingRewardsUsd`| Number | No | `0` | Pending rewards in USD | 266 | | `stakedAmount` | Number | Yes | - | The initial staked amount | 267 | | `stakedAmountUsd` | Number | Yes | - | The initial staked amount in USD | 268 | 269 | ### Transaction Schema 270 | 271 | | Field | Type | Required | Default | Description | 272 | |--------------------|---------|----------|---------|----------------------------------------------------| 273 | | `delegatorId` | String | Yes | - | Public key of the delegator | 274 | | `timestamp` | Number | Yes | - | Timestamp of the transaction | 275 | | `type` | String | Yes | - | Transaction type (e.g., `STAKE`, `UNSTAKE`) | 276 | | `amount` | Number | Yes | - | Amount involved in the transaction | 277 | | `solUsd` | Number | Yes | - | Price of Solana in USD at the time of transaction | 278 | | `transactionCount` | Number | Yes | - | Transaction count for the delegator | 279 | | `transactionHash` | String | Yes | - | Hash of the transaction | 280 | | `fee` | Number | Yes | - | Transaction fee | 281 | 282 | ## How it works? 283 | 284 | The rewards are calulated from the start epoch, as we loop through each entry in our MongoDB to fetch the rewards of delegators for the specific epoch 285 | 286 | ### [Constants and Functions](https://luganodes.github.io/Solana-Indexer/global.html) 287 | 288 | 289 | ## Contributing 290 | 291 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 292 | 293 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 294 | Don't forget to give the project a star! Thanks again! 295 | 296 | 1. Fork the Project 297 | 2. Create your Feature Branch (`git checkout -b feat/AmazingFeature`) 298 | 3. Make some amazing changes. 299 | 4. `git add .` 300 | 4. Commit your Changes (`git commit -m ": "`) 301 | 4. Push to the Branch (`git push origin feat/AmazingFeature`) 302 | 5. Open a Pull Request 303 | 304 | To start contributing, check out [`CONTRIBUTING.md`](https://github.com/Luganodes/Solana-Indexer/blob/main/CONTRIBUTING.md) . New contributors are always welcome to support this project. 305 | 306 |

(back to top)

307 | 308 | 309 | ## License 310 | 311 | Distributed under the MIT License. See [`LICENSE`](https://github.com/Luganodes/Solana-Indexer/blob/main/LICENSE) for more information. 312 | 313 |

(back to top)

-------------------------------------------------------------------------------- /docs/scripts/third-party/hljs-line-num-original.js: -------------------------------------------------------------------------------- 1 | // jshint multistr:true 2 | 3 | (function (w, d) { 4 | 'use strict'; 5 | 6 | var TABLE_NAME = 'hljs-ln', 7 | LINE_NAME = 'hljs-ln-line', 8 | CODE_BLOCK_NAME = 'hljs-ln-code', 9 | NUMBERS_BLOCK_NAME = 'hljs-ln-numbers', 10 | NUMBER_LINE_NAME = 'hljs-ln-n', 11 | DATA_ATTR_NAME = 'data-line-number', 12 | BREAK_LINE_REGEXP = /\r\n|\r|\n/g; 13 | 14 | if (w.hljs) { 15 | w.hljs.initLineNumbersOnLoad = initLineNumbersOnLoad; 16 | w.hljs.lineNumbersBlock = lineNumbersBlock; 17 | w.hljs.lineNumbersValue = lineNumbersValue; 18 | 19 | addStyles(); 20 | } else { 21 | w.console.error('highlight.js not detected!'); 22 | } 23 | 24 | function isHljsLnCodeDescendant(domElt) { 25 | var curElt = domElt; 26 | while (curElt) { 27 | if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) { 28 | return true; 29 | } 30 | curElt = curElt.parentNode; 31 | } 32 | return false; 33 | } 34 | 35 | function getHljsLnTable(hljsLnDomElt) { 36 | var curElt = hljsLnDomElt; 37 | while (curElt.nodeName !== 'TABLE') { 38 | curElt = curElt.parentNode; 39 | } 40 | return curElt; 41 | } 42 | 43 | // Function to workaround a copy issue with Microsoft Edge. 44 | // Due to hljs-ln wrapping the lines of code inside a element, 45 | // itself wrapped inside a
 element, window.getSelection().toString()
 46 |     // does not contain any line breaks. So we need to get them back using the
 47 |     // rendered code in the DOM as reference.
 48 |     function edgeGetSelectedCodeLines(selection) {
 49 |         // current selected text without line breaks
 50 |         var selectionText = selection.toString();
 51 | 
 52 |         // get the 
' + 220 | '' + 222 | '' + 225 | '', 226 | [ 227 | LINE_NAME, 228 | NUMBERS_BLOCK_NAME, 229 | NUMBER_LINE_NAME, 230 | DATA_ATTR_NAME, 231 | CODE_BLOCK_NAME, 232 | i + options.startFrom, 233 | lines[i].length > 0 ? lines[i] : ' ' 234 | ]); 235 | } 236 | 237 | return format('
element wrapping the first line of selected code 53 | var tdAnchor = selection.anchorNode; 54 | while (tdAnchor.nodeName !== 'TD') { 55 | tdAnchor = tdAnchor.parentNode; 56 | } 57 | 58 | // get the element wrapping the last line of selected code 59 | var tdFocus = selection.focusNode; 60 | while (tdFocus.nodeName !== 'TD') { 61 | tdFocus = tdFocus.parentNode; 62 | } 63 | 64 | // extract line numbers 65 | var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber); 66 | var lastLineNumber = parseInt(tdFocus.dataset.lineNumber); 67 | 68 | // multi-lines copied case 69 | if (firstLineNumber != lastLineNumber) { 70 | 71 | var firstLineText = tdAnchor.textContent; 72 | var lastLineText = tdFocus.textContent; 73 | 74 | // if the selection was made backward, swap values 75 | if (firstLineNumber > lastLineNumber) { 76 | var tmp = firstLineNumber; 77 | firstLineNumber = lastLineNumber; 78 | lastLineNumber = tmp; 79 | tmp = firstLineText; 80 | firstLineText = lastLineText; 81 | lastLineText = tmp; 82 | } 83 | 84 | // discard not copied characters in first line 85 | while (selectionText.indexOf(firstLineText) !== 0) { 86 | firstLineText = firstLineText.slice(1); 87 | } 88 | 89 | // discard not copied characters in last line 90 | while (selectionText.lastIndexOf(lastLineText) === -1) { 91 | lastLineText = lastLineText.slice(0, -1); 92 | } 93 | 94 | // reconstruct and return the real copied text 95 | var selectedText = firstLineText; 96 | var hljsLnTable = getHljsLnTable(tdAnchor); 97 | for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) { 98 | var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]); 99 | var codeLineElt = hljsLnTable.querySelector(codeLineSel); 100 | selectedText += '\n' + codeLineElt.textContent; 101 | } 102 | selectedText += '\n' + lastLineText; 103 | return selectedText; 104 | // single copied line case 105 | } else { 106 | return selectionText; 107 | } 108 | } 109 | 110 | // ensure consistent code copy/paste behavior across all browsers 111 | // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51) 112 | document.addEventListener('copy', function(e) { 113 | // get current selection 114 | var selection = window.getSelection(); 115 | // override behavior when one wants to copy line of codes 116 | if (isHljsLnCodeDescendant(selection.anchorNode)) { 117 | var selectionText; 118 | // workaround an issue with Microsoft Edge as copied line breaks 119 | // are removed otherwise from the selection string 120 | if (window.navigator.userAgent.indexOf('Edge') !== -1) { 121 | selectionText = edgeGetSelectedCodeLines(selection); 122 | } else { 123 | // other browsers can directly use the selection string 124 | selectionText = selection.toString(); 125 | } 126 | e.clipboardData.setData( 127 | 'text/plain', 128 | selectionText 129 | .replace(/(^\t)/gm, '') 130 | ); 131 | e.preventDefault(); 132 | } 133 | }); 134 | 135 | function addStyles () { 136 | var css = d.createElement('style'); 137 | css.type = 'text/css'; 138 | css.innerHTML = format( 139 | '.{0}{border-collapse:collapse}' + 140 | '.{0} td{padding:0}' + 141 | '.{1}:before{content:attr({2})}', 142 | [ 143 | TABLE_NAME, 144 | NUMBER_LINE_NAME, 145 | DATA_ATTR_NAME 146 | ]); 147 | d.getElementsByTagName('head')[0].appendChild(css); 148 | } 149 | 150 | function initLineNumbersOnLoad (options) { 151 | if (d.readyState === 'interactive' || d.readyState === 'complete') { 152 | documentReady(options); 153 | } else { 154 | w.addEventListener('DOMContentLoaded', function () { 155 | documentReady(options); 156 | }); 157 | } 158 | } 159 | 160 | function documentReady (options) { 161 | try { 162 | var blocks = d.querySelectorAll('code.hljs,code.nohighlight'); 163 | 164 | for (var i in blocks) { 165 | if (blocks.hasOwnProperty(i)) { 166 | if (!isPluginDisabledForBlock(blocks[i])) { 167 | lineNumbersBlock(blocks[i], options); 168 | } 169 | } 170 | } 171 | } catch (e) { 172 | w.console.error('LineNumbers error: ', e); 173 | } 174 | } 175 | 176 | function isPluginDisabledForBlock(element) { 177 | return element.classList.contains('nohljsln'); 178 | } 179 | 180 | function lineNumbersBlock (element, options) { 181 | if (typeof element !== 'object') return; 182 | 183 | async(function () { 184 | element.innerHTML = lineNumbersInternal(element, options); 185 | }); 186 | } 187 | 188 | function lineNumbersValue (value, options) { 189 | if (typeof value !== 'string') return; 190 | 191 | var element = document.createElement('code') 192 | element.innerHTML = value 193 | 194 | return lineNumbersInternal(element, options); 195 | } 196 | 197 | function lineNumbersInternal (element, options) { 198 | 199 | var internalOptions = mapOptions(element, options); 200 | 201 | duplicateMultilineNodes(element); 202 | 203 | return addLineNumbersBlockFor(element.innerHTML, internalOptions); 204 | } 205 | 206 | function addLineNumbersBlockFor (inputHtml, options) { 207 | var lines = getLines(inputHtml); 208 | 209 | // if last line contains only carriage return remove it 210 | if (lines[lines.length-1].trim() === '') { 211 | lines.pop(); 212 | } 213 | 214 | if (lines.length > 1 || options.singleLine) { 215 | var html = ''; 216 | 217 | for (var i = 0, l = lines.length; i < l; i++) { 218 | html += format( 219 | '
' + 221 | '' + 223 | '{6}' + 224 | '
{1}
', [ TABLE_NAME, html ]); 238 | } 239 | 240 | return inputHtml; 241 | } 242 | 243 | /** 244 | * @param {HTMLElement} element Code block. 245 | * @param {Object} options External API options. 246 | * @returns {Object} Internal API options. 247 | */ 248 | function mapOptions (element, options) { 249 | options = options || {}; 250 | return { 251 | singleLine: getSingleLineOption(options), 252 | startFrom: getStartFromOption(element, options) 253 | }; 254 | } 255 | 256 | function getSingleLineOption (options) { 257 | var defaultValue = false; 258 | if (!!options.singleLine) { 259 | return options.singleLine; 260 | } 261 | return defaultValue; 262 | } 263 | 264 | function getStartFromOption (element, options) { 265 | var defaultValue = 1; 266 | var startFrom = defaultValue; 267 | 268 | if (isFinite(options.startFrom)) { 269 | startFrom = options.startFrom; 270 | } 271 | 272 | // can be overridden because local option is priority 273 | var value = getAttribute(element, 'data-ln-start-from'); 274 | if (value !== null) { 275 | startFrom = toNumber(value, defaultValue); 276 | } 277 | 278 | return startFrom; 279 | } 280 | 281 | /** 282 | * Recursive method for fix multi-line elements implementation in highlight.js 283 | * Doing deep passage on child nodes. 284 | * @param {HTMLElement} element 285 | */ 286 | function duplicateMultilineNodes (element) { 287 | var nodes = element.childNodes; 288 | for (var node in nodes) { 289 | if (nodes.hasOwnProperty(node)) { 290 | var child = nodes[node]; 291 | if (getLinesCount(child.textContent) > 0) { 292 | if (child.childNodes.length > 0) { 293 | duplicateMultilineNodes(child); 294 | } else { 295 | duplicateMultilineNode(child.parentNode); 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | /** 303 | * Method for fix multi-line elements implementation in highlight.js 304 | * @param {HTMLElement} element 305 | */ 306 | function duplicateMultilineNode (element) { 307 | var className = element.className; 308 | 309 | if ( ! /hljs-/.test(className)) return; 310 | 311 | var lines = getLines(element.innerHTML); 312 | 313 | for (var i = 0, result = ''; i < lines.length; i++) { 314 | var lineText = lines[i].length > 0 ? lines[i] : ' '; 315 | result += format('{1}\n', [ className, lineText ]); 316 | } 317 | 318 | element.innerHTML = result.trim(); 319 | } 320 | 321 | function getLines (text) { 322 | if (text.length === 0) return []; 323 | return text.split(BREAK_LINE_REGEXP); 324 | } 325 | 326 | function getLinesCount (text) { 327 | return (text.trim().match(BREAK_LINE_REGEXP) || []).length; 328 | } 329 | 330 | /// 331 | /// HELPERS 332 | /// 333 | 334 | function async (func) { 335 | w.setTimeout(func, 0); 336 | } 337 | 338 | /** 339 | * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript} 340 | * @param {string} format 341 | * @param {array} args 342 | */ 343 | function format (format, args) { 344 | return format.replace(/\{(\d+)\}/g, function(m, n){ 345 | return args[n] !== undefined ? args[n] : m; 346 | }); 347 | } 348 | 349 | /** 350 | * @param {HTMLElement} element Code block. 351 | * @param {String} attrName Attribute name. 352 | * @returns {String} Attribute value or empty. 353 | */ 354 | function getAttribute (element, attrName) { 355 | return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null; 356 | } 357 | 358 | /** 359 | * @param {String} str Source string. 360 | * @param {Number} fallback Fallback value. 361 | * @returns Parsed number or fallback value. 362 | */ 363 | function toNumber (str, fallback) { 364 | if (!str) return fallback; 365 | var number = Number(str); 366 | return isFinite(number) ? number : fallback; 367 | } 368 | 369 | }(window, document)); 370 | -------------------------------------------------------------------------------- /docs/config_db.js.html: -------------------------------------------------------------------------------- 1 | Source: config/db.js
On this page

config_db.js

import mongoose from 'mongoose'
 4 | import dotenv from 'dotenv'
 5 | import logger from '../logger/logger.js'
 6 | 
 7 | // Load environment variables from .env file
 8 | dotenv.config()
 9 | 
10 | /**
11 |  * Establishes a connection to the MongoDB using the provided configurations.
12 |  * Connection configurations are sourced from environment variables.
13 |  *
14 |  * @returns {Promise} A promise object that resolves once the connection is established.
15 |  * @throws {Error} - If the connection couldn't be established.
16 |  */
17 | export const connectDB = async () => {
18 |     try {
19 |         mongoose.set('strictQuery', false)
20 |         const connection = await mongoose.connect(process.env.DB_URI, {
21 |             useNewUrlParser: true,
22 |             useUnifiedTopology: true,
23 |             user: process.env.MONGO_USER,
24 |             pass: process.env.MONGO_PASSWORD,
25 |             dbName: process.env.DB_NAME,
26 |         })
27 |         logger.info('Successfully connected to the database')
28 |         return connection
29 |     } catch (e) {
30 |         logger.error(`Error connecting to the database: ${e.message}`)
31 |         throw e
32 |     }
33 | }
34 | 
-------------------------------------------------------------------------------- /docs/index.js.html: -------------------------------------------------------------------------------- 1 | Source: index.js
On this page

index.js

import { connectDB } from './config/db.js'
 4 | import delegatorCron from './Delegator/delegatorCron.js'
 5 | import rewardsCron from './Rewards/index.js'
 6 | import validatorRewardsCron from './Rewards/validator.js'
 7 | import logger from './logger/logger.js'
 8 | 
 9 | /**
10 |  * Initialize and run essential services.
11 |  */
12 | const cronHandles = []
13 | const main = async () => {
14 |     try {
15 |         // Connect to the database
16 |         await connectDB()
17 | 
18 |         // Start necessary cron jobs
19 |         cronHandles.push(await delegatorCron())
20 |         cronHandles.push(await validatorRewardsCron())
21 |         cronHandles.push(await rewardsCron())
22 |     } catch (e) {
23 |         logger.error(`Error in main initialization: ${e}`)
24 |     }
25 | }
26 | 
27 | main()
28 | 
29 | process.on('SIGINT', async () => {
30 |     try {
31 |         for (const handle of cronHandles) {
32 |             await handle.cancel()
33 |             logger.info('Cron job successfully cancelled.')
34 |         }
35 |         // eslint-disable-next-line no-process-exit
36 |         process.exit(0)
37 |     } catch (e) {
38 |         logger.error(`Error during graceful shutdown: ${e}`)
39 |         // eslint-disable-next-line no-process-exit
40 |         process.exit(1) // Exit with an error code
41 |     }
42 | })
43 | 
-------------------------------------------------------------------------------- /docs/Rewards_popSolUsd.js.html: -------------------------------------------------------------------------------- 1 | Source: Rewards/popSolUsd.js
On this page

Rewards_popSolUsd.js

import Reward from '../models/Reward.js'
 4 | import logger from '../logger/logger.js'
 5 | import { fetchSolanaPriceAtDate } from '../repository/network.repository.js'
 6 | 
 7 | /**
 8 |  * Populate the solUsd field in all Reward model instances
 9 |  */
10 | export const populateSolUsd = async () => {
11 |     try {
12 |         // Fetch all rewards from the database
13 |         const rewards = await Reward.find()
14 | 
15 |         // Loop through each reward to populate solUsd
16 |         for (let i = 0; i < rewards.length; i++) {
17 |             // Introduce a delay of 1 second to avoid rate limiting
18 |             await new Promise((resolve) => setTimeout(resolve, 1000))
19 | 
20 |             // Fetch the current SOL to USD conversion rate
21 |             const solUsd = await fetchSolanaPriceAtDate(rewards[i].timestamp)
22 | 
23 |             // Update the solUsd field for the current reward
24 |             rewards[i].solUsd = solUsd
25 |             await rewards[i].save()
26 | 
27 |             logger.info(
28 |                 `Successfully updated solUsd for reward ID: ${rewards[i]._id}`
29 |             )
30 |         }
31 | 
32 |         logger.info('SOL-USD population task is DONE.')
33 |     } catch (e) {
34 |         logger.error(`An error occurred while populating SOL-USD: ${e.message}`)
35 |     }
36 | }
37 | 
-------------------------------------------------------------------------------- /docs/Delegator_utils.js.html: -------------------------------------------------------------------------------- 1 | Source: Delegator/utils.js
On this page

Delegator_utils.js

import Transaction from '../models/Transaction.js'
 4 | import logger from '../logger/logger.js'
 5 | import {
 6 |     fetchSolanaPriceAtDate,
 7 |     getTransaction,
 8 |     getSignaturesForAddress,
 9 | } from '../repository/network.repository.js'
10 | 
11 | const VALIDATOR_PUB_KEY = process.env.VALIDATOR_PUB_KEY
12 | const LAMPORTS_PER_SOL = 1000000000
13 | 
14 | /**
15 |  * Create and persist a delegate transaction
16 |  * @param {string} address - The delegator's public key
17 |  * @param {number} stakedAmount - The amount of SOL the user staked
18 |  */
19 | export const createDelegateTransaction = async (address, stakedAmount) => {
20 |     try {
21 |         const data = await getSignaturesForAddress(address)
22 |         const transactionSignatures = data.result
23 | 
24 |         for (const signature of transactionSignatures) {
25 |             const transactionData = await getTransaction(signature.signature)
26 |             if (!transactionData.result) continue
27 |             const result =
28 |                 transactionData.result.transaction.message.accountKeys.filter(
29 |                     (key) => key === VALIDATOR_PUB_KEY
30 |                 )
31 | 
32 |             if (result.length > 0) {
33 |                 const solUsd = await fetchSolanaPriceAtDate(
34 |                     transactionData.result.blockTime * 1000
35 |                 )
36 |                 const transactionFee =
37 |                     transactionData.result.meta.fee / LAMPORTS_PER_SOL
38 | 
39 |                 await Transaction.create({
40 |                     delegatorId: address,
41 |                     timestamp: transactionData.result.blockTime * 1000,
42 |                     type: 'STAKE',
43 |                     amount: stakedAmount,
44 |                     solUsd,
45 |                     fee: transactionFee,
46 |                     transactionHash: signature.signature,
47 |                     transactionCount: transactionSignatures.length,
48 |                 })
49 |                 logger.info(`Transaction created [${address}]`)
50 |             }
51 |         }
52 |     } catch (e) {
53 |         logger.error(
54 |             `Error creating delegate transaction [${address}]: ${e.message}`
55 |         )
56 |     }
57 | }
58 | 
--------------------------------------------------------------------------------