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
85 changes: 85 additions & 0 deletions .github/workflows/deploy-monitoring-randomness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Deploy Randomness Monitor

on:
workflow_dispatch:

env:
REGISTRY: ghcr.io
ORGANIZATION: happychaindevs
PM2_PROCESS_NAME: randomness-monitor

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4.2.0

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Build code
run: |
make randomness-monitor.build

- name: Copy files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_2_HOST }}
username: ${{ secrets.SERVER_2_USERNAME }}
key: ${{ secrets.SERVER_2_KEY }}
port: ${{ secrets.SERVER_2_PORT }}
source: "apps/randomness-monitor/build/*,node_modules/better-sqlite3,node_modules/bindings,node_modules/file-uri-to-path"
target: /home/deployer/randomness-monitor
strip_components: 1
rm: true

- name: Deploy randomness to server
uses: appleboy/ssh-action@v1.1.0
with:
host: ${{ secrets.SERVER_2_HOST }}
username: ${{ secrets.SERVER_2_USERNAME }}
key: ${{ secrets.SERVER_2_KEY }}
port: ${{ secrets.SERVER_2_PORT }}
# We copy better-sqlite3, bindings, and file-uri-to-path separately because the bundled file does not include
# better-sqlite3 and its dependencies
script: |
chmod -R o+rX /home/deployer/randomness-monitor
mv /home/deployer/randomness-monitor /tmp
sudo -u randomness-monitor bash -c '
rm -rf /home/randomness-monitor/build
rm -rf /home/randomness-monitor/node_modules
cp -r /tmp/randomness-monitor/randomness-monitor/build /home/randomness-monitor/build

mkdir -p /home/randomness-monitor/node_modules
cp -r /tmp/randomness-monitor/better-sqlite3 /home/randomness-monitor/node_modules/better-sqlite3
cp -r /tmp/randomness-monitor/bindings /home/randomness-monitor/node_modules/bindings
cp -r /tmp/randomness-monitor/file-uri-to-path /home/randomness-monitor/node_modules/file-uri-to-path

cd /home/randomness-monitor

rm -f .env

cat > .env <<-EOF
${{vars.RANDOMNESS_MONITOR_ENV}}
EOF

npm rebuild

pm2 delete ${{ env.PM2_PROCESS_NAME }}

source .env && node ./build/migrate.es.js

source .env && pm2 start ./build/index.es.js \
--name ${{ env.PM2_PROCESS_NAME }}
pm2 save
'
rm -rf /tmp/randomness-monitor
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ randomness.build: setup.ts shared.build
cd apps/randomness && make build
.PHONY: randomness.build

randomness-monitor.build: setup.ts shared.build
cd apps/randomness-monitor && make build
.PHONY: randomness-monitor.build

txm.build: setup.ts shared.build
cd packages/txm && make build
.PHONY: txm.build
Expand Down
4 changes: 0 additions & 4 deletions apps/docs/src/pages/txm/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,4 @@ const hookHandler: TxmHookHandler = (tx: Transaction) => {
}

txm.addHook(TxmHookType.OnStatusChange, hookHandler)
<<<<<<< HEAD
```
=======
```
>>>>>>> e24f8b15 (fix: docs)
4 changes: 4 additions & 0 deletions apps/randomness-monitor/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
MONITORING_DB_PATH=
RPC_URL=wss://rpc.testnet.happy.tech/ws
CHAIN_ID=216
RANDOM_CONTRACT_ADDRESS=0xd7dafcdC292906540Cc3357E9fD913390256b978
14 changes: 14 additions & 0 deletions apps/randomness-monitor/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
SRC_ROOT_DIR := src

include ../../makefiles/lib.mk
include ../../makefiles/formatting.mk
include ../../makefiles/bundling.mk
include ../../makefiles/help.mk

start: ## Starts the randomness service
node --env-file=.env dist/index.es.js
.PHONY: start

migrate: ## Runs pending migrations
node --env-file=.env dist/migrate.es.js
PHONY: migrate
9 changes: 9 additions & 0 deletions apps/randomness-monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Randomness Monitor

This application monitors the randomness service by directly inspecting the blockchain to verify whether randomness data is included in each block.

The results are stored in a SQLite database and visualized via Grafana on the following dashboard:

🔗 [View Dashboard](https://grafana.happy.tech/goto/BMpaDZbNg?orgId=1)

In addition to visualization, the collected data is used to trigger alerts when the randomness service's performance drops below 95% block coverage.
10 changes: 10 additions & 0 deletions apps/randomness-monitor/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "@happy.tech/happybuild"

export default defineConfig({
exports: [".", "./migrate"],
bunConfig: {
minify: false,
target: "node",
external: ["better-sqlite3"],
},
})
25 changes: 25 additions & 0 deletions apps/randomness-monitor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@happy.tech/randomness-monitor",
"private": true,
"version": "0.1.0",
"type": "module",
"main": "./dist/index.es.js",
"module": "./dist/index.es.js",
"exports": {
".": "./dist/index.es.js",
"./migrate": "./dist/migrate.es.js"
},
"dependencies": {
"@happy.tech/common": "workspace:0.1.0",
"zod": "^3.23.8",
"better-sqlite3": "^11.7.0",
"kysely": "^0.27.5",
"viem": "^2.21.53",
"neverthrow": "^8.1.0",
"@happy.tech/contracts": "workspace:0.1.0"
},
"devDependencies": {
"@happy.tech/happybuild": "workspace:0.1.1",
"typescript": "^5.6.2"
}
}
26 changes: 26 additions & 0 deletions apps/randomness-monitor/src/Check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export enum CheckResult {
Success = "success",
Failure = "failure",
}

