Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions balance-reporter/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[**.{ts,tsx,json,js,jsx}]
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
8 changes: 8 additions & 0 deletions balance-reporter/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# IDEs
.idea
.vscode

lib
*.log
node_modules/
build/
4 changes: 4 additions & 0 deletions balance-reporter/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 100,
"trailingComma": "all"
}
23 changes: 23 additions & 0 deletions balance-reporter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "balance-reporter",
"version": "1.0.0",
"main": "src/index.ts",
"license": "ISC",
"scripts": {
"build": "tsc --incremental -p .",
"lint": "tslint -p && prettier '**/*.ts' -l",
"fmt": "tslint -p . --fix && prettier '**/*ts' --write"
},
"dependencies": {
"@sendgrid/mail": "^6.4.0",
"codechain-primitives": "^1.0.1",
"codechain-rpc": "^0.1.6"
},
"devDependencies": {
"prettier": "^1.18.2",
"ts-node": "^8.3.0",
"tslint": "^5.18.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.5.2"
}
}
67 changes: 67 additions & 0 deletions balance-reporter/src/Email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as sendgrid from "@sendgrid/mail";

export interface Email {
sendError(msg: string): void;
sendWarning(text: string): void;
sendInfo(title: string, msg: string): void;
}

class NullEmail implements Email {
public sendError(_msg: string): void {}
public sendWarning(_text: string): void {}
public sendInfo(_title: string, _msg: string): void {}
}

const from = "[email protected]";

function createTitle(params: { title: string; tag: string; level: string }): string {
const { title, tag, level } = params;
return `[${level}]${tag} ${title} - ${new Date().toISOString()}`;
}

class Sendgrid implements Email {
private readonly tag: string;
private readonly to: string;

public constructor(params: { tag: string; sendgridApiKey: string; to: string }) {
const { tag, sendgridApiKey, to } = params;
this.tag = tag;
sendgrid.setApiKey(sendgridApiKey);
this.to = to;
}

public sendError(text: string): void {
const subject = createTitle({ tag: this.tag, title: "has a problem.", level: "error" });
this.send(subject, text);
}

public sendWarning(text: string): void {
const subject = createTitle({ tag: this.tag, title: "finds a problem.", level: "warn" });
this.send(subject, text);
}

public sendInfo(title: string, text: string): void {
const subject = createTitle({ tag: this.tag, title, level: "info" });
this.send(subject, text);
}

private send(subject: string, value: string): void {
sendgrid
.send({ subject, from, to: this.to, content: [{ type: "text/html", value }] })
.catch(console.error);
}
}

export function createEmail(params: { tag: string; to?: string; sendgridApiKey?: string }): Email {
const { tag, to, sendgridApiKey } = params;
if (sendgridApiKey != null) {
if (to == null) {
throw Error("The email destination is not set");
}
console.log("Sendgrid key is set");
return new Sendgrid({ tag, sendgridApiKey, to });
} else {
console.log("Donot use sendgrid");
return new NullEmail();
}
}
107 changes: 107 additions & 0 deletions balance-reporter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { U64 } from "codechain-primitives";
import Rpc from "codechain-rpc";
import { createEmail } from "./Email";

type balanceInfo = {
address: string;
balance: U64;
};

function getConfig(field: string, defaultVal?: string): string {
const c = process.env[field];
if (c == null) {
if (defaultVal == null) {
throw new Error(`${field} is not specified`);
}
return defaultVal;
}
return c;
}

const rpcUrl = getConfig("RPC_URL");
const sendgridApiKey = getConfig("SENDGRID_API_KEY");
const sendgridTo = getConfig("SENDGRID_TO");
const minAllowedBalance = new U64(getConfig("MIN_BALANCE"));
const maxAllowedBalance = new U64(getConfig("MAX_BALANCE"));

const rpc = new Rpc(rpcUrl);
const email = createEmail({
tag: `[mainnet][balance-reporter]`,
sendgridApiKey,
to: sendgridTo,
});

const targetAddresses: string[] = process.argv.slice(2);

function outOfRange(target: U64, min: U64, max: U64) {
return target.lt(min) || target.gt(max);
}

async function getCCCBalances(
addresses: string[],
blockNumber: number,
): Promise<Array<balanceInfo>> {
const balances = await Promise.all(
addresses.map(address => rpc.chain.getBalance({ address, blockNumber })),
);
const result = [];
for (let i = 0; i < addresses.length; i++) {
const inst = balances[i];
if (inst != null) {
result.push({
address: addresses[i],
balance: new U64(inst),
});
}
}
return result;
}

function toReportMessage(prefix: string, infos: balanceInfo[], blockNumber: number): string {
const infosString = infos
.map(info => `<li> ${info.address} : ${info.balance} </li>`)
.join("<br />\r\n");
return `<p>${prefix}, block number: ${blockNumber}</p>
<ul>${infosString}</ul>
`;
}

async function main() {
let lastReportDate = new Date().getUTCDate();
let lastCheckedBlockNumber = await rpc.chain.getBestBlockNumber();
let lastCheckedBalances: balanceInfo[] = [];

setInterval(() => {
const nowDate = new Date().getUTCDate();
if (nowDate !== lastReportDate && lastCheckedBalances.length > 0) {
const message = toReportMessage(
"Daily report",
lastCheckedBalances,
lastCheckedBlockNumber,
);
email.sendInfo("daily report", message);
}
lastReportDate = nowDate;
}, 60 * 1000); // 1 minute interval

setInterval(async () => {
lastCheckedBlockNumber = await rpc.chain.getBestBlockNumber()!;
lastCheckedBalances = await getCCCBalances(targetAddresses, lastCheckedBlockNumber);
const reportingTarget = Array.from(lastCheckedBalances).filter(info =>
outOfRange(info.balance, minAllowedBalance, maxAllowedBalance),
);
if (reportingTarget.length > 0) {
const message = toReportMessage(
"Balances are out of range",
reportingTarget,
lastCheckedBlockNumber,
);
email.sendWarning(message);
}
}, 60 * 1000 * 5); // 5 miniute interval
}

main().catch(error => {
console.log({ error });
email.sendError(error.message);
});
14 changes: 14 additions & 0 deletions balance-reporter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2018",
"types": ["node"],
"module": "commonjs",
"declaration": true,
"outDir": "build",
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["**/*.ts"]
}
28 changes: 28 additions & 0 deletions balance-reporter/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"extends": ["tslint:recommended", "tslint-config-prettier"],
"rules": {
"strict-boolean-expressions": true,
"interface-name": false,
"interface-over-type-literal": false,
"no-console": false,
"no-empty": [true, "allow-empty-functions"],
"object-literal-sort-keys": false,
"no-var-requires": false,
"array-type": false,
"variable-name": [
true,
"check-format",
"allow-leading-underscore",
"allow-pascal-case"
],
"max-classes-per-file": true,
"no-bitwise": false
},
"jsRules": {
"no-console": false,
"object-literal-sort-keys": false
},
"linterOptions": {
"exclude": ["node_modules/**/*.ts", "/build/*"]
}
}
Loading