hood-alerts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +369 -0
- package/dist/bot/index.cjs +1008 -0
- package/dist/bot/index.cjs.map +1 -0
- package/dist/bot/index.d.cts +241 -0
- package/dist/bot/index.d.ts +241 -0
- package/dist/bot/index.js +30 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/chunk-32GS6XVE.js +732 -0
- package/dist/chunk-32GS6XVE.js.map +1 -0
- package/dist/chunk-3LHCQA4Z.js +76 -0
- package/dist/chunk-3LHCQA4Z.js.map +1 -0
- package/dist/chunk-3NHLSKZE.js +1 -0
- package/dist/chunk-3NHLSKZE.js.map +1 -0
- package/dist/chunk-6JQK5G3Z.js +72 -0
- package/dist/chunk-6JQK5G3Z.js.map +1 -0
- package/dist/chunk-CBNZ6AZT.js +598 -0
- package/dist/chunk-CBNZ6AZT.js.map +1 -0
- package/dist/chunk-CIURPPOW.js +1 -0
- package/dist/chunk-CIURPPOW.js.map +1 -0
- package/dist/chunk-IUOEGBF6.js +49 -0
- package/dist/chunk-IUOEGBF6.js.map +1 -0
- package/dist/chunk-NBMINJ5E.js +230 -0
- package/dist/chunk-NBMINJ5E.js.map +1 -0
- package/dist/chunk-O2RY2SWN.js +1 -0
- package/dist/chunk-O2RY2SWN.js.map +1 -0
- package/dist/chunk-PLMVKTQM.js +1 -0
- package/dist/chunk-PLMVKTQM.js.map +1 -0
- package/dist/chunk-RSTLW7XH.js +130 -0
- package/dist/chunk-RSTLW7XH.js.map +1 -0
- package/dist/chunk-SOBZ2FPP.js +355 -0
- package/dist/chunk-SOBZ2FPP.js.map +1 -0
- package/dist/chunk-VC5MKEY2.js +170 -0
- package/dist/chunk-VC5MKEY2.js.map +1 -0
- package/dist/chunk-W2GUAWUR.js +147 -0
- package/dist/chunk-W2GUAWUR.js.map +1 -0
- package/dist/chunk-Y2M3USWZ.js +355 -0
- package/dist/chunk-Y2M3USWZ.js.map +1 -0
- package/dist/chunk-Z4DJPESQ.js +1216 -0
- package/dist/chunk-Z4DJPESQ.js.map +1 -0
- package/dist/chunk-ZDM5VNWB.js +1 -0
- package/dist/chunk-ZDM5VNWB.js.map +1 -0
- package/dist/events/index.cjs +820 -0
- package/dist/events/index.cjs.map +1 -0
- package/dist/events/index.d.cts +58 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.js +69 -0
- package/dist/events/index.js.map +1 -0
- package/dist/format-6RG4OcyE.d.ts +57 -0
- package/dist/format-B7d40f65.d.cts +57 -0
- package/dist/index.cjs +4122 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-3phvV_fl.d.cts +185 -0
- package/dist/logger-CYza_0eU.d.ts +185 -0
- package/dist/logs-D7sYBvSy.d.cts +66 -0
- package/dist/logs-D7sYBvSy.d.ts +66 -0
- package/dist/notifiers/index.cjs +620 -0
- package/dist/notifiers/index.cjs.map +1 -0
- package/dist/notifiers/index.d.cts +186 -0
- package/dist/notifiers/index.d.ts +186 -0
- package/dist/notifiers/index.js +70 -0
- package/dist/notifiers/index.js.map +1 -0
- package/dist/pricing-BumgLpvB.d.cts +79 -0
- package/dist/pricing-BumgLpvB.d.ts +79 -0
- package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
- package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
- package/dist/registry-D3We8Lg6.d.ts +50 -0
- package/dist/registry-DePA3IyS.d.cts +50 -0
- package/dist/rules/index.cjs +614 -0
- package/dist/rules/index.cjs.map +1 -0
- package/dist/rules/index.d.cts +166 -0
- package/dist/rules/index.d.ts +166 -0
- package/dist/rules/index.js +60 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/schema-DuDMPqpq.d.ts +183 -0
- package/dist/schema-NnIdKFl2.d.cts +183 -0
- package/dist/service/index.cjs +3774 -0
- package/dist/service/index.cjs.map +1 -0
- package/dist/service/index.d.cts +262 -0
- package/dist/service/index.d.ts +262 -0
- package/dist/service/index.js +45 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/main.cjs +3759 -0
- package/dist/service/main.cjs.map +1 -0
- package/dist/service/main.d.cts +1 -0
- package/dist/service/main.d.ts +1 -0
- package/dist/service/main.js +60 -0
- package/dist/service/main.js.map +1 -0
- package/dist/sources-CAaQR6-N.d.ts +159 -0
- package/dist/sources-CpDOUlzx.d.cts +159 -0
- package/dist/telegram-CpodYJrP.d.cts +64 -0
- package/dist/telegram-pvphmBwn.d.ts +64 -0
- package/dist/tiers/index.cjs +471 -0
- package/dist/tiers/index.cjs.map +1 -0
- package/dist/tiers/index.d.cts +231 -0
- package/dist/tiers/index.d.ts +231 -0
- package/dist/tiers/index.js +34 -0
- package/dist/tiers/index.js.map +1 -0
- package/dist/types-C0ETog04.d.ts +109 -0
- package/dist/types-CcoKpStA.d.cts +109 -0
- package/dist/types-DFPpTMWo.d.cts +186 -0
- package/dist/types-DFPpTMWo.d.ts +186 -0
- package/package.json +114 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/notifiers/index.ts
|
|
21
|
+
var notifiers_exports = {};
|
|
22
|
+
__export(notifiers_exports, {
|
|
23
|
+
EMBED_COLORS: () => EMBED_COLORS,
|
|
24
|
+
createCaptureNotifier: () => createCaptureNotifier,
|
|
25
|
+
createDiscordBotNotifier: () => createDiscordBotNotifier,
|
|
26
|
+
createDiscordRateLimiter: () => createDiscordRateLimiter,
|
|
27
|
+
createDiscordWebhookNotifier: () => createDiscordWebhookNotifier,
|
|
28
|
+
createTelegramClient: () => createTelegramClient,
|
|
29
|
+
createTelegramNotifier: () => createTelegramNotifier,
|
|
30
|
+
delivered: () => delivered,
|
|
31
|
+
detailRows: () => detailRows,
|
|
32
|
+
displayLabel: () => displayLabel,
|
|
33
|
+
escapeDiscordMarkdown: () => escapeDiscordMarkdown,
|
|
34
|
+
escapeHtml: () => escapeHtml,
|
|
35
|
+
escapeHtmlAttribute: () => escapeHtmlAttribute,
|
|
36
|
+
escapeMarkdownV2: () => escapeMarkdownV2,
|
|
37
|
+
escapeMarkdownV2Code: () => escapeMarkdownV2Code,
|
|
38
|
+
escapeMarkdownV2Url: () => escapeMarkdownV2Url,
|
|
39
|
+
failed: () => failed,
|
|
40
|
+
formatAmount: () => formatAmount,
|
|
41
|
+
formatEth: () => formatEth,
|
|
42
|
+
formatUsd: () => formatUsd,
|
|
43
|
+
headline: () => headline,
|
|
44
|
+
htmlLink: () => htmlLink,
|
|
45
|
+
markdownV2Link: () => markdownV2Link,
|
|
46
|
+
numericHeader: () => numericHeader,
|
|
47
|
+
pick: () => pick,
|
|
48
|
+
postJson: () => postJson,
|
|
49
|
+
renderAlert: () => renderAlert,
|
|
50
|
+
resolveFetch: () => resolveFetch,
|
|
51
|
+
shortAddress: () => shortAddress,
|
|
52
|
+
sleep: () => sleep,
|
|
53
|
+
truncate: () => truncate
|
|
54
|
+
});
|
|
55
|
+
module.exports = __toCommonJS(notifiers_exports);
|
|
56
|
+
|
|
57
|
+
// src/notifiers/escape.ts
|
|
58
|
+
var MARKDOWN_V2_RESERVED = "_*[]()~`>#+-=|{}.!";
|
|
59
|
+
function escapeMarkdownV2(text) {
|
|
60
|
+
let out = "";
|
|
61
|
+
for (const char of text) {
|
|
62
|
+
if (MARKDOWN_V2_RESERVED.includes(char)) out += "\\";
|
|
63
|
+
out += char;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
function escapeMarkdownV2Url(url) {
|
|
68
|
+
return url.replace(/[\\)]/g, (char) => `\\${char}`);
|
|
69
|
+
}
|
|
70
|
+
function escapeMarkdownV2Code(text) {
|
|
71
|
+
return text.replace(/[\\`]/g, (char) => `\\${char}`);
|
|
72
|
+
}
|
|
73
|
+
function markdownV2Link(label, url) {
|
|
74
|
+
return `[${escapeMarkdownV2(label)}](${escapeMarkdownV2Url(url)})`;
|
|
75
|
+
}
|
|
76
|
+
function escapeHtml(text) {
|
|
77
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
78
|
+
}
|
|
79
|
+
function escapeHtmlAttribute(value) {
|
|
80
|
+
return escapeHtml(value).replace(/"/g, """);
|
|
81
|
+
}
|
|
82
|
+
function htmlLink(label, url) {
|
|
83
|
+
return `<a href="${escapeHtmlAttribute(url)}">${escapeHtml(label)}</a>`;
|
|
84
|
+
}
|
|
85
|
+
var DISCORD_RESERVED = /[\\*_~`|>[\]()]/g;
|
|
86
|
+
function escapeDiscordMarkdown(text) {
|
|
87
|
+
return text.replace(DISCORD_RESERVED, (char) => `\\${char}`);
|
|
88
|
+
}
|
|
89
|
+
function truncate(text, max) {
|
|
90
|
+
if (max <= 0) return "";
|
|
91
|
+
const chars = [...text];
|
|
92
|
+
if (chars.length <= max) return text;
|
|
93
|
+
if (max === 1) return "\u2026";
|
|
94
|
+
return `${chars.slice(0, max - 1).join("")}\u2026`;
|
|
95
|
+
}
|
|
96
|
+
function displayLabel(symbol, address) {
|
|
97
|
+
const trimmed = symbol?.trim();
|
|
98
|
+
if (trimmed) return truncate(trimmed, 40);
|
|
99
|
+
return shortAddress(address);
|
|
100
|
+
}
|
|
101
|
+
function shortAddress(address) {
|
|
102
|
+
if (address.length <= 12) return address;
|
|
103
|
+
return `${address.slice(0, 6)}\u2026${address.slice(-4)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/notifiers/format.ts
|
|
107
|
+
var import_viem = require("viem");
|
|
108
|
+
var EMBED_COLORS = {
|
|
109
|
+
launch: 2278750,
|
|
110
|
+
curve_trade: 3718648,
|
|
111
|
+
graduation: 11032055,
|
|
112
|
+
whale_trade: 16096779
|
|
113
|
+
};
|
|
114
|
+
var KIND_EMOJI = {
|
|
115
|
+
launch: "\u{1F680}",
|
|
116
|
+
curve_trade: "\u{1F4C8}",
|
|
117
|
+
graduation: "\u{1F393}",
|
|
118
|
+
whale_trade: "\u{1F40B}"
|
|
119
|
+
};
|
|
120
|
+
var LAUNCHPAD_NAMES = { noxa: "NOXA", odyssey: "The Odyssey" };
|
|
121
|
+
var usdFormatter = new Intl.NumberFormat("en-US", {
|
|
122
|
+
style: "currency",
|
|
123
|
+
currency: "USD",
|
|
124
|
+
maximumFractionDigits: 2
|
|
125
|
+
});
|
|
126
|
+
function formatUsd(value) {
|
|
127
|
+
if (value === null || !Number.isFinite(value)) return "unknown";
|
|
128
|
+
const abs = Math.abs(value);
|
|
129
|
+
if (abs >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
|
130
|
+
if (abs >= 1e4) return `$${(value / 1e3).toFixed(1)}K`;
|
|
131
|
+
return usdFormatter.format(value);
|
|
132
|
+
}
|
|
133
|
+
function formatAmount(value) {
|
|
134
|
+
const abs = Math.abs(value);
|
|
135
|
+
if (abs === 0) return "0";
|
|
136
|
+
if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
|
137
|
+
if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
|
138
|
+
if (abs >= 1e3) return `${(value / 1e3).toFixed(2)}K`;
|
|
139
|
+
if (abs >= 1) return value.toFixed(4).replace(/\.?0+$/, "");
|
|
140
|
+
return value.toPrecision(4);
|
|
141
|
+
}
|
|
142
|
+
function formatEth(wei) {
|
|
143
|
+
return `${formatAmount(Number((0, import_viem.formatUnits)(wei < 0n ? -wei : wei, 18)))} ETH`;
|
|
144
|
+
}
|
|
145
|
+
function headline(event) {
|
|
146
|
+
const label = displayLabel(event.symbol, event.token);
|
|
147
|
+
const pad = LAUNCHPAD_NAMES[event.launchpad];
|
|
148
|
+
switch (event.kind) {
|
|
149
|
+
case "launch":
|
|
150
|
+
return `${KIND_EMOJI.launch} New launch: ${label} on ${pad}`;
|
|
151
|
+
case "curve_trade":
|
|
152
|
+
return `${KIND_EMOJI.curve_trade} Curve ${event.side}: ${label} for ${formatUsd(event.usdValue)}`;
|
|
153
|
+
case "graduation":
|
|
154
|
+
return `${KIND_EMOJI.graduation} Graduated: ${label} filled its curve`;
|
|
155
|
+
case "whale_trade":
|
|
156
|
+
return `${KIND_EMOJI.whale_trade} Whale ${event.side}: ${formatUsd(event.usdValue)} of ${label}`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function detailRows(event) {
|
|
160
|
+
const rows = [];
|
|
161
|
+
rows.push({ label: "Launchpad", value: LAUNCHPAD_NAMES[event.launchpad], inline: true });
|
|
162
|
+
if (event.name) rows.push({ label: "Name", value: truncate(event.name, 64), inline: true });
|
|
163
|
+
rows.push({ label: "Token", value: shortAddress(event.token), inline: true });
|
|
164
|
+
switch (event.kind) {
|
|
165
|
+
case "launch": {
|
|
166
|
+
rows.push({
|
|
167
|
+
label: "Listing",
|
|
168
|
+
value: event.instantListing ? "Instant (live Uniswap v3 pool, no curve)" : "Bonding curve (graduates when filled)",
|
|
169
|
+
inline: true
|
|
170
|
+
});
|
|
171
|
+
rows.push({ label: "Deployer", value: shortAddress(event.actor), inline: true });
|
|
172
|
+
if (event.initialBuyAmount > 0n) {
|
|
173
|
+
rows.push({ label: "Initial buy", value: formatUsd(event.usdValue), inline: true });
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case "curve_trade": {
|
|
178
|
+
rows.push({ label: "Side", value: event.side === "buy" ? "Buy" : "Sell", inline: true });
|
|
179
|
+
rows.push({ label: "Size", value: formatEth(event.quoteAmountWei), inline: true });
|
|
180
|
+
rows.push({ label: "Value", value: formatUsd(event.usdValue), inline: true });
|
|
181
|
+
rows.push({
|
|
182
|
+
label: "Tokens",
|
|
183
|
+
value: formatAmount(Number((0, import_viem.formatUnits)(event.tokenAmount, 18))),
|
|
184
|
+
inline: true
|
|
185
|
+
});
|
|
186
|
+
rows.push({ label: "Trader", value: shortAddress(event.actor), inline: true });
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case "graduation": {
|
|
190
|
+
rows.push({ label: "Pool", value: shortAddress(event.pool), inline: true });
|
|
191
|
+
rows.push({ label: "Seeded", value: formatUsd(event.usdValue), inline: true });
|
|
192
|
+
rows.push({ label: "LP position", value: `#${event.positionId}`, inline: true });
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "whale_trade": {
|
|
196
|
+
rows.push({ label: "Side", value: event.side === "buy" ? "Buy" : "Sell", inline: true });
|
|
197
|
+
rows.push({ label: "Value", value: formatUsd(event.usdValue), inline: true });
|
|
198
|
+
rows.push({
|
|
199
|
+
label: "Size",
|
|
200
|
+
value: `${formatAmount(event.tokenAmount)} for ${formatAmount(event.quoteAmount)} ${event.quoteSymbol}`,
|
|
201
|
+
inline: true
|
|
202
|
+
});
|
|
203
|
+
rows.push({ label: "Price", value: `${formatAmount(event.price)} ${event.quoteSymbol}`, inline: true });
|
|
204
|
+
rows.push({ label: "Fee tier", value: `${event.feeTier / 1e4}%`, inline: true });
|
|
205
|
+
rows.push({ label: "Trader", value: shortAddress(event.actor), inline: true });
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return rows;
|
|
210
|
+
}
|
|
211
|
+
function renderAlert(event, options = {}) {
|
|
212
|
+
const parseMode = options.parseMode ?? "HTML";
|
|
213
|
+
const title = headline(event);
|
|
214
|
+
const rows = detailRows(event);
|
|
215
|
+
const label = displayLabel(event.symbol, event.token);
|
|
216
|
+
const linkPairs = [
|
|
217
|
+
{ label: "Transaction", url: event.explorer.tx },
|
|
218
|
+
{ label: "Token", url: event.explorer.token }
|
|
219
|
+
];
|
|
220
|
+
if (event.explorer.pool) linkPairs.push({ label: "Pool", url: event.explorer.pool });
|
|
221
|
+
linkPairs.push({ label: "Actor", url: event.explorer.actor });
|
|
222
|
+
const text = parseMode === "HTML" ? [
|
|
223
|
+
`<b>${escapeHtml(title)}</b>`,
|
|
224
|
+
"",
|
|
225
|
+
...rows.map((row) => `${escapeHtml(row.label)}: <b>${escapeHtml(row.value)}</b>`),
|
|
226
|
+
`Contract: <code>${escapeHtml(event.token)}</code>`,
|
|
227
|
+
"",
|
|
228
|
+
linkPairs.map((link) => htmlLink(link.label, link.url)).join(" \xB7 "),
|
|
229
|
+
...options.footer ? ["", `<i>${escapeHtml(options.footer)}</i>`] : []
|
|
230
|
+
].join("\n") : [
|
|
231
|
+
`*${escapeMarkdownV2(title)}*`,
|
|
232
|
+
"",
|
|
233
|
+
...rows.map((row) => `${escapeMarkdownV2(row.label)}: *${escapeMarkdownV2(row.value)}*`),
|
|
234
|
+
`Contract: \`${event.token}\``,
|
|
235
|
+
"",
|
|
236
|
+
linkPairs.map((link) => markdownV2Link(link.label, link.url)).join(" \xB7 "),
|
|
237
|
+
...options.footer ? ["", `_${escapeMarkdownV2(options.footer)}_`] : []
|
|
238
|
+
].join("\n");
|
|
239
|
+
const fields = rows.map((row) => ({
|
|
240
|
+
name: truncate(row.label, 256),
|
|
241
|
+
value: truncate(escapeDiscordMarkdown(row.value), 1024),
|
|
242
|
+
inline: row.inline ?? false
|
|
243
|
+
}));
|
|
244
|
+
fields.push({
|
|
245
|
+
name: "Links",
|
|
246
|
+
value: linkPairs.map((link) => `[${link.label}](${link.url})`).join(" \xB7 "),
|
|
247
|
+
inline: false
|
|
248
|
+
});
|
|
249
|
+
const embed = {
|
|
250
|
+
title: truncate(escapeDiscordMarkdown(title), 256),
|
|
251
|
+
url: event.explorer.tx,
|
|
252
|
+
description: `\`${event.token}\``,
|
|
253
|
+
color: EMBED_COLORS[event.kind],
|
|
254
|
+
fields,
|
|
255
|
+
...options.footer ? { footer: { text: truncate(options.footer, 2048) } } : {},
|
|
256
|
+
...event.timestampMs !== null ? { timestamp: new Date(event.timestampMs).toISOString() } : {}
|
|
257
|
+
};
|
|
258
|
+
const plain = `${title} | ${label} ${event.token} | block ${event.blockNumber} | ${event.explorer.tx}`;
|
|
259
|
+
return { text, parseMode, embed, plain };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/notifiers/http.ts
|
|
263
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
264
|
+
function resolveFetch(injected) {
|
|
265
|
+
if (injected) return injected;
|
|
266
|
+
if (typeof globalThis.fetch !== "function") {
|
|
267
|
+
throw new Error("hood-alerts: global fetch is unavailable (Node 20+ required), and none was injected");
|
|
268
|
+
}
|
|
269
|
+
return globalThis.fetch;
|
|
270
|
+
}
|
|
271
|
+
async function postJson(fetchImpl, url, payload, headers = {}) {
|
|
272
|
+
const response = await fetchImpl(url, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "content-type": "application/json", ...headers },
|
|
275
|
+
body: JSON.stringify(payload)
|
|
276
|
+
});
|
|
277
|
+
const raw = await response.text();
|
|
278
|
+
let body = null;
|
|
279
|
+
if (raw) {
|
|
280
|
+
try {
|
|
281
|
+
body = JSON.parse(raw);
|
|
282
|
+
} catch {
|
|
283
|
+
body = null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { status: response.status, ok: response.ok, headers: response.headers, body, raw };
|
|
287
|
+
}
|
|
288
|
+
function numericHeader(headers, name) {
|
|
289
|
+
const value = headers.get(name);
|
|
290
|
+
if (value === null) return null;
|
|
291
|
+
const parsed = Number(value);
|
|
292
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
293
|
+
}
|
|
294
|
+
function pick(body, ...path) {
|
|
295
|
+
let current = body;
|
|
296
|
+
for (const key of path) {
|
|
297
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
298
|
+
current = current[key];
|
|
299
|
+
}
|
|
300
|
+
return current;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/notifiers/types.ts
|
|
304
|
+
function delivered(status, attempts, messageId) {
|
|
305
|
+
return { ok: true, status, attempts, messageId, error: null, retryable: false };
|
|
306
|
+
}
|
|
307
|
+
function failed(status, attempts, error, retryable) {
|
|
308
|
+
return { ok: false, status, attempts, messageId: null, error, retryable };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/notifiers/telegram.ts
|
|
312
|
+
var PERMANENT_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404]);
|
|
313
|
+
function createTelegramClient(options) {
|
|
314
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(options.botToken)) {
|
|
315
|
+
throw new Error('hood-alerts: TELEGRAM_BOT_TOKEN must look like "<bot id>:<secret>"');
|
|
316
|
+
}
|
|
317
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
318
|
+
const apiBase = (options.apiBase ?? "https://api.telegram.org").replace(/\/+$/, "");
|
|
319
|
+
const maxAttempts = options.maxAttempts ?? 4;
|
|
320
|
+
const backoffMs = options.backoffMs ?? 500;
|
|
321
|
+
const maxWaitMs = options.maxWaitMs ?? 6e4;
|
|
322
|
+
const sleep2 = options.sleep ?? sleep;
|
|
323
|
+
let lastAttempts = 0;
|
|
324
|
+
return {
|
|
325
|
+
get lastAttempts() {
|
|
326
|
+
return lastAttempts;
|
|
327
|
+
},
|
|
328
|
+
async call(method, params) {
|
|
329
|
+
const url = `${apiBase}/bot${options.botToken}/${method}`;
|
|
330
|
+
let attempt = 0;
|
|
331
|
+
let last = {
|
|
332
|
+
ok: false,
|
|
333
|
+
status: 0,
|
|
334
|
+
result: null,
|
|
335
|
+
description: "no attempt was made",
|
|
336
|
+
retryAfterSeconds: null
|
|
337
|
+
};
|
|
338
|
+
while (attempt < maxAttempts) {
|
|
339
|
+
attempt += 1;
|
|
340
|
+
lastAttempts = attempt;
|
|
341
|
+
let response;
|
|
342
|
+
try {
|
|
343
|
+
response = await postJson(fetchImpl, url, params);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
last = {
|
|
346
|
+
ok: false,
|
|
347
|
+
status: 0,
|
|
348
|
+
result: null,
|
|
349
|
+
description: error instanceof Error ? error.message : String(error),
|
|
350
|
+
retryAfterSeconds: null
|
|
351
|
+
};
|
|
352
|
+
if (attempt >= maxAttempts) break;
|
|
353
|
+
await sleep2(backoffMs * 2 ** (attempt - 1));
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const description = typeof pick(response.body, "description") === "string" ? pick(response.body, "description") : null;
|
|
357
|
+
const retryAfterField = pick(response.body, "parameters", "retry_after");
|
|
358
|
+
const retryAfterSeconds = typeof retryAfterField === "number" ? retryAfterField : numericHeader(response.headers, "retry-after");
|
|
359
|
+
last = {
|
|
360
|
+
ok: response.status === 200 && pick(response.body, "ok") === true,
|
|
361
|
+
status: response.status,
|
|
362
|
+
result: pick(response.body, "result") ?? null,
|
|
363
|
+
description,
|
|
364
|
+
retryAfterSeconds
|
|
365
|
+
};
|
|
366
|
+
if (last.ok) return last;
|
|
367
|
+
if (response.status === 429) {
|
|
368
|
+
const waitMs = (retryAfterSeconds ?? backoffMs / 1e3) * 1e3;
|
|
369
|
+
if (waitMs > maxWaitMs || attempt >= maxAttempts) return last;
|
|
370
|
+
await sleep2(waitMs);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (PERMANENT_STATUSES.has(response.status)) return last;
|
|
374
|
+
if (attempt >= maxAttempts) return last;
|
|
375
|
+
await sleep2(backoffMs * 2 ** (attempt - 1));
|
|
376
|
+
}
|
|
377
|
+
return last;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function createTelegramNotifier(options) {
|
|
382
|
+
const client = options.client ?? createTelegramClient(options);
|
|
383
|
+
const disableWebPagePreview = options.disableWebPagePreview ?? true;
|
|
384
|
+
const disableNotification = options.disableNotification ?? false;
|
|
385
|
+
return {
|
|
386
|
+
platform: "telegram",
|
|
387
|
+
kind: "bot",
|
|
388
|
+
async send(target, alert) {
|
|
389
|
+
if (!/^-?\d+$|^@[A-Za-z][A-Za-z0-9_]{4,}$/.test(target)) {
|
|
390
|
+
return failed(
|
|
391
|
+
null,
|
|
392
|
+
0,
|
|
393
|
+
`invalid Telegram chat id "${target}": expected a numeric id or an @username`,
|
|
394
|
+
false
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const response = await client.call("sendMessage", {
|
|
398
|
+
chat_id: /^-?\d+$/.test(target) ? Number(target) : target,
|
|
399
|
+
text: alert.text,
|
|
400
|
+
parse_mode: alert.parseMode,
|
|
401
|
+
link_preview_options: { is_disabled: disableWebPagePreview },
|
|
402
|
+
disable_notification: disableNotification
|
|
403
|
+
});
|
|
404
|
+
const attempts = client.lastAttempts;
|
|
405
|
+
if (response.ok) {
|
|
406
|
+
const messageId = pick(response.result, "message_id");
|
|
407
|
+
return delivered(response.status, attempts, messageId === void 0 ? null : String(messageId));
|
|
408
|
+
}
|
|
409
|
+
const retryable = response.status === 429 || response.status >= 500 || response.status === 0;
|
|
410
|
+
return failed(
|
|
411
|
+
response.status || null,
|
|
412
|
+
attempts,
|
|
413
|
+
response.description ?? `Telegram sendMessage failed with status ${response.status}`,
|
|
414
|
+
retryable
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/notifiers/discord.ts
|
|
421
|
+
function createDiscordRateLimiter(now = Date.now, sleep2 = sleep) {
|
|
422
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
423
|
+
let globalResetAtMs = 0;
|
|
424
|
+
return {
|
|
425
|
+
async acquire(route) {
|
|
426
|
+
const globalWait = globalResetAtMs - now();
|
|
427
|
+
if (globalWait > 0) await sleep2(globalWait);
|
|
428
|
+
const bucket = buckets.get(route);
|
|
429
|
+
if (!bucket) return;
|
|
430
|
+
if (bucket.remaining > 0) return;
|
|
431
|
+
const wait = bucket.resetAtMs - now();
|
|
432
|
+
if (wait > 0) await sleep2(wait);
|
|
433
|
+
},
|
|
434
|
+
update(route, headers) {
|
|
435
|
+
const remaining = numericHeader(headers, "x-ratelimit-remaining");
|
|
436
|
+
const resetAfter = numericHeader(headers, "x-ratelimit-reset-after");
|
|
437
|
+
if (remaining === null && resetAfter === null) return;
|
|
438
|
+
buckets.set(route, {
|
|
439
|
+
remaining: remaining ?? 1,
|
|
440
|
+
resetAtMs: now() + (resetAfter ?? 0) * 1e3
|
|
441
|
+
});
|
|
442
|
+
},
|
|
443
|
+
penalise(route, retryAfterMs, global) {
|
|
444
|
+
const resetAtMs = now() + retryAfterMs;
|
|
445
|
+
if (global) globalResetAtMs = Math.max(globalResetAtMs, resetAtMs);
|
|
446
|
+
buckets.set(route, { remaining: 0, resetAtMs });
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
var PERMANENT_STATUSES2 = /* @__PURE__ */ new Set([400, 401, 403, 404, 405]);
|
|
451
|
+
async function deliver(args, options, fetchImpl, limiter) {
|
|
452
|
+
const maxAttempts = options.maxAttempts ?? 4;
|
|
453
|
+
const backoffMs = options.backoffMs ?? 500;
|
|
454
|
+
const maxWaitMs = options.maxWaitMs ?? 6e4;
|
|
455
|
+
const sleep2 = options.sleep ?? sleep;
|
|
456
|
+
let attempt = 0;
|
|
457
|
+
let lastStatus = null;
|
|
458
|
+
let lastError = "no attempt was made";
|
|
459
|
+
let retryable = true;
|
|
460
|
+
while (attempt < maxAttempts) {
|
|
461
|
+
attempt += 1;
|
|
462
|
+
await limiter.acquire(args.route);
|
|
463
|
+
let response;
|
|
464
|
+
try {
|
|
465
|
+
response = await postJson(fetchImpl, args.url, args.payload, args.headers);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
lastStatus = null;
|
|
468
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
469
|
+
retryable = true;
|
|
470
|
+
if (attempt >= maxAttempts) break;
|
|
471
|
+
await sleep2(backoffMs * 2 ** (attempt - 1));
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
limiter.update(args.route, response.headers);
|
|
475
|
+
lastStatus = response.status;
|
|
476
|
+
if (response.status === 429) {
|
|
477
|
+
const retryAfterField = pick(response.body, "retry_after");
|
|
478
|
+
const retryAfterSeconds = typeof retryAfterField === "number" ? retryAfterField : numericHeader(response.headers, "retry-after") ?? backoffMs / 1e3;
|
|
479
|
+
const isGlobal = pick(response.body, "global") === true || response.headers.get("x-ratelimit-global") === "true";
|
|
480
|
+
const waitMs = retryAfterSeconds * 1e3;
|
|
481
|
+
limiter.penalise(args.route, waitMs, isGlobal);
|
|
482
|
+
lastError = `rate limited, retry after ${retryAfterSeconds}s${isGlobal ? " (global)" : ""}`;
|
|
483
|
+
retryable = true;
|
|
484
|
+
if (waitMs > maxWaitMs || attempt >= maxAttempts) break;
|
|
485
|
+
await sleep2(waitMs);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (response.ok) {
|
|
489
|
+
const messageId = pick(response.body, "id");
|
|
490
|
+
return delivered(response.status, attempt, messageId === void 0 ? null : String(messageId));
|
|
491
|
+
}
|
|
492
|
+
const message = (typeof pick(response.body, "message") === "string" ? pick(response.body, "message") : null) ?? response.raw.slice(0, 200);
|
|
493
|
+
lastError = message || `Discord request failed with status ${response.status}`;
|
|
494
|
+
if (PERMANENT_STATUSES2.has(response.status)) {
|
|
495
|
+
retryable = false;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
retryable = true;
|
|
499
|
+
if (attempt >= maxAttempts) break;
|
|
500
|
+
await sleep2(backoffMs * 2 ** (attempt - 1));
|
|
501
|
+
}
|
|
502
|
+
return failed(lastStatus, attempt, lastError, retryable);
|
|
503
|
+
}
|
|
504
|
+
var WEBHOOK_PATTERN = /^https:\/\/(?:\w+\.)?discord(?:app)?\.com\/api(?:\/v\d+)?\/webhooks\/\d+\/[\w-]+$/;
|
|
505
|
+
function createDiscordWebhookNotifier(options = {}) {
|
|
506
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
507
|
+
const limiter = options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? sleep);
|
|
508
|
+
return {
|
|
509
|
+
platform: "discord",
|
|
510
|
+
kind: "webhook",
|
|
511
|
+
async send(target, alert) {
|
|
512
|
+
const url = target || options.webhookUrl;
|
|
513
|
+
if (!url || !WEBHOOK_PATTERN.test(url)) {
|
|
514
|
+
return failed(
|
|
515
|
+
null,
|
|
516
|
+
0,
|
|
517
|
+
`invalid Discord webhook URL "${target}": expected https://discord.com/api/webhooks/<id>/<token>`,
|
|
518
|
+
false
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return deliver(
|
|
522
|
+
{
|
|
523
|
+
url: `${url}?wait=true`,
|
|
524
|
+
// Discord buckets webhook routes per webhook id.
|
|
525
|
+
route: `webhook:${url.split("/").slice(-2, -1)[0] ?? url}`,
|
|
526
|
+
payload: {
|
|
527
|
+
embeds: [alert.embed],
|
|
528
|
+
...options.username ? { username: options.username } : {}
|
|
529
|
+
},
|
|
530
|
+
headers: {}
|
|
531
|
+
},
|
|
532
|
+
options,
|
|
533
|
+
fetchImpl,
|
|
534
|
+
limiter
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function createDiscordBotNotifier(options) {
|
|
540
|
+
if (!options.botToken.trim()) {
|
|
541
|
+
throw new Error("hood-alerts: DISCORD_BOT_TOKEN is required for the Discord bot notifier");
|
|
542
|
+
}
|
|
543
|
+
const fetchImpl = resolveFetch(options.fetch);
|
|
544
|
+
const apiBase = (options.apiBase ?? "https://discord.com/api/v10").replace(/\/+$/, "");
|
|
545
|
+
const limiter = options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? sleep);
|
|
546
|
+
return {
|
|
547
|
+
platform: "discord",
|
|
548
|
+
kind: "bot",
|
|
549
|
+
async send(target, alert) {
|
|
550
|
+
if (!/^\d{5,25}$/.test(target)) {
|
|
551
|
+
return failed(null, 0, `invalid Discord channel id "${target}": expected a numeric snowflake`, false);
|
|
552
|
+
}
|
|
553
|
+
return deliver(
|
|
554
|
+
{
|
|
555
|
+
url: `${apiBase}/channels/${target}/messages`,
|
|
556
|
+
route: `channel:${target}`,
|
|
557
|
+
payload: { embeds: [alert.embed] },
|
|
558
|
+
headers: { authorization: `Bot ${options.botToken}` }
|
|
559
|
+
},
|
|
560
|
+
options,
|
|
561
|
+
fetchImpl,
|
|
562
|
+
limiter
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/notifiers/capture.ts
|
|
569
|
+
function createCaptureNotifier(platform, onSend, now = Date.now) {
|
|
570
|
+
const sent = [];
|
|
571
|
+
return {
|
|
572
|
+
platform,
|
|
573
|
+
kind: "capture",
|
|
574
|
+
sent,
|
|
575
|
+
clear() {
|
|
576
|
+
sent.length = 0;
|
|
577
|
+
},
|
|
578
|
+
async send(target, alert) {
|
|
579
|
+
const delivery = { platform, target, alert, atMs: now() };
|
|
580
|
+
sent.push(delivery);
|
|
581
|
+
onSend?.(delivery);
|
|
582
|
+
return delivered(200, 1, `capture-${sent.length}`);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
587
|
+
0 && (module.exports = {
|
|
588
|
+
EMBED_COLORS,
|
|
589
|
+
createCaptureNotifier,
|
|
590
|
+
createDiscordBotNotifier,
|
|
591
|
+
createDiscordRateLimiter,
|
|
592
|
+
createDiscordWebhookNotifier,
|
|
593
|
+
createTelegramClient,
|
|
594
|
+
createTelegramNotifier,
|
|
595
|
+
delivered,
|
|
596
|
+
detailRows,
|
|
597
|
+
displayLabel,
|
|
598
|
+
escapeDiscordMarkdown,
|
|
599
|
+
escapeHtml,
|
|
600
|
+
escapeHtmlAttribute,
|
|
601
|
+
escapeMarkdownV2,
|
|
602
|
+
escapeMarkdownV2Code,
|
|
603
|
+
escapeMarkdownV2Url,
|
|
604
|
+
failed,
|
|
605
|
+
formatAmount,
|
|
606
|
+
formatEth,
|
|
607
|
+
formatUsd,
|
|
608
|
+
headline,
|
|
609
|
+
htmlLink,
|
|
610
|
+
markdownV2Link,
|
|
611
|
+
numericHeader,
|
|
612
|
+
pick,
|
|
613
|
+
postJson,
|
|
614
|
+
renderAlert,
|
|
615
|
+
resolveFetch,
|
|
616
|
+
shortAddress,
|
|
617
|
+
sleep,
|
|
618
|
+
truncate
|
|
619
|
+
});
|
|
620
|
+
//# sourceMappingURL=index.cjs.map
|