Skip to content

Commit e3c2f51

Browse files
Agilulfo1820claude
andcommitted
fix: cleaner terminal UI, Docker signal handling, and build fixes
- Simplify display to minimal colored text with dim labels and dividers - Fix VOT3 label (was incorrectly showing B3TR) - Add double Ctrl+C to force quit (first graceful, second immediate) - Add tini to Dockerfile for proper signal forwarding - Default Docker network to mainnet - Remove "type": "module" from package.json (CJS output) - Restrict tsconfig types to node only (fix minimatch type error) - Bump version to 0.0.1, add publish:npm script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6c94bb1 commit e3c2f51

File tree

5 files changed

+99
-69
lines changed

5 files changed

+99
-69
lines changed

Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ COPY src ./src
77
RUN NODE_OPTIONS=--max-old-space-size=4096 npx tsc
88

99
FROM node:20-alpine
10+
RUN apk add --no-cache tini
1011
WORKDIR /app
1112
COPY package.json ./
1213
RUN npm install --production
1314
COPY --from=builder /app/dist ./dist
14-
ENV RELAYER_NETWORK=testnet-staging
15+
ENV RELAYER_NETWORK=mainnet
16+
ENTRYPOINT ["/sbin/tini", "--"]
1517
CMD ["node", "dist/index.js"]

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"name": "@vebetterdao/relayer-node",
3-
"version": "1.0.0",
3+
"version": "0.0.1",
44
"description": "Standalone relayer node for VeBetterDAO auto-voting and reward claiming",
55
"main": "dist/index.js",
6+
"license": "MIT",
67
"bin": {
78
"vbd-relayer": "dist/index.js"
89
},
@@ -13,7 +14,8 @@
1314
"build": "tsc",
1415
"start": "node dist/index.js",
1516
"dev": "ts-node src/index.ts",
16-
"prepublishOnly": "npm run build"
17+
"prepublishOnly": "npm run build",
18+
"publish:npm": "npm publish --access public"
1719
},
1820
"engines": {
1921
"node": ">=20"

src/display.ts

Lines changed: 79 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import chalk from "chalk"
22
import { RelayerSummary, CycleResult } from "./types"
33

4-
const W = 66
5-
64
function formatB3TR(wei: bigint): string {
75
const whole = wei / 10n ** 18n
86
const frac = (wei % 10n ** 18n) / 10n ** 16n
@@ -24,86 +22,100 @@ function pct(num: bigint, den: bigint): string {
2422
return ((Number(num) / Number(den)) * 100).toFixed(2) + "%"
2523
}
2624

27-
function pad(left: string, right: string, width: number = W - 4): string {
28-
const gap = width - left.length - right.length
25+
function stripAnsi(str: string): number {
26+
return str.replace(/\x1b\[[0-9;]*m/g, "").length
27+
}
28+
29+
function pad(left: string, right: string, width: number = 62): string {
30+
const gap = width - stripAnsi(left) - stripAnsi(right)
2931
return left + " ".repeat(Math.max(1, gap)) + right
3032
}
3133

32-
function line(content: string): string {
33-
const inner = content.padEnd(W - 4)
34-
return `║ ${inner} ║`
34+
function heading(text: string): string {
35+
return chalk.bold.cyan(text)
3536
}
3637

37-
function sep(): string {
38-
return "╠" + "═".repeat(W - 2) + "╣"
38+
function label(text: string): string {
39+
return chalk.dim(text)
3940
}
4041

4142
export function renderSummary(s: RelayerSummary): string {
4243
const out: string[] = []
43-
const top = "╔" + "═".repeat(W - 2) + "╗"
44-
const bot = "╚" + "═".repeat(W - 2) + "╝"
45-
46-
const title = "VeBetterDAO Relayer Node"
47-
const titlePad = Math.floor((W - 4 - title.length) / 2)
48-
49-
out.push(top)
50-
out.push(line(" ".repeat(titlePad) + chalk.bold.cyan(title)))
51-
out.push(sep())
52-
53-
const status = s.isRegistered ? chalk.green("✓ Registered") : chalk.red("✗ Not registered")
54-
out.push(line(pad("Network " + chalk.white(s.network), "Block " + chalk.white(s.latestBlock.toLocaleString()))))
55-
out.push(line(pad("Node " + chalk.gray(new URL(s.nodeUrl).hostname), "")))
56-
out.push(line(pad("Address " + chalk.yellow(shortAddr(s.relayerAddress)), status)))
57-
58-
out.push(sep())
59-
60-
const roundStatus = s.isRoundActive ? chalk.green("● Active") : chalk.gray("○ Ended")
61-
out.push(line(chalk.bold(`ROUND #${s.currentRoundId}`) + " " + roundStatus))
62-
out.push(line(pad(`Snapshot ${s.roundSnapshot}`, `Deadline ${s.roundDeadline}`)))
63-
out.push(line(pad(`Auto-voters ${chalk.white(s.autoVotingUsers.toString())}`, `Relayers ${chalk.white(s.registeredRelayers.length.toString())}`)))
64-
out.push(line(pad(`Voters ${chalk.white(s.totalVoters.toString())}`, `Total VOT3 ${chalk.white(formatVOT3(s.totalVotes))}`)))
65-
66-
out.push(sep())
6744

45+
out.push("")
46+
out.push(heading(" VeBetterDAO Relayer Node"))
47+
out.push(chalk.dim(" " + "─".repeat(60)))
48+
out.push("")
49+
50+
// Node info
51+
const regStatus = s.isRegistered ? chalk.green("Registered") : chalk.red("Not registered")
52+
out.push(" " + pad(label("Network") + " " + chalk.white.bold(s.network), label("Block") + " " + chalk.white(s.latestBlock.toLocaleString())))
53+
out.push(" " + pad(label("Node") + " " + chalk.gray(new URL(s.nodeUrl).hostname), ""))
54+
out.push(" " + pad(label("Address") + " " + chalk.yellow(shortAddr(s.relayerAddress)), regStatus))
55+
56+
out.push("")
57+
out.push(chalk.dim(" " + "─".repeat(60)))
58+
out.push("")
59+
60+
// Round info
61+
const roundStatus = s.isRoundActive ? chalk.green("Active") : chalk.dim("Ended")
62+
out.push(" " + heading(`Round #${s.currentRoundId}`) + " " + roundStatus)
63+
out.push(" " + pad(label("Snapshot") + " " + chalk.white(s.roundSnapshot.toString()), label("Deadline") + " " + chalk.white(s.roundDeadline.toString())))
64+
out.push(" " + pad(label("Auto-voters") + " " + chalk.white.bold(s.autoVotingUsers.toString()), label("Relayers") + " " + chalk.white.bold(s.registeredRelayers.length.toString())))
65+
out.push(" " + pad(label("Voters") + " " + chalk.white(s.totalVoters.toString()), label("Total") + " " + chalk.cyan(formatVOT3(s.totalVotes))))
66+
67+
out.push("")
68+
out.push(chalk.dim(" " + "─".repeat(60)))
69+
out.push("")
70+
71+
// Fee config
6872
const feeStr = s.feeDenominator > 0n ? pct(s.feePercentage, s.feeDenominator) : "—"
69-
out.push(line(pad(`Vote Wt ${s.voteWeight}`, `Claim Wt ${s.claimWeight}`)))
70-
out.push(line(pad(`Fee ${feeStr}`, `Cap ${formatB3TR(s.feeCap)}`)))
71-
out.push(line(pad(`Early Access ${s.earlyAccessBlocks} blocks`, "")))
73+
out.push(" " + pad(label("Vote Weight") + " " + chalk.white.bold(s.voteWeight.toString()), label("Claim Weight") + " " + chalk.white.bold(s.claimWeight.toString())))
74+
out.push(" " + pad(label("Fee") + " " + chalk.yellow(feeStr), label("Cap") + " " + chalk.yellow(formatB3TR(s.feeCap))))
75+
out.push(" " + pad(label("Early Access") + " " + chalk.white(s.earlyAccessBlocks.toString()) + chalk.dim(" blocks"), ""))
7276

73-
out.push(sep())
74-
out.push(line(chalk.bold("THIS ROUND")))
77+
out.push("")
78+
out.push(chalk.dim(" " + "─".repeat(60)))
79+
out.push("")
7580

81+
// This round stats
82+
out.push(" " + heading("This Round"))
7683
const completionPct = s.currentTotalWeighted > 0n
7784
? pct(s.currentCompletedWeighted, s.currentTotalWeighted)
7885
: "—"
79-
out.push(line(pad(
80-
`Completion ${completionPct}`,
81-
`Missed ${s.currentMissedUsers}`,
82-
)))
83-
out.push(line(pad(
84-
`Pool ${chalk.green(formatB3TR(s.currentTotalRewards))}`,
85-
`Your share ${chalk.green(formatB3TR(s.currentRelayerClaimable))}`,
86-
)))
87-
out.push(line(pad(
88-
`Actions ${s.currentRelayerActions} (wt: ${s.currentRelayerWeighted})`,
89-
`Total acts ${s.currentTotalActions}`,
90-
)))
91-
86+
const completionColor = s.currentTotalWeighted > 0n && s.currentCompletedWeighted >= s.currentTotalWeighted
87+
? chalk.green : chalk.yellow
88+
out.push(" " + pad(
89+
label("Completion") + " " + completionColor(completionPct),
90+
label("Missed") + " " + (s.currentMissedUsers > 0n ? chalk.red(s.currentMissedUsers.toString()) : chalk.green(s.currentMissedUsers.toString())),
91+
))
92+
out.push(" " + pad(
93+
label("Pool") + " " + chalk.green(formatB3TR(s.currentTotalRewards)),
94+
label("Your share") + " " + chalk.greenBright.bold(formatB3TR(s.currentRelayerClaimable)),
95+
))
96+
out.push(" " + pad(
97+
label("Actions") + " " + chalk.white(s.currentRelayerActions.toString()) + chalk.dim(" (wt: ") + chalk.white(s.currentRelayerWeighted.toString()) + chalk.dim(")"),
98+
label("Total") + " " + chalk.white(s.currentTotalActions.toString()),
99+
))
100+
101+
// Previous round
92102
if (s.previousRoundId > 0) {
93-
out.push(line(""))
94-
out.push(line(chalk.bold(`PREVIOUS ROUND #${s.previousRoundId}`)))
95-
const claimStatus = s.previousRewardClaimable ? chalk.green("✓ Claimable") : chalk.gray("✗ Not yet")
96-
out.push(line(pad(
97-
`Pool ${chalk.green(formatB3TR(s.previousTotalRewards))}`,
98-
`Your share ${chalk.green(formatB3TR(s.previousRelayerClaimable))}`,
99-
)))
100-
out.push(line(pad(
101-
`Actions ${s.previousRelayerActions}`,
103+
out.push("")
104+
out.push(chalk.dim(" " + "─".repeat(60)))
105+
out.push("")
106+
const claimStatus = s.previousRewardClaimable ? chalk.green("Claimable") : chalk.dim("Not yet")
107+
out.push(" " + heading(`Previous Round #${s.previousRoundId}`))
108+
out.push(" " + pad(
109+
label("Pool") + " " + chalk.green(formatB3TR(s.previousTotalRewards)),
110+
label("Your share") + " " + chalk.greenBright.bold(formatB3TR(s.previousRelayerClaimable)),
111+
))
112+
out.push(" " + pad(
113+
label("Actions") + " " + chalk.white(s.previousRelayerActions.toString()),
102114
claimStatus,
103-
)))
115+
))
104116
}
105117

106-
out.push(bot)
118+
out.push("")
107119
return out.join("\n")
108120
}
109121

@@ -113,11 +125,14 @@ export function renderCycleResult(r: CycleResult): string[] {
113125
const dryTag = r.dryRun ? chalk.yellow(" (DRY RUN)") : ""
114126

115127
if (r.totalUsers === 0) {
116-
lines.push(`${label} round #${r.roundId}: no users${dryTag}`)
128+
lines.push(`${label} round #${r.roundId}: ${chalk.dim("no users")}${dryTag}`)
117129
return lines
118130
}
119131

120-
lines.push(`${label} round #${r.roundId}: ${chalk.green(r.successful.toString())}/${r.totalUsers} successful${dryTag}`)
132+
const ratio = r.successful === r.totalUsers
133+
? chalk.green.bold(`${r.successful}/${r.totalUsers}`)
134+
: chalk.yellow(`${r.successful}/${r.totalUsers}`)
135+
lines.push(`${label} round #${r.roundId}: ${ratio} successful${dryTag}`)
121136

122137
if (r.failed.length > 0)
123138
lines.push(chalk.red(` ${r.failed.length} failed`) + chalk.gray(` (${r.failed.slice(0, 3).map((f) => shortAddr(f.user)).join(", ")}${r.failed.length > 3 ? "..." : ""})`))

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,18 @@ async function main() {
7474
const thor = ThorClient.at(config.nodeUrl, { isPollingEnabled: false })
7575

7676
let running = true
77-
process.on("SIGINT", () => { running = false })
78-
process.on("SIGTERM", () => { running = false })
77+
let forceExit = false
78+
const shutdown = () => {
79+
if (forceExit) {
80+
log(chalk.red("Force exit."))
81+
process.exit(1)
82+
}
83+
forceExit = true
84+
running = false
85+
log(chalk.yellow("Shutting down after current operation... (press Ctrl+C again to force quit)"))
86+
}
87+
process.on("SIGINT", shutdown)
88+
process.on("SIGTERM", shutdown)
7989

8090
while (running) {
8191
try {

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"forceConsistentCasingInFileNames": true,
77
"strict": true,
88
"skipLibCheck": true,
9+
"types": ["node"],
910
"resolveJsonModule": true,
1011
"declaration": true,
1112
"outDir": "dist",

0 commit comments

Comments
 (0)