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 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/notifiers/index.ts","../../src/notifiers/escape.ts","../../src/notifiers/format.ts","../../src/notifiers/http.ts","../../src/notifiers/types.ts","../../src/notifiers/telegram.ts","../../src/notifiers/discord.ts","../../src/notifiers/capture.ts"],"sourcesContent":["/**\n * Delivery adapters: Telegram Bot API, Discord webhook and Discord bot, behind\n * one {@link Notifier} interface, with correct escaping and correct\n * rate-limit handling per platform.\n *\n * @packageDocumentation\n */\n\nexport {\n displayLabel,\n escapeDiscordMarkdown,\n escapeHtml,\n escapeHtmlAttribute,\n escapeMarkdownV2,\n escapeMarkdownV2Code,\n escapeMarkdownV2Url,\n htmlLink,\n markdownV2Link,\n shortAddress,\n truncate,\n} from './escape.js'\n\nexport { EMBED_COLORS, detailRows, formatAmount, formatEth, formatUsd, headline, renderAlert } from './format.js'\nexport type { RenderOptions } from './format.js'\n\nexport { createTelegramClient, createTelegramNotifier } from './telegram.js'\nexport type {\n TelegramClient,\n TelegramClientOptions,\n TelegramNotifierOptions,\n TelegramResponse,\n} from './telegram.js'\n\nexport {\n createDiscordBotNotifier,\n createDiscordRateLimiter,\n createDiscordWebhookNotifier,\n} from './discord.js'\nexport type {\n DiscordBotOptions,\n DiscordOptionsBase,\n DiscordRateLimiter,\n DiscordWebhookOptions,\n} from './discord.js'\n\nexport { createCaptureNotifier } from './capture.js'\nexport type { CaptureNotifier, CapturedDelivery } from './capture.js'\n\nexport { delivered, failed } from './types.js'\nexport type {\n DeliveryResult,\n DiscordEmbed,\n DiscordEmbedField,\n FetchLike,\n FetchLikeResponse,\n Notifier,\n NotifierHttpOptions,\n RenderedAlert,\n} from './types.js'\n\nexport { numericHeader, pick, postJson, resolveFetch, sleep } from './http.js'\nexport type { JsonResponse } from './http.js'\n","/**\n * Escaping. The single most common way an alert bot breaks in production.\n *\n * Memecoin names are adversarial input by nature. A token called `WHO_LET_THE`\n * silently italicises half a Telegram message under MarkdownV2 and a token\n * called `<b>RUG` injects markup under HTML parse mode; either way Telegram\n * answers `400 Bad Request: can't parse entities` and the alert is lost. The\n * rules are not symmetric between platforms or even between contexts within a\n * platform, so each context gets its own function and its own tests.\n */\n\n/**\n * Telegram MarkdownV2 reserves exactly these characters in normal text.\n * Source: Telegram Bot API \"MarkdownV2 style\" section. All 18 of them must be\n * escaped with a preceding backslash anywhere they appear as literal text.\n */\nconst MARKDOWN_V2_RESERVED = '_*[]()~`>#+-=|{}.!'\n\n/** Escape literal text for Telegram MarkdownV2. */\nexport function escapeMarkdownV2(text: string): string {\n let out = ''\n for (const char of text) {\n if (MARKDOWN_V2_RESERVED.includes(char)) out += '\\\\'\n out += char\n }\n return out\n}\n\n/**\n * Escape the URL inside a MarkdownV2 inline link `[label](url)`.\n * Inside the parentheses only `)` and `\\` are special, and escaping the full\n * reserved set here would corrupt query strings and path separators.\n */\nexport function escapeMarkdownV2Url(url: string): string {\n return url.replace(/[\\\\)]/g, (char) => `\\\\${char}`)\n}\n\n/**\n * Escape text inside a MarkdownV2 code span or `pre` block. Only `` ` `` and\n * `\\` are special there. Escaping the full set would print literal backslashes\n * to the user, which is the classic over-correction of this bug.\n */\nexport function escapeMarkdownV2Code(text: string): string {\n return text.replace(/[\\\\`]/g, (char) => `\\\\${char}`)\n}\n\n/** Build a MarkdownV2 inline link with both halves escaped correctly. */\nexport function markdownV2Link(label: string, url: string): string {\n return `[${escapeMarkdownV2(label)}](${escapeMarkdownV2Url(url)})`\n}\n\n/**\n * Escape text for Telegram HTML parse mode. Telegram's HTML is a small subset\n * and requires exactly `&`, `<` and `>` to be replaced in text nodes.\n */\nexport function escapeHtml(text: string): string {\n return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\n}\n\n/** Escape a value going into an HTML attribute (a link `href`). */\nexport function escapeHtmlAttribute(value: string): string {\n return escapeHtml(value).replace(/\"/g, '"')\n}\n\n/** Build a Telegram HTML anchor with both halves escaped. */\nexport function htmlLink(label: string, url: string): string {\n return `<a href=\"${escapeHtmlAttribute(url)}\">${escapeHtml(label)}</a>`\n}\n\n/**\n * Discord markdown special characters. Discord renders markdown inside embed\n * titles, descriptions and field values, so untrusted token names need the\n * same treatment. `#` is not escaped: it is only special at the start of a\n * line for headings, and every place we interpolate a name is mid-line.\n */\nconst DISCORD_RESERVED = /[\\\\*_~`|>[\\]()]/g\n\n/** Escape literal text for Discord markdown rendering. */\nexport function escapeDiscordMarkdown(text: string): string {\n return text.replace(DISCORD_RESERVED, (char) => `\\\\${char}`)\n}\n\n/**\n * Truncate to a maximum length without splitting a surrogate pair, which is\n * what turns a long emoji-bearing token name into a replacement character.\n * The ellipsis is included in the budget.\n */\nexport function truncate(text: string, max: number): string {\n if (max <= 0) return ''\n const chars = [...text]\n if (chars.length <= max) return text\n if (max === 1) return '…'\n return `${chars.slice(0, max - 1).join('')}…`\n}\n\n/**\n * A display label for a token: its symbol when the contract exposed one,\n * otherwise a shortened address. Never returns an empty string, because an\n * empty Telegram entity is a 400.\n */\nexport function displayLabel(symbol: string | null, address: string): string {\n const trimmed = symbol?.trim()\n if (trimmed) return truncate(trimmed, 40)\n return shortAddress(address)\n}\n\n/** `0x1234…abcd`, the standard compact address rendering. */\nexport function shortAddress(address: string): string {\n if (address.length <= 12) return address\n return `${address.slice(0, 6)}…${address.slice(-4)}`\n}\n","import { formatUnits } from 'viem'\nimport type { AlertEvent } from '../events/types.js'\nimport {\n displayLabel,\n escapeDiscordMarkdown,\n escapeHtml,\n escapeMarkdownV2,\n htmlLink,\n markdownV2Link,\n shortAddress,\n truncate,\n} from './escape.js'\nimport type { DiscordEmbed, DiscordEmbedField, RenderedAlert } from './types.js'\n\n/**\n * Message formatting, one alert at a time, for both platforms.\n *\n * Every piece of chain-derived text (token name, symbol) goes through the\n * platform's escaper. Every piece of package-authored text (labels, headings)\n * is written to be escape-safe already, so the output stays readable instead\n * of being a wall of backslashes.\n */\n\n/** Colour per event kind, as Discord's decimal RGB. */\nexport const EMBED_COLORS = {\n launch: 0x22c55e,\n curve_trade: 0x38bdf8,\n graduation: 0xa855f7,\n whale_trade: 0xf59e0b,\n} as const\n\nconst KIND_EMOJI = {\n launch: '🚀',\n curve_trade: '📈',\n graduation: '🎓',\n whale_trade: '🐋',\n} as const\n\nconst LAUNCHPAD_NAMES = { noxa: 'NOXA', odyssey: 'The Odyssey' } as const\n\nconst usdFormatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 2,\n})\n\n/** Compact USD rendering: `$1,234.50`, `$12.4K`, `$3.1M`. */\nexport function formatUsd(value: number | null): string {\n if (value === null || !Number.isFinite(value)) return 'unknown'\n const abs = Math.abs(value)\n if (abs >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`\n if (abs >= 10_000) return `$${(value / 1_000).toFixed(1)}K`\n return usdFormatter.format(value)\n}\n\n/** Human amount rendering with sane precision across 12 orders of magnitude. */\nexport function formatAmount(value: number): string {\n const abs = Math.abs(value)\n if (abs === 0) return '0'\n if (abs >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)}B`\n if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`\n if (abs >= 1_000) return `${(value / 1_000).toFixed(2)}K`\n if (abs >= 1) return value.toFixed(4).replace(/\\.?0+$/, '')\n return value.toPrecision(4)\n}\n\n/** ETH amount from wei, with a trailing `ETH`. */\nexport function formatEth(wei: bigint): string {\n return `${formatAmount(Number(formatUnits(wei < 0n ? -wei : wei, 18)))} ETH`\n}\n\n/** The one-line headline for an event, before escaping. */\nexport function headline(event: AlertEvent): string {\n const label = displayLabel(event.symbol, event.token)\n const pad = LAUNCHPAD_NAMES[event.launchpad]\n switch (event.kind) {\n case 'launch':\n return `${KIND_EMOJI.launch} New launch: ${label} on ${pad}`\n case 'curve_trade':\n return `${KIND_EMOJI.curve_trade} Curve ${event.side}: ${label} for ${formatUsd(event.usdValue)}`\n case 'graduation':\n return `${KIND_EMOJI.graduation} Graduated: ${label} filled its curve`\n case 'whale_trade':\n return `${KIND_EMOJI.whale_trade} Whale ${event.side}: ${formatUsd(event.usdValue)} of ${label}`\n }\n}\n\n/** Label/value pairs describing an event, before escaping. */\nexport function detailRows(event: AlertEvent): { label: string; value: string; inline?: boolean }[] {\n const rows: { label: string; value: string; inline?: boolean }[] = []\n rows.push({ label: 'Launchpad', value: LAUNCHPAD_NAMES[event.launchpad], inline: true })\n if (event.name) rows.push({ label: 'Name', value: truncate(event.name, 64), inline: true })\n rows.push({ label: 'Token', value: shortAddress(event.token), inline: true })\n\n switch (event.kind) {\n case 'launch': {\n rows.push({\n label: 'Listing',\n value: event.instantListing\n ? 'Instant (live Uniswap v3 pool, no curve)'\n : 'Bonding curve (graduates when filled)',\n inline: true,\n })\n rows.push({ label: 'Deployer', value: shortAddress(event.actor), inline: true })\n if (event.initialBuyAmount > 0n) {\n rows.push({ label: 'Initial buy', value: formatUsd(event.usdValue), inline: true })\n }\n break\n }\n case 'curve_trade': {\n rows.push({ label: 'Side', value: event.side === 'buy' ? 'Buy' : 'Sell', inline: true })\n rows.push({ label: 'Size', value: formatEth(event.quoteAmountWei), inline: true })\n rows.push({ label: 'Value', value: formatUsd(event.usdValue), inline: true })\n rows.push({\n label: 'Tokens',\n value: formatAmount(Number(formatUnits(event.tokenAmount, 18))),\n inline: true,\n })\n rows.push({ label: 'Trader', value: shortAddress(event.actor), inline: true })\n break\n }\n case 'graduation': {\n rows.push({ label: 'Pool', value: shortAddress(event.pool), inline: true })\n rows.push({ label: 'Seeded', value: formatUsd(event.usdValue), inline: true })\n rows.push({ label: 'LP position', value: `#${event.positionId}`, inline: true })\n break\n }\n case 'whale_trade': {\n rows.push({ label: 'Side', value: event.side === 'buy' ? 'Buy' : 'Sell', inline: true })\n rows.push({ label: 'Value', value: formatUsd(event.usdValue), inline: true })\n rows.push({\n label: 'Size',\n value: `${formatAmount(event.tokenAmount)} for ${formatAmount(event.quoteAmount)} ${event.quoteSymbol}`,\n inline: true,\n })\n rows.push({ label: 'Price', value: `${formatAmount(event.price)} ${event.quoteSymbol}`, inline: true })\n rows.push({ label: 'Fee tier', value: `${event.feeTier / 10_000}%`, inline: true })\n rows.push({ label: 'Trader', value: shortAddress(event.actor), inline: true })\n break\n }\n }\n return rows\n}\n\n/** Options for {@link renderAlert}. */\nexport interface RenderOptions {\n /**\n * Telegram parse mode. HTML is the default: its escape rules have three\n * special characters instead of eighteen, which makes malformed-entity 400s\n * far less likely on adversarial token names.\n * @defaultValue `'HTML'`\n */\n parseMode?: 'HTML' | 'MarkdownV2'\n /** Footer text, e.g. the rule that matched. */\n footer?: string\n}\n\n/**\n * Render an event for both platforms.\n *\n * @example\n * ```ts\n * const alert = renderAlert(event, { footer: 'rule: whales' })\n * await telegram.send('123456789', alert)\n * await discord.send('987654321', alert)\n * ```\n */\nexport function renderAlert(event: AlertEvent, options: RenderOptions = {}): RenderedAlert {\n const parseMode = options.parseMode ?? 'HTML'\n const title = headline(event)\n const rows = detailRows(event)\n const label = displayLabel(event.symbol, event.token)\n\n const linkPairs: { label: string; url: string }[] = [\n { label: 'Transaction', url: event.explorer.tx },\n { label: 'Token', url: event.explorer.token },\n ]\n if (event.explorer.pool) linkPairs.push({ label: 'Pool', url: event.explorer.pool })\n linkPairs.push({ label: 'Actor', url: event.explorer.actor })\n\n const text =\n parseMode === 'HTML'\n ? [\n `<b>${escapeHtml(title)}</b>`,\n '',\n ...rows.map((row) => `${escapeHtml(row.label)}: <b>${escapeHtml(row.value)}</b>`),\n `Contract: <code>${escapeHtml(event.token)}</code>`,\n '',\n linkPairs.map((link) => htmlLink(link.label, link.url)).join(' · '),\n ...(options.footer ? ['', `<i>${escapeHtml(options.footer)}</i>`] : []),\n ].join('\\n')\n : [\n `*${escapeMarkdownV2(title)}*`,\n '',\n ...rows.map((row) => `${escapeMarkdownV2(row.label)}: *${escapeMarkdownV2(row.value)}*`),\n `Contract: \\`${event.token}\\``,\n '',\n linkPairs.map((link) => markdownV2Link(link.label, link.url)).join(' · '),\n ...(options.footer ? ['', `_${escapeMarkdownV2(options.footer)}_`] : []),\n ].join('\\n')\n\n const fields: DiscordEmbedField[] = rows.map((row) => ({\n name: truncate(row.label, 256),\n value: truncate(escapeDiscordMarkdown(row.value), 1024),\n inline: row.inline ?? false,\n }))\n fields.push({\n name: 'Links',\n value: linkPairs.map((link) => `[${link.label}](${link.url})`).join(' · '),\n inline: false,\n })\n\n const embed: DiscordEmbed = {\n title: truncate(escapeDiscordMarkdown(title), 256),\n url: event.explorer.tx,\n description: `\\`${event.token}\\``,\n color: EMBED_COLORS[event.kind],\n fields,\n ...(options.footer ? { footer: { text: truncate(options.footer, 2048) } } : {}),\n ...(event.timestampMs !== null\n ? { timestamp: new Date(event.timestampMs).toISOString() }\n : {}),\n }\n\n const plain = `${title} | ${label} ${event.token} | block ${event.blockNumber} | ${event.explorer.tx}`\n\n return { text, parseMode, embed, plain }\n}\n","import type { FetchLike, FetchLikeResponse } from './types.js'\n\n/** A completed HTTP response with its body parsed as JSON when possible. */\nexport interface JsonResponse {\n status: number\n ok: boolean\n headers: { get(name: string): string | null }\n body: unknown\n raw: string\n}\n\n/** Default sleep. */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)))\n\n/** Resolve the `fetch` to use: the injected double in tests, the global otherwise. */\nexport function resolveFetch(injected?: FetchLike): FetchLike {\n if (injected) return injected\n if (typeof globalThis.fetch !== 'function') {\n throw new Error('hood-alerts: global fetch is unavailable (Node 20+ required), and none was injected')\n }\n return globalThis.fetch as unknown as FetchLike\n}\n\n/**\n * POST JSON and parse the response.\n *\n * A non-JSON body (an HTML error page from a proxy, an empty 204) is not an\n * error here: `body` becomes `null` and `raw` keeps the text, so callers can\n * report what actually came back instead of a `SyntaxError` from deep inside\n * the adapter.\n */\nexport async function postJson(\n fetchImpl: FetchLike,\n url: string,\n payload: unknown,\n headers: Record<string, string> = {},\n): Promise<JsonResponse> {\n const response: FetchLikeResponse = await fetchImpl(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...headers },\n body: JSON.stringify(payload),\n })\n const raw = await response.text()\n let body: unknown = null\n if (raw) {\n try {\n body = JSON.parse(raw)\n } catch {\n body = null\n }\n }\n return { status: response.status, ok: response.ok, headers: response.headers, body, raw }\n}\n\n/** Read a numeric header, or `null` when absent or unparseable. */\nexport function numericHeader(\n headers: { get(name: string): string | null },\n name: string,\n): number | null {\n const value = headers.get(name)\n if (value === null) return null\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/** Read a nested property from an unknown JSON body without casting blindly. */\nexport function pick(body: unknown, ...path: string[]): unknown {\n let current: unknown = body\n for (const key of path) {\n if (typeof current !== 'object' || current === null) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n","import type { Platform } from '../rules/schema.js'\n\n/** A Discord embed field. */\nexport interface DiscordEmbedField {\n name: string\n value: string\n inline?: boolean\n}\n\n/** The subset of the Discord embed object this package produces. */\nexport interface DiscordEmbed {\n title: string\n url?: string\n description?: string\n /** Decimal RGB, per Discord's API. */\n color: number\n fields: DiscordEmbedField[]\n footer?: { text: string }\n /** ISO-8601 timestamp. Omitted when the block time was not read. */\n timestamp?: string\n}\n\n/**\n * One alert, rendered for every platform up front.\n *\n * Rendering once and letting each notifier pick its representation keeps the\n * escaping rules next to each other (where they can be compared and tested\n * together) and means a fan-out to Telegram and Discord formats the event\n * once, not twice.\n */\nexport interface RenderedAlert {\n /** Telegram body, escaped for {@link RenderedAlert.parseMode}. */\n text: string\n parseMode: 'HTML' | 'MarkdownV2'\n /** Discord embed representation of the same alert. */\n embed: DiscordEmbed\n /** Unstyled one-line summary, for logs and for platforms added later. */\n plain: string\n}\n\n/** What happened when a notifier tried to deliver. */\nexport interface DeliveryResult {\n ok: boolean\n /** HTTP status of the final attempt, or `null` if the request never completed. */\n status: number | null\n /** How many requests were made, including the successful one. */\n attempts: number\n /** Platform message id, when the API returned one. */\n messageId: string | null\n /** Failure detail. `null` on success. */\n error: string | null\n /**\n * `true` when the failure is worth retrying later (5xx, network, exhausted\n * rate limit). `false` for a permanent rejection such as a bad chat id, so\n * the service can disable a dead subscription instead of retrying forever.\n */\n retryable: boolean\n}\n\n/** The one interface every delivery adapter implements. */\nexport interface Notifier {\n readonly platform: Platform\n /** Adapter flavour, for logs and metrics: `bot`, `webhook`. */\n readonly kind: string\n /**\n * Deliver a rendered alert to a platform-specific target: a Telegram chat\n * id, a Discord channel id, or a Discord webhook URL.\n */\n send(target: string, alert: RenderedAlert): Promise<DeliveryResult>\n}\n\n/** The `fetch` shape the adapters use, so tests can substitute a double. */\nexport type FetchLike = (\n input: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<FetchLikeResponse>\n\n/** The response shape the adapters read. */\nexport interface FetchLikeResponse {\n ok: boolean\n status: number\n headers: { get(name: string): string | null }\n text(): Promise<string>\n}\n\n/** Options common to every HTTP-backed notifier. */\nexport interface NotifierHttpOptions {\n /** Injected `fetch`. Defaults to the global. */\n fetch?: FetchLike\n /** Max attempts per delivery, including the first. @defaultValue `4` */\n maxAttempts?: number\n /** Base backoff in ms for retryable failures, doubled per attempt. @defaultValue `500` */\n backoffMs?: number\n /**\n * Upper bound on how long a single delivery will wait for a rate limit, in\n * ms. A `retry_after` beyond this gives up and reports a retryable failure\n * rather than blocking the whole dispatch loop.\n * @defaultValue `60_000`\n */\n maxWaitMs?: number\n /** Sleep injection point for tests. */\n sleep?: (ms: number) => Promise<void>\n /** Request timeout in ms. @defaultValue `15_000` */\n timeoutMs?: number\n}\n\n/** A successful send result. */\nexport function delivered(status: number, attempts: number, messageId: string | null): DeliveryResult {\n return { ok: true, status, attempts, messageId, error: null, retryable: false }\n}\n\n/** A failed send result. */\nexport function failed(\n status: number | null,\n attempts: number,\n error: string,\n retryable: boolean,\n): DeliveryResult {\n return { ok: false, status, attempts, messageId: null, error, retryable }\n}\n","import { numericHeader, pick, postJson, resolveFetch, sleep as realSleep } from './http.js'\nimport {\n delivered,\n failed,\n type DeliveryResult,\n type FetchLike,\n type Notifier,\n type NotifierHttpOptions,\n type RenderedAlert,\n} from './types.js'\n\n/**\n * Telegram Bot API adapter.\n *\n * Rate limiting is the part everyone gets wrong. Telegram does not use a\n * header budget: it answers `429` with a JSON body\n * `{\"ok\":false,\"error_code\":429,\"description\":\"Too Many Requests: retry after 7\",\n * \"parameters\":{\"retry_after\":7}}` and expects the caller to wait exactly that\n * many *seconds* before retrying. This adapter honours `parameters.retry_after`\n * (falling back to the `Retry-After` header, then to exponential backoff), caps\n * the total wait at `maxWaitMs` so one throttled chat cannot stall the whole\n * dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a\n * blocked bot stops being retried forever.\n */\n\n/** A raw Telegram Bot API response. */\nexport interface TelegramResponse {\n ok: boolean\n status: number\n /** `result` on success. */\n result: unknown\n /** `description` on failure. */\n description: string | null\n /** `parameters.retry_after` in seconds, when Telegram sent one. */\n retryAfterSeconds: number | null\n}\n\n/** Options for {@link createTelegramClient}. */\nexport interface TelegramClientOptions extends NotifierHttpOptions {\n /** Bot token from @BotFather, `<id>:<secret>`. */\n botToken: string\n /** API origin. Override for a local Bot API server. @defaultValue `https://api.telegram.org` */\n apiBase?: string\n}\n\n/** A thin, rate-limit-aware Telegram Bot API client. */\nexport interface TelegramClient {\n /** Call any Bot API method. Retries 429 and 5xx, never retries 4xx. */\n call(method: string, params: Record<string, unknown>): Promise<TelegramResponse>\n /** How many requests the last {@link TelegramClient.call} needed. */\n readonly lastAttempts: number\n}\n\nconst PERMANENT_STATUSES = new Set([400, 401, 403, 404])\n\n/** Build a Telegram Bot API client. */\nexport function createTelegramClient(options: TelegramClientOptions): TelegramClient {\n if (!/^\\d+:[A-Za-z0-9_-]+$/.test(options.botToken)) {\n throw new Error('hood-alerts: TELEGRAM_BOT_TOKEN must look like \"<bot id>:<secret>\"')\n }\n const fetchImpl: FetchLike = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://api.telegram.org').replace(/\\/+$/, '')\n const maxAttempts = options.maxAttempts ?? 4\n const backoffMs = options.backoffMs ?? 500\n const maxWaitMs = options.maxWaitMs ?? 60_000\n const sleep = options.sleep ?? realSleep\n\n let lastAttempts = 0\n\n return {\n get lastAttempts() {\n return lastAttempts\n },\n async call(method: string, params: Record<string, unknown>): Promise<TelegramResponse> {\n const url = `${apiBase}/bot${options.botToken}/${method}`\n let attempt = 0\n let last: TelegramResponse = {\n ok: false,\n status: 0,\n result: null,\n description: 'no attempt was made',\n retryAfterSeconds: null,\n }\n\n while (attempt < maxAttempts) {\n attempt += 1\n lastAttempts = attempt\n let response\n try {\n response = await postJson(fetchImpl, url, params)\n } catch (error) {\n // Transport failure: retry with backoff, it is not a Telegram verdict.\n last = {\n ok: false,\n status: 0,\n result: null,\n description: error instanceof Error ? error.message : String(error),\n retryAfterSeconds: null,\n }\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n continue\n }\n\n const description = typeof pick(response.body, 'description') === 'string'\n ? (pick(response.body, 'description') as string)\n : null\n const retryAfterField = pick(response.body, 'parameters', 'retry_after')\n const retryAfterSeconds =\n typeof retryAfterField === 'number'\n ? retryAfterField\n : numericHeader(response.headers, 'retry-after')\n\n last = {\n ok: response.status === 200 && pick(response.body, 'ok') === true,\n status: response.status,\n result: pick(response.body, 'result') ?? null,\n description,\n retryAfterSeconds,\n }\n if (last.ok) return last\n\n if (response.status === 429) {\n const waitMs = (retryAfterSeconds ?? backoffMs / 1000) * 1000\n if (waitMs > maxWaitMs || attempt >= maxAttempts) return last\n await sleep(waitMs)\n continue\n }\n if (PERMANENT_STATUSES.has(response.status)) return last\n if (attempt >= maxAttempts) return last\n await sleep(backoffMs * 2 ** (attempt - 1))\n }\n return last\n },\n }\n}\n\n/** Options for {@link createTelegramNotifier}. */\nexport interface TelegramNotifierOptions extends TelegramClientOptions {\n /** Suppress link previews. @defaultValue `true` (alerts carry several links). */\n disableWebPagePreview?: boolean\n /** Deliver silently, without a notification sound. @defaultValue `false` */\n disableNotification?: boolean\n /** Reuse an existing client instead of building one (the bot shares its client). */\n client?: TelegramClient\n}\n\n/**\n * Build the Telegram delivery adapter.\n *\n * @example\n * ```ts\n * const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN! })\n * const result = await telegram.send('-1001234567890', renderAlert(event))\n * if (!result.ok) console.error(result.error, 'retryable:', result.retryable)\n * ```\n */\nexport function createTelegramNotifier(options: TelegramNotifierOptions): Notifier {\n const client = options.client ?? createTelegramClient(options)\n const disableWebPagePreview = options.disableWebPagePreview ?? true\n const disableNotification = options.disableNotification ?? false\n\n return {\n platform: 'telegram',\n kind: 'bot',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n if (!/^-?\\d+$|^@[A-Za-z][A-Za-z0-9_]{4,}$/.test(target)) {\n return failed(\n null,\n 0,\n `invalid Telegram chat id \"${target}\": expected a numeric id or an @username`,\n false,\n )\n }\n const response = await client.call('sendMessage', {\n chat_id: /^-?\\d+$/.test(target) ? Number(target) : target,\n text: alert.text,\n parse_mode: alert.parseMode,\n link_preview_options: { is_disabled: disableWebPagePreview },\n disable_notification: disableNotification,\n })\n const attempts = client.lastAttempts\n\n if (response.ok) {\n const messageId = pick(response.result, 'message_id')\n return delivered(response.status, attempts, messageId === undefined ? null : String(messageId))\n }\n const retryable = response.status === 429 || response.status >= 500 || response.status === 0\n return failed(\n response.status || null,\n attempts,\n response.description ?? `Telegram sendMessage failed with status ${response.status}`,\n retryable,\n )\n },\n }\n}\n","import { numericHeader, pick, postJson, resolveFetch, sleep as realSleep } from './http.js'\nimport {\n delivered,\n failed,\n type DeliveryResult,\n type FetchLike,\n type Notifier,\n type NotifierHttpOptions,\n type RenderedAlert,\n} from './types.js'\n\n/**\n * Discord adapters: webhook and bot (application) delivery, behind the same\n * {@link Notifier} interface.\n *\n * Discord's rate limiting is a per-route bucket, published on every response:\n * `X-RateLimit-Remaining` and `X-RateLimit-Reset-After` (seconds, fractional).\n * The correct behaviour is to *not send* once a bucket is exhausted rather\n * than to send and absorb the 429, so this adapter tracks the bucket per route\n * and waits before the request. A 429 that still slips through (a global limit,\n * or another process sharing the token) is honoured through the `retry_after`\n * field in the JSON body, which unlike Telegram's is in **seconds as a float**.\n * `X-RateLimit-Global` marks a token-wide limit, which is applied to every\n * route rather than only the one that hit it.\n */\n\ninterface Bucket {\n remaining: number\n resetAtMs: number\n}\n\n/** Shared rate-limit state for one Discord credential. */\nexport interface DiscordRateLimiter {\n /** Wait until `route` may be called. */\n acquire(route: string): Promise<void>\n /** Record what a response said about the bucket. */\n update(route: string, headers: { get(name: string): string | null }): void\n /** Apply a 429, globally when Discord says so. */\n penalise(route: string, retryAfterMs: number, global: boolean): void\n}\n\n/** Build the shared rate-limit tracker. */\nexport function createDiscordRateLimiter(\n now: () => number = Date.now,\n sleep: (ms: number) => Promise<void> = realSleep,\n): DiscordRateLimiter {\n const buckets = new Map<string, Bucket>()\n let globalResetAtMs = 0\n\n return {\n async acquire(route: string): Promise<void> {\n const globalWait = globalResetAtMs - now()\n if (globalWait > 0) await sleep(globalWait)\n const bucket = buckets.get(route)\n if (!bucket) return\n if (bucket.remaining > 0) return\n const wait = bucket.resetAtMs - now()\n if (wait > 0) await sleep(wait)\n },\n update(route: string, headers: { get(name: string): string | null }): void {\n const remaining = numericHeader(headers, 'x-ratelimit-remaining')\n const resetAfter = numericHeader(headers, 'x-ratelimit-reset-after')\n if (remaining === null && resetAfter === null) return\n buckets.set(route, {\n remaining: remaining ?? 1,\n resetAtMs: now() + (resetAfter ?? 0) * 1000,\n })\n },\n penalise(route: string, retryAfterMs: number, global: boolean): void {\n const resetAtMs = now() + retryAfterMs\n if (global) globalResetAtMs = Math.max(globalResetAtMs, resetAtMs)\n buckets.set(route, { remaining: 0, resetAtMs })\n },\n }\n}\n\n/** Options shared by both Discord adapters. */\nexport interface DiscordOptionsBase extends NotifierHttpOptions {\n /** Share one limiter across adapters that use the same credential. */\n limiter?: DiscordRateLimiter\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n /** Username override for webhook posts. */\n username?: string\n}\n\nconst PERMANENT_STATUSES = new Set([400, 401, 403, 404, 405])\n\ninterface SendArgs {\n url: string\n route: string\n payload: unknown\n headers: Record<string, string>\n}\n\nasync function deliver(\n args: SendArgs,\n options: DiscordOptionsBase,\n fetchImpl: FetchLike,\n limiter: DiscordRateLimiter,\n): Promise<DeliveryResult> {\n const maxAttempts = options.maxAttempts ?? 4\n const backoffMs = options.backoffMs ?? 500\n const maxWaitMs = options.maxWaitMs ?? 60_000\n const sleep = options.sleep ?? realSleep\n\n let attempt = 0\n let lastStatus: number | null = null\n let lastError = 'no attempt was made'\n let retryable = true\n\n while (attempt < maxAttempts) {\n attempt += 1\n await limiter.acquire(args.route)\n\n let response\n try {\n response = await postJson(fetchImpl, args.url, args.payload, args.headers)\n } catch (error) {\n lastStatus = null\n lastError = error instanceof Error ? error.message : String(error)\n retryable = true\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n continue\n }\n\n limiter.update(args.route, response.headers)\n lastStatus = response.status\n\n if (response.status === 429) {\n const retryAfterField = pick(response.body, 'retry_after')\n const retryAfterSeconds =\n typeof retryAfterField === 'number'\n ? retryAfterField\n : (numericHeader(response.headers, 'retry-after') ?? backoffMs / 1000)\n const isGlobal =\n pick(response.body, 'global') === true ||\n response.headers.get('x-ratelimit-global') === 'true'\n const waitMs = retryAfterSeconds * 1000\n limiter.penalise(args.route, waitMs, isGlobal)\n lastError = `rate limited, retry after ${retryAfterSeconds}s${isGlobal ? ' (global)' : ''}`\n retryable = true\n if (waitMs > maxWaitMs || attempt >= maxAttempts) break\n await sleep(waitMs)\n continue\n }\n\n if (response.ok) {\n // Webhooks answer 204 with an empty body unless `wait=true` was used.\n const messageId = pick(response.body, 'id')\n return delivered(response.status, attempt, messageId === undefined ? null : String(messageId))\n }\n\n const message =\n (typeof pick(response.body, 'message') === 'string'\n ? (pick(response.body, 'message') as string)\n : null) ?? response.raw.slice(0, 200)\n lastError = message || `Discord request failed with status ${response.status}`\n\n if (PERMANENT_STATUSES.has(response.status)) {\n retryable = false\n break\n }\n retryable = true\n if (attempt >= maxAttempts) break\n await sleep(backoffMs * 2 ** (attempt - 1))\n }\n\n return failed(lastStatus, attempt, lastError, retryable)\n}\n\n/** Options for {@link createDiscordWebhookNotifier}. */\nexport interface DiscordWebhookOptions extends DiscordOptionsBase {\n /**\n * Default webhook URL. Optional: `send()` accepts a webhook URL as its\n * target, which is how per-subscription webhooks work.\n */\n webhookUrl?: string\n}\n\nconst WEBHOOK_PATTERN = /^https:\\/\\/(?:\\w+\\.)?discord(?:app)?\\.com\\/api(?:\\/v\\d+)?\\/webhooks\\/\\d+\\/[\\w-]+$/\n\n/**\n * Webhook delivery. No bot account, no gateway, no permissions setup: the\n * subscriber pastes a channel webhook URL and alerts arrive. This is the\n * lowest-friction Discord path and the one most subscriptions use.\n *\n * @example\n * ```ts\n * const discord = createDiscordWebhookNotifier()\n * await discord.send('https://discord.com/api/webhooks/123/abc', renderAlert(event))\n * ```\n */\nexport function createDiscordWebhookNotifier(options: DiscordWebhookOptions = {}): Notifier {\n const fetchImpl = resolveFetch(options.fetch)\n const limiter =\n options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? realSleep)\n\n return {\n platform: 'discord',\n kind: 'webhook',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n const url = target || options.webhookUrl\n if (!url || !WEBHOOK_PATTERN.test(url)) {\n return failed(\n null,\n 0,\n `invalid Discord webhook URL \"${target}\": expected https://discord.com/api/webhooks/<id>/<token>`,\n false,\n )\n }\n return deliver(\n {\n url: `${url}?wait=true`,\n // Discord buckets webhook routes per webhook id.\n route: `webhook:${url.split('/').slice(-2, -1)[0] ?? url}`,\n payload: {\n embeds: [alert.embed],\n ...(options.username ? { username: options.username } : {}),\n },\n headers: {},\n },\n options,\n fetchImpl,\n limiter,\n )\n },\n }\n}\n\n/** Options for {@link createDiscordBotNotifier}. */\nexport interface DiscordBotOptions extends DiscordOptionsBase {\n /** Bot token from the Discord developer portal (no `Bot ` prefix). */\n botToken: string\n /** API base. @defaultValue `https://discord.com/api/v10` */\n apiBase?: string\n}\n\n/**\n * Bot delivery: posts to a channel id with the application's bot token, which\n * is what slash-command subscriptions use (the bot is already in the guild, so\n * no webhook needs creating).\n *\n * @example\n * ```ts\n * const discord = createDiscordBotNotifier({ botToken: process.env.DISCORD_BOT_TOKEN! })\n * await discord.send('1234567890', renderAlert(event))\n * ```\n */\nexport function createDiscordBotNotifier(options: DiscordBotOptions): Notifier {\n if (!options.botToken.trim()) {\n throw new Error('hood-alerts: DISCORD_BOT_TOKEN is required for the Discord bot notifier')\n }\n const fetchImpl = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://discord.com/api/v10').replace(/\\/+$/, '')\n const limiter =\n options.limiter ?? createDiscordRateLimiter(options.now ?? Date.now, options.sleep ?? realSleep)\n\n return {\n platform: 'discord',\n kind: 'bot',\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n if (!/^\\d{5,25}$/.test(target)) {\n return failed(null, 0, `invalid Discord channel id \"${target}\": expected a numeric snowflake`, false)\n }\n return deliver(\n {\n url: `${apiBase}/channels/${target}/messages`,\n route: `channel:${target}`,\n payload: { embeds: [alert.embed] },\n headers: { authorization: `Bot ${options.botToken}` },\n },\n options,\n fetchImpl,\n limiter,\n )\n },\n }\n}\n","import type { Platform } from '../rules/schema.js'\nimport { delivered, type DeliveryResult, type Notifier, type RenderedAlert } from './types.js'\n\n/** One captured delivery. */\nexport interface CapturedDelivery {\n platform: Platform\n target: string\n alert: RenderedAlert\n atMs: number\n}\n\n/** A notifier that records deliveries instead of sending them. */\nexport interface CaptureNotifier extends Notifier {\n /** Everything captured so far, in order. */\n readonly sent: readonly CapturedDelivery[]\n /** Drop the captured history. */\n clear(): void\n}\n\n/**\n * A delivery adapter that records instead of sending.\n *\n * This is a first-class operating mode, not a stub: running the service with\n * `DRY_RUN=1` swaps the real adapters for this one, so an operator can point a\n * fresh deployment at mainnet and watch exactly which alerts *would* have gone\n * out, with the real rendered text, before handing it bot tokens. It is also\n * what the notifier tests assert against.\n *\n * @example\n * ```ts\n * const capture = createCaptureNotifier('telegram')\n * await capture.send('-1001234567890', renderAlert(event))\n * console.log(capture.sent[0]?.alert.text)\n * ```\n */\nexport function createCaptureNotifier(\n platform: Platform,\n onSend?: (delivery: CapturedDelivery) => void,\n now: () => number = Date.now,\n): CaptureNotifier {\n const sent: CapturedDelivery[] = []\n return {\n platform,\n kind: 'capture',\n sent,\n clear(): void {\n sent.length = 0\n },\n async send(target: string, alert: RenderedAlert): Promise<DeliveryResult> {\n const delivery: CapturedDelivery = { platform, target, alert, atMs: now() }\n sent.push(delivery)\n onSend?.(delivery)\n return delivered(200, 1, `capture-${sent.length}`)\n },\n }\n}\n"],"mappings":"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","names":["sleep","sleep","PERMANENT_STATUSES"]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export { E as EMBED_COLORS, R as RenderOptions, d as detailRows, f as formatAmount, a as formatEth, b as formatUsd, h as headline, r as renderAlert } from '../format-B7d40f65.cjs';
|
|
2
|
+
export { T as TelegramClient, a as TelegramClientOptions, b as TelegramNotifierOptions, c as TelegramResponse, d as createTelegramClient, e as createTelegramNotifier } from '../telegram-CpodYJrP.cjs';
|
|
3
|
+
import { d as NotifierHttpOptions, N as Notifier, R as RenderedAlert, F as FetchLike } from '../types-CcoKpStA.cjs';
|
|
4
|
+
export { D as DeliveryResult, a as DiscordEmbed, b as DiscordEmbedField, c as FetchLikeResponse, e as delivered, f as failed } from '../types-CcoKpStA.cjs';
|
|
5
|
+
import { P as Platform } from '../schema-NnIdKFl2.cjs';
|
|
6
|
+
import '../types-DFPpTMWo.cjs';
|
|
7
|
+
import 'viem';
|
|
8
|
+
import 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escaping. The single most common way an alert bot breaks in production.
|
|
12
|
+
*
|
|
13
|
+
* Memecoin names are adversarial input by nature. A token called `WHO_LET_THE`
|
|
14
|
+
* silently italicises half a Telegram message under MarkdownV2 and a token
|
|
15
|
+
* called `<b>RUG` injects markup under HTML parse mode; either way Telegram
|
|
16
|
+
* answers `400 Bad Request: can't parse entities` and the alert is lost. The
|
|
17
|
+
* rules are not symmetric between platforms or even between contexts within a
|
|
18
|
+
* platform, so each context gets its own function and its own tests.
|
|
19
|
+
*/
|
|
20
|
+
/** Escape literal text for Telegram MarkdownV2. */
|
|
21
|
+
declare function escapeMarkdownV2(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Escape the URL inside a MarkdownV2 inline link `[label](url)`.
|
|
24
|
+
* Inside the parentheses only `)` and `\` are special, and escaping the full
|
|
25
|
+
* reserved set here would corrupt query strings and path separators.
|
|
26
|
+
*/
|
|
27
|
+
declare function escapeMarkdownV2Url(url: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Escape text inside a MarkdownV2 code span or `pre` block. Only `` ` `` and
|
|
30
|
+
* `\` are special there. Escaping the full set would print literal backslashes
|
|
31
|
+
* to the user, which is the classic over-correction of this bug.
|
|
32
|
+
*/
|
|
33
|
+
declare function escapeMarkdownV2Code(text: string): string;
|
|
34
|
+
/** Build a MarkdownV2 inline link with both halves escaped correctly. */
|
|
35
|
+
declare function markdownV2Link(label: string, url: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Escape text for Telegram HTML parse mode. Telegram's HTML is a small subset
|
|
38
|
+
* and requires exactly `&`, `<` and `>` to be replaced in text nodes.
|
|
39
|
+
*/
|
|
40
|
+
declare function escapeHtml(text: string): string;
|
|
41
|
+
/** Escape a value going into an HTML attribute (a link `href`). */
|
|
42
|
+
declare function escapeHtmlAttribute(value: string): string;
|
|
43
|
+
/** Build a Telegram HTML anchor with both halves escaped. */
|
|
44
|
+
declare function htmlLink(label: string, url: string): string;
|
|
45
|
+
/** Escape literal text for Discord markdown rendering. */
|
|
46
|
+
declare function escapeDiscordMarkdown(text: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Truncate to a maximum length without splitting a surrogate pair, which is
|
|
49
|
+
* what turns a long emoji-bearing token name into a replacement character.
|
|
50
|
+
* The ellipsis is included in the budget.
|
|
51
|
+
*/
|
|
52
|
+
declare function truncate(text: string, max: number): string;
|
|
53
|
+
/**
|
|
54
|
+
* A display label for a token: its symbol when the contract exposed one,
|
|
55
|
+
* otherwise a shortened address. Never returns an empty string, because an
|
|
56
|
+
* empty Telegram entity is a 400.
|
|
57
|
+
*/
|
|
58
|
+
declare function displayLabel(symbol: string | null, address: string): string;
|
|
59
|
+
/** `0x1234…abcd`, the standard compact address rendering. */
|
|
60
|
+
declare function shortAddress(address: string): string;
|
|
61
|
+
|
|
62
|
+
/** Shared rate-limit state for one Discord credential. */
|
|
63
|
+
interface DiscordRateLimiter {
|
|
64
|
+
/** Wait until `route` may be called. */
|
|
65
|
+
acquire(route: string): Promise<void>;
|
|
66
|
+
/** Record what a response said about the bucket. */
|
|
67
|
+
update(route: string, headers: {
|
|
68
|
+
get(name: string): string | null;
|
|
69
|
+
}): void;
|
|
70
|
+
/** Apply a 429, globally when Discord says so. */
|
|
71
|
+
penalise(route: string, retryAfterMs: number, global: boolean): void;
|
|
72
|
+
}
|
|
73
|
+
/** Build the shared rate-limit tracker. */
|
|
74
|
+
declare function createDiscordRateLimiter(now?: () => number, sleep?: (ms: number) => Promise<void>): DiscordRateLimiter;
|
|
75
|
+
/** Options shared by both Discord adapters. */
|
|
76
|
+
interface DiscordOptionsBase extends NotifierHttpOptions {
|
|
77
|
+
/** Share one limiter across adapters that use the same credential. */
|
|
78
|
+
limiter?: DiscordRateLimiter;
|
|
79
|
+
/** Clock injection point for tests. @defaultValue `Date.now` */
|
|
80
|
+
now?: () => number;
|
|
81
|
+
/** Username override for webhook posts. */
|
|
82
|
+
username?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Options for {@link createDiscordWebhookNotifier}. */
|
|
85
|
+
interface DiscordWebhookOptions extends DiscordOptionsBase {
|
|
86
|
+
/**
|
|
87
|
+
* Default webhook URL. Optional: `send()` accepts a webhook URL as its
|
|
88
|
+
* target, which is how per-subscription webhooks work.
|
|
89
|
+
*/
|
|
90
|
+
webhookUrl?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Webhook delivery. No bot account, no gateway, no permissions setup: the
|
|
94
|
+
* subscriber pastes a channel webhook URL and alerts arrive. This is the
|
|
95
|
+
* lowest-friction Discord path and the one most subscriptions use.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const discord = createDiscordWebhookNotifier()
|
|
100
|
+
* await discord.send('https://discord.com/api/webhooks/123/abc', renderAlert(event))
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
declare function createDiscordWebhookNotifier(options?: DiscordWebhookOptions): Notifier;
|
|
104
|
+
/** Options for {@link createDiscordBotNotifier}. */
|
|
105
|
+
interface DiscordBotOptions extends DiscordOptionsBase {
|
|
106
|
+
/** Bot token from the Discord developer portal (no `Bot ` prefix). */
|
|
107
|
+
botToken: string;
|
|
108
|
+
/** API base. @defaultValue `https://discord.com/api/v10` */
|
|
109
|
+
apiBase?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Bot delivery: posts to a channel id with the application's bot token, which
|
|
113
|
+
* is what slash-command subscriptions use (the bot is already in the guild, so
|
|
114
|
+
* no webhook needs creating).
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const discord = createDiscordBotNotifier({ botToken: process.env.DISCORD_BOT_TOKEN! })
|
|
119
|
+
* await discord.send('1234567890', renderAlert(event))
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function createDiscordBotNotifier(options: DiscordBotOptions): Notifier;
|
|
123
|
+
|
|
124
|
+
/** One captured delivery. */
|
|
125
|
+
interface CapturedDelivery {
|
|
126
|
+
platform: Platform;
|
|
127
|
+
target: string;
|
|
128
|
+
alert: RenderedAlert;
|
|
129
|
+
atMs: number;
|
|
130
|
+
}
|
|
131
|
+
/** A notifier that records deliveries instead of sending them. */
|
|
132
|
+
interface CaptureNotifier extends Notifier {
|
|
133
|
+
/** Everything captured so far, in order. */
|
|
134
|
+
readonly sent: readonly CapturedDelivery[];
|
|
135
|
+
/** Drop the captured history. */
|
|
136
|
+
clear(): void;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A delivery adapter that records instead of sending.
|
|
140
|
+
*
|
|
141
|
+
* This is a first-class operating mode, not a stub: running the service with
|
|
142
|
+
* `DRY_RUN=1` swaps the real adapters for this one, so an operator can point a
|
|
143
|
+
* fresh deployment at mainnet and watch exactly which alerts *would* have gone
|
|
144
|
+
* out, with the real rendered text, before handing it bot tokens. It is also
|
|
145
|
+
* what the notifier tests assert against.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const capture = createCaptureNotifier('telegram')
|
|
150
|
+
* await capture.send('-1001234567890', renderAlert(event))
|
|
151
|
+
* console.log(capture.sent[0]?.alert.text)
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function createCaptureNotifier(platform: Platform, onSend?: (delivery: CapturedDelivery) => void, now?: () => number): CaptureNotifier;
|
|
155
|
+
|
|
156
|
+
/** A completed HTTP response with its body parsed as JSON when possible. */
|
|
157
|
+
interface JsonResponse {
|
|
158
|
+
status: number;
|
|
159
|
+
ok: boolean;
|
|
160
|
+
headers: {
|
|
161
|
+
get(name: string): string | null;
|
|
162
|
+
};
|
|
163
|
+
body: unknown;
|
|
164
|
+
raw: string;
|
|
165
|
+
}
|
|
166
|
+
/** Default sleep. */
|
|
167
|
+
declare const sleep: (ms: number) => Promise<void>;
|
|
168
|
+
/** Resolve the `fetch` to use: the injected double in tests, the global otherwise. */
|
|
169
|
+
declare function resolveFetch(injected?: FetchLike): FetchLike;
|
|
170
|
+
/**
|
|
171
|
+
* POST JSON and parse the response.
|
|
172
|
+
*
|
|
173
|
+
* A non-JSON body (an HTML error page from a proxy, an empty 204) is not an
|
|
174
|
+
* error here: `body` becomes `null` and `raw` keeps the text, so callers can
|
|
175
|
+
* report what actually came back instead of a `SyntaxError` from deep inside
|
|
176
|
+
* the adapter.
|
|
177
|
+
*/
|
|
178
|
+
declare function postJson(fetchImpl: FetchLike, url: string, payload: unknown, headers?: Record<string, string>): Promise<JsonResponse>;
|
|
179
|
+
/** Read a numeric header, or `null` when absent or unparseable. */
|
|
180
|
+
declare function numericHeader(headers: {
|
|
181
|
+
get(name: string): string | null;
|
|
182
|
+
}, name: string): number | null;
|
|
183
|
+
/** Read a nested property from an unknown JSON body without casting blindly. */
|
|
184
|
+
declare function pick(body: unknown, ...path: string[]): unknown;
|
|
185
|
+
|
|
186
|
+
export { type CaptureNotifier, type CapturedDelivery, type DiscordBotOptions, type DiscordOptionsBase, type DiscordRateLimiter, type DiscordWebhookOptions, FetchLike, type JsonResponse, Notifier, NotifierHttpOptions, RenderedAlert, createCaptureNotifier, createDiscordBotNotifier, createDiscordRateLimiter, createDiscordWebhookNotifier, displayLabel, escapeDiscordMarkdown, escapeHtml, escapeHtmlAttribute, escapeMarkdownV2, escapeMarkdownV2Code, escapeMarkdownV2Url, htmlLink, markdownV2Link, numericHeader, pick, postJson, resolveFetch, shortAddress, sleep, truncate };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export { E as EMBED_COLORS, R as RenderOptions, d as detailRows, f as formatAmount, a as formatEth, b as formatUsd, h as headline, r as renderAlert } from '../format-6RG4OcyE.js';
|
|
2
|
+
export { T as TelegramClient, a as TelegramClientOptions, b as TelegramNotifierOptions, c as TelegramResponse, d as createTelegramClient, e as createTelegramNotifier } from '../telegram-pvphmBwn.js';
|
|
3
|
+
import { d as NotifierHttpOptions, N as Notifier, R as RenderedAlert, F as FetchLike } from '../types-C0ETog04.js';
|
|
4
|
+
export { D as DeliveryResult, a as DiscordEmbed, b as DiscordEmbedField, c as FetchLikeResponse, e as delivered, f as failed } from '../types-C0ETog04.js';
|
|
5
|
+
import { P as Platform } from '../schema-DuDMPqpq.js';
|
|
6
|
+
import '../types-DFPpTMWo.js';
|
|
7
|
+
import 'viem';
|
|
8
|
+
import 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escaping. The single most common way an alert bot breaks in production.
|
|
12
|
+
*
|
|
13
|
+
* Memecoin names are adversarial input by nature. A token called `WHO_LET_THE`
|
|
14
|
+
* silently italicises half a Telegram message under MarkdownV2 and a token
|
|
15
|
+
* called `<b>RUG` injects markup under HTML parse mode; either way Telegram
|
|
16
|
+
* answers `400 Bad Request: can't parse entities` and the alert is lost. The
|
|
17
|
+
* rules are not symmetric between platforms or even between contexts within a
|
|
18
|
+
* platform, so each context gets its own function and its own tests.
|
|
19
|
+
*/
|
|
20
|
+
/** Escape literal text for Telegram MarkdownV2. */
|
|
21
|
+
declare function escapeMarkdownV2(text: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Escape the URL inside a MarkdownV2 inline link `[label](url)`.
|
|
24
|
+
* Inside the parentheses only `)` and `\` are special, and escaping the full
|
|
25
|
+
* reserved set here would corrupt query strings and path separators.
|
|
26
|
+
*/
|
|
27
|
+
declare function escapeMarkdownV2Url(url: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Escape text inside a MarkdownV2 code span or `pre` block. Only `` ` `` and
|
|
30
|
+
* `\` are special there. Escaping the full set would print literal backslashes
|
|
31
|
+
* to the user, which is the classic over-correction of this bug.
|
|
32
|
+
*/
|
|
33
|
+
declare function escapeMarkdownV2Code(text: string): string;
|
|
34
|
+
/** Build a MarkdownV2 inline link with both halves escaped correctly. */
|
|
35
|
+
declare function markdownV2Link(label: string, url: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Escape text for Telegram HTML parse mode. Telegram's HTML is a small subset
|
|
38
|
+
* and requires exactly `&`, `<` and `>` to be replaced in text nodes.
|
|
39
|
+
*/
|
|
40
|
+
declare function escapeHtml(text: string): string;
|
|
41
|
+
/** Escape a value going into an HTML attribute (a link `href`). */
|
|
42
|
+
declare function escapeHtmlAttribute(value: string): string;
|
|
43
|
+
/** Build a Telegram HTML anchor with both halves escaped. */
|
|
44
|
+
declare function htmlLink(label: string, url: string): string;
|
|
45
|
+
/** Escape literal text for Discord markdown rendering. */
|
|
46
|
+
declare function escapeDiscordMarkdown(text: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Truncate to a maximum length without splitting a surrogate pair, which is
|
|
49
|
+
* what turns a long emoji-bearing token name into a replacement character.
|
|
50
|
+
* The ellipsis is included in the budget.
|
|
51
|
+
*/
|
|
52
|
+
declare function truncate(text: string, max: number): string;
|
|
53
|
+
/**
|
|
54
|
+
* A display label for a token: its symbol when the contract exposed one,
|
|
55
|
+
* otherwise a shortened address. Never returns an empty string, because an
|
|
56
|
+
* empty Telegram entity is a 400.
|
|
57
|
+
*/
|
|
58
|
+
declare function displayLabel(symbol: string | null, address: string): string;
|
|
59
|
+
/** `0x1234…abcd`, the standard compact address rendering. */
|
|
60
|
+
declare function shortAddress(address: string): string;
|
|
61
|
+
|
|
62
|
+
/** Shared rate-limit state for one Discord credential. */
|
|
63
|
+
interface DiscordRateLimiter {
|
|
64
|
+
/** Wait until `route` may be called. */
|
|
65
|
+
acquire(route: string): Promise<void>;
|
|
66
|
+
/** Record what a response said about the bucket. */
|
|
67
|
+
update(route: string, headers: {
|
|
68
|
+
get(name: string): string | null;
|
|
69
|
+
}): void;
|
|
70
|
+
/** Apply a 429, globally when Discord says so. */
|
|
71
|
+
penalise(route: string, retryAfterMs: number, global: boolean): void;
|
|
72
|
+
}
|
|
73
|
+
/** Build the shared rate-limit tracker. */
|
|
74
|
+
declare function createDiscordRateLimiter(now?: () => number, sleep?: (ms: number) => Promise<void>): DiscordRateLimiter;
|
|
75
|
+
/** Options shared by both Discord adapters. */
|
|
76
|
+
interface DiscordOptionsBase extends NotifierHttpOptions {
|
|
77
|
+
/** Share one limiter across adapters that use the same credential. */
|
|
78
|
+
limiter?: DiscordRateLimiter;
|
|
79
|
+
/** Clock injection point for tests. @defaultValue `Date.now` */
|
|
80
|
+
now?: () => number;
|
|
81
|
+
/** Username override for webhook posts. */
|
|
82
|
+
username?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Options for {@link createDiscordWebhookNotifier}. */
|
|
85
|
+
interface DiscordWebhookOptions extends DiscordOptionsBase {
|
|
86
|
+
/**
|
|
87
|
+
* Default webhook URL. Optional: `send()` accepts a webhook URL as its
|
|
88
|
+
* target, which is how per-subscription webhooks work.
|
|
89
|
+
*/
|
|
90
|
+
webhookUrl?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Webhook delivery. No bot account, no gateway, no permissions setup: the
|
|
94
|
+
* subscriber pastes a channel webhook URL and alerts arrive. This is the
|
|
95
|
+
* lowest-friction Discord path and the one most subscriptions use.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* const discord = createDiscordWebhookNotifier()
|
|
100
|
+
* await discord.send('https://discord.com/api/webhooks/123/abc', renderAlert(event))
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
declare function createDiscordWebhookNotifier(options?: DiscordWebhookOptions): Notifier;
|
|
104
|
+
/** Options for {@link createDiscordBotNotifier}. */
|
|
105
|
+
interface DiscordBotOptions extends DiscordOptionsBase {
|
|
106
|
+
/** Bot token from the Discord developer portal (no `Bot ` prefix). */
|
|
107
|
+
botToken: string;
|
|
108
|
+
/** API base. @defaultValue `https://discord.com/api/v10` */
|
|
109
|
+
apiBase?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Bot delivery: posts to a channel id with the application's bot token, which
|
|
113
|
+
* is what slash-command subscriptions use (the bot is already in the guild, so
|
|
114
|
+
* no webhook needs creating).
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const discord = createDiscordBotNotifier({ botToken: process.env.DISCORD_BOT_TOKEN! })
|
|
119
|
+
* await discord.send('1234567890', renderAlert(event))
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function createDiscordBotNotifier(options: DiscordBotOptions): Notifier;
|
|
123
|
+
|
|
124
|
+
/** One captured delivery. */
|
|
125
|
+
interface CapturedDelivery {
|
|
126
|
+
platform: Platform;
|
|
127
|
+
target: string;
|
|
128
|
+
alert: RenderedAlert;
|
|
129
|
+
atMs: number;
|
|
130
|
+
}
|
|
131
|
+
/** A notifier that records deliveries instead of sending them. */
|
|
132
|
+
interface CaptureNotifier extends Notifier {
|
|
133
|
+
/** Everything captured so far, in order. */
|
|
134
|
+
readonly sent: readonly CapturedDelivery[];
|
|
135
|
+
/** Drop the captured history. */
|
|
136
|
+
clear(): void;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A delivery adapter that records instead of sending.
|
|
140
|
+
*
|
|
141
|
+
* This is a first-class operating mode, not a stub: running the service with
|
|
142
|
+
* `DRY_RUN=1` swaps the real adapters for this one, so an operator can point a
|
|
143
|
+
* fresh deployment at mainnet and watch exactly which alerts *would* have gone
|
|
144
|
+
* out, with the real rendered text, before handing it bot tokens. It is also
|
|
145
|
+
* what the notifier tests assert against.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const capture = createCaptureNotifier('telegram')
|
|
150
|
+
* await capture.send('-1001234567890', renderAlert(event))
|
|
151
|
+
* console.log(capture.sent[0]?.alert.text)
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function createCaptureNotifier(platform: Platform, onSend?: (delivery: CapturedDelivery) => void, now?: () => number): CaptureNotifier;
|
|
155
|
+
|
|
156
|
+
/** A completed HTTP response with its body parsed as JSON when possible. */
|
|
157
|
+
interface JsonResponse {
|
|
158
|
+
status: number;
|
|
159
|
+
ok: boolean;
|
|
160
|
+
headers: {
|
|
161
|
+
get(name: string): string | null;
|
|
162
|
+
};
|
|
163
|
+
body: unknown;
|
|
164
|
+
raw: string;
|
|
165
|
+
}
|
|
166
|
+
/** Default sleep. */
|
|
167
|
+
declare const sleep: (ms: number) => Promise<void>;
|
|
168
|
+
/** Resolve the `fetch` to use: the injected double in tests, the global otherwise. */
|
|
169
|
+
declare function resolveFetch(injected?: FetchLike): FetchLike;
|
|
170
|
+
/**
|
|
171
|
+
* POST JSON and parse the response.
|
|
172
|
+
*
|
|
173
|
+
* A non-JSON body (an HTML error page from a proxy, an empty 204) is not an
|
|
174
|
+
* error here: `body` becomes `null` and `raw` keeps the text, so callers can
|
|
175
|
+
* report what actually came back instead of a `SyntaxError` from deep inside
|
|
176
|
+
* the adapter.
|
|
177
|
+
*/
|
|
178
|
+
declare function postJson(fetchImpl: FetchLike, url: string, payload: unknown, headers?: Record<string, string>): Promise<JsonResponse>;
|
|
179
|
+
/** Read a numeric header, or `null` when absent or unparseable. */
|
|
180
|
+
declare function numericHeader(headers: {
|
|
181
|
+
get(name: string): string | null;
|
|
182
|
+
}, name: string): number | null;
|
|
183
|
+
/** Read a nested property from an unknown JSON body without casting blindly. */
|
|
184
|
+
declare function pick(body: unknown, ...path: string[]): unknown;
|
|
185
|
+
|
|
186
|
+
export { type CaptureNotifier, type CapturedDelivery, type DiscordBotOptions, type DiscordOptionsBase, type DiscordRateLimiter, type DiscordWebhookOptions, FetchLike, type JsonResponse, Notifier, NotifierHttpOptions, RenderedAlert, createCaptureNotifier, createDiscordBotNotifier, createDiscordRateLimiter, createDiscordWebhookNotifier, displayLabel, escapeDiscordMarkdown, escapeHtml, escapeHtmlAttribute, escapeMarkdownV2, escapeMarkdownV2Code, escapeMarkdownV2Url, htmlLink, markdownV2Link, numericHeader, pick, postJson, resolveFetch, shortAddress, sleep, truncate };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import "../chunk-CIURPPOW.js";
|
|
2
|
+
import {
|
|
3
|
+
EMBED_COLORS,
|
|
4
|
+
createCaptureNotifier,
|
|
5
|
+
createDiscordBotNotifier,
|
|
6
|
+
createDiscordRateLimiter,
|
|
7
|
+
createDiscordWebhookNotifier,
|
|
8
|
+
detailRows,
|
|
9
|
+
formatAmount,
|
|
10
|
+
formatEth,
|
|
11
|
+
formatUsd,
|
|
12
|
+
headline,
|
|
13
|
+
renderAlert
|
|
14
|
+
} from "../chunk-SOBZ2FPP.js";
|
|
15
|
+
import {
|
|
16
|
+
createTelegramClient,
|
|
17
|
+
createTelegramNotifier,
|
|
18
|
+
delivered,
|
|
19
|
+
displayLabel,
|
|
20
|
+
escapeDiscordMarkdown,
|
|
21
|
+
escapeHtml,
|
|
22
|
+
escapeHtmlAttribute,
|
|
23
|
+
escapeMarkdownV2,
|
|
24
|
+
escapeMarkdownV2Code,
|
|
25
|
+
escapeMarkdownV2Url,
|
|
26
|
+
failed,
|
|
27
|
+
htmlLink,
|
|
28
|
+
markdownV2Link,
|
|
29
|
+
numericHeader,
|
|
30
|
+
pick,
|
|
31
|
+
postJson,
|
|
32
|
+
resolveFetch,
|
|
33
|
+
shortAddress,
|
|
34
|
+
sleep,
|
|
35
|
+
truncate
|
|
36
|
+
} from "../chunk-NBMINJ5E.js";
|
|
37
|
+
export {
|
|
38
|
+
EMBED_COLORS,
|
|
39
|
+
createCaptureNotifier,
|
|
40
|
+
createDiscordBotNotifier,
|
|
41
|
+
createDiscordRateLimiter,
|
|
42
|
+
createDiscordWebhookNotifier,
|
|
43
|
+
createTelegramClient,
|
|
44
|
+
createTelegramNotifier,
|
|
45
|
+
delivered,
|
|
46
|
+
detailRows,
|
|
47
|
+
displayLabel,
|
|
48
|
+
escapeDiscordMarkdown,
|
|
49
|
+
escapeHtml,
|
|
50
|
+
escapeHtmlAttribute,
|
|
51
|
+
escapeMarkdownV2,
|
|
52
|
+
escapeMarkdownV2Code,
|
|
53
|
+
escapeMarkdownV2Url,
|
|
54
|
+
failed,
|
|
55
|
+
formatAmount,
|
|
56
|
+
formatEth,
|
|
57
|
+
formatUsd,
|
|
58
|
+
headline,
|
|
59
|
+
htmlLink,
|
|
60
|
+
markdownV2Link,
|
|
61
|
+
numericHeader,
|
|
62
|
+
pick,
|
|
63
|
+
postJson,
|
|
64
|
+
renderAlert,
|
|
65
|
+
resolveFetch,
|
|
66
|
+
shortAddress,
|
|
67
|
+
sleep,
|
|
68
|
+
truncate
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Address } from 'viem';
|
|
2
|
+
import { HoodClient } from 'hoodchain';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* USD valuation for alert events.
|
|
6
|
+
*
|
|
7
|
+
* There is no hardcoded price anywhere in this package. Every dollar figure
|
|
8
|
+
* comes from live chain state:
|
|
9
|
+
*
|
|
10
|
+
* - **USDG legs are the unit of account.** USDG is Paxos' Global Dollar, a
|
|
11
|
+
* fully reserved 1:1 USD stablecoin with 6 decimals. A USDG leg of a trade
|
|
12
|
+
* is its own USD value, no oracle needed.
|
|
13
|
+
* - **ETH legs are priced through the chain's own Uniswap v3 liquidity.** The
|
|
14
|
+
* oracle asks `hoodchain`'s `quoteSwap` for the real output of selling 1
|
|
15
|
+
* WETH into USDG across every fee tier and two-hop route, and takes the best
|
|
16
|
+
* route's output as the ETH/USD rate. That is the price a trader would
|
|
17
|
+
* actually get, sourced from the same pools the alerts watch.
|
|
18
|
+
* - **Anything else is `null`.** A memecoin/memecoin pool with no USDG or WETH
|
|
19
|
+
* leg has no honest USD value from on-chain data alone, so the event carries
|
|
20
|
+
* `usdValue: null` and min-USD rules simply do not match it.
|
|
21
|
+
*
|
|
22
|
+
* Rates are cached for a short TTL so a burst of events in one block does not
|
|
23
|
+
* re-quote the router once per event.
|
|
24
|
+
*/
|
|
25
|
+
/** A source of USD rates for the two priceable legs. */
|
|
26
|
+
interface PriceOracle {
|
|
27
|
+
/**
|
|
28
|
+
* USD per 1 ETH, or `null` when no WETH/USDG route has liquidity right now.
|
|
29
|
+
* Never throws: a quote failure degrades to `null`, which downgrades USD
|
|
30
|
+
* filters rather than dropping the alert pipeline.
|
|
31
|
+
*/
|
|
32
|
+
ethUsd(): Promise<number | null>;
|
|
33
|
+
/** Convert a raw wei amount to USD, or `null` when the rate is unavailable. */
|
|
34
|
+
weiToUsd(wei: bigint): Promise<number | null>;
|
|
35
|
+
/** Convert a raw USDG amount (6 decimals) to USD. Always available. */
|
|
36
|
+
usdgToUsd(atomic: bigint): number;
|
|
37
|
+
}
|
|
38
|
+
/** Options for {@link createPriceOracle}. */
|
|
39
|
+
interface PriceOracleOptions {
|
|
40
|
+
/**
|
|
41
|
+
* How long a quoted ETH/USD rate stays fresh, in milliseconds.
|
|
42
|
+
* @defaultValue `30_000`
|
|
43
|
+
*/
|
|
44
|
+
ttlMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Probe size used to quote WETH into USDG. Larger sizes are less sensitive
|
|
47
|
+
* to a thin tick but need deeper liquidity to fill.
|
|
48
|
+
* @defaultValue `1` ETH
|
|
49
|
+
*/
|
|
50
|
+
probeWei?: bigint;
|
|
51
|
+
/** Clock injection point for tests. @defaultValue `Date.now` */
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
54
|
+
/** WETH and USDG for the client's network. */
|
|
55
|
+
declare function quoteTokens(client: HoodClient): {
|
|
56
|
+
weth: Address;
|
|
57
|
+
usdg: Address;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Build a caching ETH/USD oracle backed by real Uniswap v3 quotes.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { createHoodClient } from 'hoodchain'
|
|
65
|
+
* import { createPriceOracle } from 'hood-alerts/events'
|
|
66
|
+
*
|
|
67
|
+
* const hood = createHoodClient()
|
|
68
|
+
* const oracle = createPriceOracle(hood)
|
|
69
|
+
* console.log('ETH/USD from chain liquidity:', await oracle.ethUsd())
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare function createPriceOracle(client: HoodClient, options?: PriceOracleOptions): PriceOracle;
|
|
73
|
+
/**
|
|
74
|
+
* A fixed-rate oracle. Useful when an operator already has a trusted ETH/USD
|
|
75
|
+
* feed and wants alerts to agree with it, and for deterministic tests.
|
|
76
|
+
*/
|
|
77
|
+
declare function createStaticPriceOracle(ethUsdRate: number | null): PriceOracle;
|
|
78
|
+
|
|
79
|
+
export { type PriceOracle as P, type PriceOracleOptions as a, createStaticPriceOracle as b, createPriceOracle as c, quoteTokens as q };
|