feat(wallet-monitor): list every wallet with full address in each alert
When an alert fires, the Slack message now includes the full roster — low (🔴) and healthy (🟢) wallets alike — each with its complete, copy-pasteable address (backticked), instead of only the low wallets with a truncated address. The posting trigger and dedup/reminder logic are unchanged: still silent when all wallets are healthy; the buyer (normally healthy) now appears for context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,9 @@ One-shot wallet-balance Slack alerter for the `vector-marketplace` mainnet box.
|
|||||||
Checks every wallet in `wallets.json` against a threshold (default **10 APEX**),
|
Checks every wallet in `wallets.json` against a threshold (default **10 APEX**),
|
||||||
posts a combined Slack message when any wallet is below it, re-reminds every
|
posts a combined Slack message when any wallet is below it, re-reminds every
|
||||||
`reminder_hours` while a wallet stays low, and posts a recovery message when it
|
`reminder_hours` while a wallet stays low, and posts a recovery message when it
|
||||||
climbs back above. Balances are read from Ogmios via
|
climbs back above. Each alert lists **every** monitored wallet — low (🔴) and
|
||||||
|
healthy (🟢) — with its full, copy-pasteable address; it stays fully silent
|
||||||
|
when all wallets are healthy. Balances are read from Ogmios via
|
||||||
`ReadOnlyOgmiosProvider` (`@marketplace/shared/chain`) — the same client behind
|
`ReadOnlyOgmiosProvider` (`@marketplace/shared/chain`) — the same client behind
|
||||||
the buyer's `/v1/wallet/balance` and `buyer/scripts/monitor-wallets.ts`.
|
the buyer's `/v1/wallet/balance` and `buyer/scripts/monitor-wallets.ts`.
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ function fmtAp3x(lovelace: bigint): string {
|
|||||||
return (Number(lovelace) / LOVELACE_PER_AP3X).toFixed(2);
|
return (Number(lovelace) / LOVELACE_PER_AP3X).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortAddr(a: string): string {
|
|
||||||
return a.length <= 18 ? a : `${a.slice(0, 10)}…${a.slice(-6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadState(path: string): StateFile {
|
function loadState(path: string): StateFile {
|
||||||
try {
|
try {
|
||||||
const obj: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
const obj: unknown = JSON.parse(readFileSync(path, "utf-8"));
|
||||||
@@ -123,8 +119,13 @@ async function main(): Promise<void> {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nextState: StateFile = { ...prevState };
|
const nextState: StateFile = { ...prevState };
|
||||||
|
|
||||||
const lowLines: string[] = [];
|
// Classify each successfully-queried wallet, advancing dedup state. The
|
||||||
const recoveredLines: string[] = [];
|
// decision to POST is driven only by transitions (a newly-low wallet, a low
|
||||||
|
// wallet past its reminder window, or a recovery). But when we DO post, the
|
||||||
|
// message lists EVERY monitored wallet for full context (built below).
|
||||||
|
let anyDueLow = false;
|
||||||
|
let anyRecovered = false;
|
||||||
|
const recoveredNames = new Set<string>();
|
||||||
|
|
||||||
for (const c of checked) {
|
for (const c of checked) {
|
||||||
// Skip wallets we couldn't query — leave their state untouched so we don't
|
// Skip wallets we couldn't query — leave their state untouched so we don't
|
||||||
@@ -138,9 +139,7 @@ async function main(): Promise<void> {
|
|||||||
const wasLow = prev?.status === "low";
|
const wasLow = prev?.status === "low";
|
||||||
const due = !wasLow || now - (prev?.lastAlertMs ?? 0) >= reminderMs;
|
const due = !wasLow || now - (prev?.lastAlertMs ?? 0) >= reminderMs;
|
||||||
if (due) {
|
if (due) {
|
||||||
lowLines.push(
|
anyDueLow = true;
|
||||||
`🔴 ${c.name} ${fmtAp3x(c.lovelace)} / ${fmtAp3x(c.minLovelace)} APEX ${shortAddr(c.address)}`,
|
|
||||||
);
|
|
||||||
nextState[c.name] = { status: "low", lastAlertMs: now };
|
nextState[c.name] = { status: "low", lastAlertMs: now };
|
||||||
} else {
|
} else {
|
||||||
// Still low but the reminder window hasn't elapsed — keep prev as-is.
|
// Still low but the reminder window hasn't elapsed — keep prev as-is.
|
||||||
@@ -148,20 +147,42 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (prev?.status === "low") {
|
if (prev?.status === "low") {
|
||||||
recoveredLines.push(`✅ ${c.name} ${fmtAp3x(c.lovelace)} APEX (recovered)`);
|
anyRecovered = true;
|
||||||
|
recoveredNames.add(c.name);
|
||||||
}
|
}
|
||||||
nextState[c.name] = { status: "ok", lastAlertMs: prev?.lastAlertMs ?? 0 };
|
nextState[c.name] = { status: "ok", lastAlertMs: prev?.lastAlertMs ?? 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const needPost = lowLines.length > 0 || recoveredLines.length > 0;
|
const needPost = anyDueLow || anyRecovered;
|
||||||
|
|
||||||
if (needPost) {
|
if (needPost) {
|
||||||
const title =
|
const anyLow = checked.some(
|
||||||
lowLines.length > 0
|
(c) => c.error === null && c.lovelace !== null && c.lovelace < c.minLovelace,
|
||||||
? "⚠️ *Wallet balance alert* — vector-marketplace"
|
);
|
||||||
: "✅ *Wallet balance recovered* — vector-marketplace";
|
const title = anyLow
|
||||||
const text = [title, ...lowLines, ...recoveredLines].join("\n");
|
? "⚠️ *Wallet balance alert* — vector-marketplace"
|
||||||
|
: "✅ *Wallet balance recovered* — vector-marketplace";
|
||||||
|
|
||||||
|
// Full roster — every monitored wallet (low first, then healthy, then any
|
||||||
|
// that failed to query), each with its FULL address backticked so it's
|
||||||
|
// copy-pasteable in Slack.
|
||||||
|
const low: string[] = [];
|
||||||
|
const ok: string[] = [];
|
||||||
|
const failed: string[] = [];
|
||||||
|
for (const c of checked) {
|
||||||
|
if (c.error !== null || c.lovelace === null) {
|
||||||
|
failed.push(`⚠️ ${c.name} query failed\n \`${c.address}\``);
|
||||||
|
} else if (c.lovelace < c.minLovelace) {
|
||||||
|
low.push(
|
||||||
|
`🔴 ${c.name} ${fmtAp3x(c.lovelace)} / ${fmtAp3x(c.minLovelace)} APEX\n \`${c.address}\``,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const tag = recoveredNames.has(c.name) ? " (recovered)" : "";
|
||||||
|
ok.push(`🟢 ${c.name} ${fmtAp3x(c.lovelace)} APEX${tag}\n \`${c.address}\``);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const text = [title, ...low, ...ok, ...failed].join("\n");
|
||||||
|
|
||||||
const r = await postSlack(cfg.slackWebhookUrl, text);
|
const r = await postSlack(cfg.slackWebhookUrl, text);
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -170,7 +191,9 @@ async function main(): Promise<void> {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[wallet-monitor] posted: ${lowLines.length} low, ${recoveredLines.length} recovered`);
|
console.log(
|
||||||
|
`[wallet-monitor] posted roster: ${low.length} low, ${ok.length} ok, ${failed.length} failed`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log("[wallet-monitor] all wallets healthy — no Slack post");
|
console.log("[wallet-monitor] all wallets healthy — no Slack post");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user