export class Check {
readonly blockNumber: bigint
readonly blockTimestamp: bigint
readonly result: CheckResult
readonly errorDescription: string | undefined
readonly value: string | undefined

constructor(
blockNumber: bigint,
blockTimestamp: bigint,
result: CheckResult,
errorDescription?: string,
value?: string,
) {
this.blockNumber = blockNumber
this.blockTimestamp = blockTimestamp
this.result = result
this.errorDescription = errorDescription
this.value = value
}
}
38 changes: 38 additions & 0 deletions apps/randomness-monitor/src/CheckRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { unknownToError } from "@happy.tech/common"
import { type Result, ResultAsync, err, ok } from "neverthrow"
import { db } from "./db/driver"
import { checkEntityToRow, checkRowToEntity } from "./db/types"
import type { Check } from "./Check"

const PRUNE_BLOCK_TIMESTAMP_THRESHOLD_SECONDS = 60 * 60 * 24 * 30 // 30 days

export class CheckRepository {
async saveCheck(check: Check): Promise<Result<void, Error>> {
const row = checkEntityToRow(check)

return await ResultAsync.fromPromise(db.insertInto("checks").values(row).execute(), unknownToError).map(
() => undefined,
)
}

async findLatestCheck(): Promise<Result<Check | undefined, Error>> {
const result = await ResultAsync.fromPromise(
db.selectFrom("checks").selectAll().orderBy("blockNumber", "desc").limit(1).executeTakeFirst(),
unknownToError,
)
if (result.isErr()) {
return err(result.error)
}
return ok(result.value ? checkRowToEntity(result.value) : undefined)
}

async pruneChecks(blockTimestamp: bigint): Promise<Result<void, Error>> {
return await ResultAsync.fromPromise(
db
.deleteFrom("checks")
.where("blockTimestamp", "<", Number(blockTimestamp) - PRUNE_BLOCK_TIMESTAMP_THRESHOLD_SECONDS)
.execute(),
unknownToError,
).map(() => undefined)
}
}
2 changes: 2 additions & 0 deletions apps/randomness-monitor/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Quantity of digits in the max uint256 value
export const DIGITS_MAX_UINT256 = 78
16 changes: 16 additions & 0 deletions apps/randomness-monitor/src/db/driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SQLite from "better-sqlite3"
import { Kysely, SqliteDialect } from "kysely"
import { env } from "../env.js"
import type { Database } from "./types.js"

const sqlite3 = new SQLite(env.MONITORING_DB_PATH ?? ":memory:")

sqlite3.defaultSafeIntegers()

const dialect = new SqliteDialect({
database: sqlite3,
})

export const db = new Kysely<Database>({
dialect,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Kysely } from "kysely"
import type { Database } from "../types"

export async function up(db: Kysely<Database>) {
await db.schema
.createTable("checks")
.addColumn("blockNumber", "integer", (col) => col.notNull())
.addColumn("blockTimestamp", "integer", (col) => col.notNull())
.addColumn("result", "text", (col) => col.notNull())
.addColumn("errorDescription", "text")
.addColumn("value", "text")
.execute()
}

export const migration20250403123000 = { up }
5 changes: 5 additions & 0 deletions apps/randomness-monitor/src/db/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { migration20250403123000 } from "./Migration20250403123000"

export const migrations = {
"20250403123000": migration20250403123000,
}
33 changes: 33 additions & 0 deletions apps/randomness-monitor/src/db/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Check, type CheckResult } from "../Check"

export interface CheckRow {
blockNumber: number
blockTimestamp: number
result: CheckResult
errorDescription: string | undefined
value: string | undefined
}

export function checkRowToEntity(row: CheckRow): Check {
return new Check(
BigInt(row.blockNumber),
BigInt(row.blockTimestamp),
row.result,
row.errorDescription,
row.value,
)
}

export function checkEntityToRow(entity: Check): CheckRow {
return {
blockNumber: Number(entity.blockNumber),
blockTimestamp: Number(entity.blockTimestamp),
result: entity.result,
errorDescription: entity.errorDescription,
value: entity.value,
}
}

export interface Database {
checks: CheckRow
}
21 changes: 21 additions & 0 deletions apps/randomness-monitor/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from "zod"
import { hexSchema } from "./utils/schemas"

const envSchema = z.object({
MONITORING_DB_PATH: z.string().trim(),
RPC_URL: z.string().trim(),
CHAIN_ID: z
.string()
.trim()
.transform((value) => Number(value)),
RANDOM_CONTRACT_ADDRESS: hexSchema,
})

const parsedEnv = envSchema.safeParse(process.env)

if (!parsedEnv.success) {
console.log(parsedEnv.error.issues)
throw new Error("There is an error with the server environment variables")
}

export const env = parsedEnv.data
Loading