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.
Files changed (107) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +369 -0
  3. package/dist/bot/index.cjs +1008 -0
  4. package/dist/bot/index.cjs.map +1 -0
  5. package/dist/bot/index.d.cts +241 -0
  6. package/dist/bot/index.d.ts +241 -0
  7. package/dist/bot/index.js +30 -0
  8. package/dist/bot/index.js.map +1 -0
  9. package/dist/chunk-32GS6XVE.js +732 -0
  10. package/dist/chunk-32GS6XVE.js.map +1 -0
  11. package/dist/chunk-3LHCQA4Z.js +76 -0
  12. package/dist/chunk-3LHCQA4Z.js.map +1 -0
  13. package/dist/chunk-3NHLSKZE.js +1 -0
  14. package/dist/chunk-3NHLSKZE.js.map +1 -0
  15. package/dist/chunk-6JQK5G3Z.js +72 -0
  16. package/dist/chunk-6JQK5G3Z.js.map +1 -0
  17. package/dist/chunk-CBNZ6AZT.js +598 -0
  18. package/dist/chunk-CBNZ6AZT.js.map +1 -0
  19. package/dist/chunk-CIURPPOW.js +1 -0
  20. package/dist/chunk-CIURPPOW.js.map +1 -0
  21. package/dist/chunk-IUOEGBF6.js +49 -0
  22. package/dist/chunk-IUOEGBF6.js.map +1 -0
  23. package/dist/chunk-NBMINJ5E.js +230 -0
  24. package/dist/chunk-NBMINJ5E.js.map +1 -0
  25. package/dist/chunk-O2RY2SWN.js +1 -0
  26. package/dist/chunk-O2RY2SWN.js.map +1 -0
  27. package/dist/chunk-PLMVKTQM.js +1 -0
  28. package/dist/chunk-PLMVKTQM.js.map +1 -0
  29. package/dist/chunk-RSTLW7XH.js +130 -0
  30. package/dist/chunk-RSTLW7XH.js.map +1 -0
  31. package/dist/chunk-SOBZ2FPP.js +355 -0
  32. package/dist/chunk-SOBZ2FPP.js.map +1 -0
  33. package/dist/chunk-VC5MKEY2.js +170 -0
  34. package/dist/chunk-VC5MKEY2.js.map +1 -0
  35. package/dist/chunk-W2GUAWUR.js +147 -0
  36. package/dist/chunk-W2GUAWUR.js.map +1 -0
  37. package/dist/chunk-Y2M3USWZ.js +355 -0
  38. package/dist/chunk-Y2M3USWZ.js.map +1 -0
  39. package/dist/chunk-Z4DJPESQ.js +1216 -0
  40. package/dist/chunk-Z4DJPESQ.js.map +1 -0
  41. package/dist/chunk-ZDM5VNWB.js +1 -0
  42. package/dist/chunk-ZDM5VNWB.js.map +1 -0
  43. package/dist/events/index.cjs +820 -0
  44. package/dist/events/index.cjs.map +1 -0
  45. package/dist/events/index.d.cts +58 -0
  46. package/dist/events/index.d.ts +58 -0
  47. package/dist/events/index.js +69 -0
  48. package/dist/events/index.js.map +1 -0
  49. package/dist/format-6RG4OcyE.d.ts +57 -0
  50. package/dist/format-B7d40f65.d.cts +57 -0
  51. package/dist/index.cjs +4122 -0
  52. package/dist/index.cjs.map +1 -0
  53. package/dist/index.d.cts +21 -0
  54. package/dist/index.d.ts +21 -0
  55. package/dist/index.js +274 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/logger-3phvV_fl.d.cts +185 -0
  58. package/dist/logger-CYza_0eU.d.ts +185 -0
  59. package/dist/logs-D7sYBvSy.d.cts +66 -0
  60. package/dist/logs-D7sYBvSy.d.ts +66 -0
  61. package/dist/notifiers/index.cjs +620 -0
  62. package/dist/notifiers/index.cjs.map +1 -0
  63. package/dist/notifiers/index.d.cts +186 -0
  64. package/dist/notifiers/index.d.ts +186 -0
  65. package/dist/notifiers/index.js +70 -0
  66. package/dist/notifiers/index.js.map +1 -0
  67. package/dist/pricing-BumgLpvB.d.cts +79 -0
  68. package/dist/pricing-BumgLpvB.d.ts +79 -0
  69. package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
  70. package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
  71. package/dist/registry-D3We8Lg6.d.ts +50 -0
  72. package/dist/registry-DePA3IyS.d.cts +50 -0
  73. package/dist/rules/index.cjs +614 -0
  74. package/dist/rules/index.cjs.map +1 -0
  75. package/dist/rules/index.d.cts +166 -0
  76. package/dist/rules/index.d.ts +166 -0
  77. package/dist/rules/index.js +60 -0
  78. package/dist/rules/index.js.map +1 -0
  79. package/dist/schema-DuDMPqpq.d.ts +183 -0
  80. package/dist/schema-NnIdKFl2.d.cts +183 -0
  81. package/dist/service/index.cjs +3774 -0
  82. package/dist/service/index.cjs.map +1 -0
  83. package/dist/service/index.d.cts +262 -0
  84. package/dist/service/index.d.ts +262 -0
  85. package/dist/service/index.js +45 -0
  86. package/dist/service/index.js.map +1 -0
  87. package/dist/service/main.cjs +3759 -0
  88. package/dist/service/main.cjs.map +1 -0
  89. package/dist/service/main.d.cts +1 -0
  90. package/dist/service/main.d.ts +1 -0
  91. package/dist/service/main.js +60 -0
  92. package/dist/service/main.js.map +1 -0
  93. package/dist/sources-CAaQR6-N.d.ts +159 -0
  94. package/dist/sources-CpDOUlzx.d.cts +159 -0
  95. package/dist/telegram-CpodYJrP.d.cts +64 -0
  96. package/dist/telegram-pvphmBwn.d.ts +64 -0
  97. package/dist/tiers/index.cjs +471 -0
  98. package/dist/tiers/index.cjs.map +1 -0
  99. package/dist/tiers/index.d.cts +231 -0
  100. package/dist/tiers/index.d.ts +231 -0
  101. package/dist/tiers/index.js +34 -0
  102. package/dist/tiers/index.js.map +1 -0
  103. package/dist/types-C0ETog04.d.ts +109 -0
  104. package/dist/types-CcoKpStA.d.cts +109 -0
  105. package/dist/types-DFPpTMWo.d.cts +186 -0
  106. package/dist/types-DFPpTMWo.d.ts +186 -0
  107. package/package.json +114 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bot/commands.ts","../src/service/logger.ts","../src/bot/telegram-bot.ts","../src/bot/discord-bot.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { getAddress } from 'viem'\nimport { escapeHtml } from '../notifiers/escape.js'\nimport {\n defaultRules,\n formatValidationError,\n parseSubscription,\n ruleComplexity,\n safeParseRule,\n type Platform,\n type Rule,\n type Subscription,\n} from '../rules/schema.js'\nimport { linkMessage, verifyWalletLink } from '../tiers/entitlements.js'\nimport type { EntitlementGate } from '../tiers/enforce.js'\nimport type { AlertStore } from '../service/store.js'\n\n/**\n * The bot's command surface, written once and shared by Telegram and Discord.\n *\n * Both platforms deliver the same thing to this router: a command name, a\n * subscriber id, a default delivery target and a list of arguments. Keeping\n * the logic here means the two bots cannot drift apart in what `/rule add`\n * accepts or what `/tier` reports, and it means the command tests exercise the\n * real code path rather than a Telegram-shaped copy of it.\n *\n * Every command answers bad input with a specific, actionable message. There\n * is no silent failure and no raw validation dump: a mistyped threshold gets\n * \"usd must be a positive number, for example /threshold whales 5000\".\n */\n\n/** What a platform hands the router. */\nexport interface CommandContext {\n /** Stable identity for entitlements: `telegram:<user id>`, `discord:<guild or user id>`. */\n subscriberId: string\n platform: Platform\n /**\n * Where alerts would go if the user does not name a target: the Telegram\n * chat id, or the Discord channel id the command was used in.\n */\n defaultTarget: string\n /** Arguments after the command word. */\n args: string[]\n /** The whole argument string, for commands that take free-form JSON. */\n rest: string\n}\n\n/** A reply to render back to the user. */\nexport interface CommandReply {\n /** Telegram HTML, which Discord renders acceptably after tag stripping. */\n html: string\n /** Plain-text equivalent, used by Discord and by logs. */\n text: string\n}\n\n/** A command the bot exposes. */\nexport interface CommandSpec {\n name: string\n description: string\n /** Usage line shown by `/help` and in the README. */\n usage: string\n /** Discord slash-command options, when the command takes arguments. */\n options?: { name: string; description: string; required: boolean }[]\n}\n\n/** Everything the router needs. */\nexport interface CommandRouterDeps {\n store: AlertStore\n gate: EntitlementGate\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n /** Id factory for new subscriptions and nonces. @defaultValue `randomUUID` */\n newId?: () => string\n /** Shown by `/upgrade`: how to pay. Set from the service config. */\n upgradeInstructions?: string\n}\n\n/** The router. */\nexport interface CommandRouter {\n /** Every command, for `/help` and for Discord registration. */\n readonly commands: readonly CommandSpec[]\n /** Handle one command. Never throws: errors come back as a reply. */\n handle(command: string, context: CommandContext): Promise<CommandReply>\n}\n\nconst reply = (text: string): CommandReply => ({ html: escapeHtml(text), text })\n\nconst html = (htmlText: string, plain: string): CommandReply => ({ html: htmlText, text: plain })\n\n/** The command catalogue. */\nexport const COMMANDS: readonly CommandSpec[] = [\n { name: 'start', description: 'Set up alerts in this chat', usage: '/start' },\n { name: 'help', description: 'Show every command', usage: '/help' },\n {\n name: 'subscribe',\n description: 'Send alerts here, with a starter rule set',\n usage: '/subscribe [target]',\n options: [\n {\n name: 'target',\n description: 'Discord webhook URL or channel id. Defaults to this channel.',\n required: false,\n },\n ],\n },\n {\n name: 'unsubscribe',\n description: 'Stop alerts for a subscription',\n usage: '/unsubscribe [subscription id]',\n options: [{ name: 'id', description: 'Subscription id from /status', required: false }],\n },\n { name: 'rules', description: 'List your rules', usage: '/rules' },\n {\n name: 'rule',\n description: 'Add, remove, enable or disable a rule',\n usage: '/rule add <json> | /rule rm <id> | /rule on <id> | /rule off <id>',\n options: [{ name: 'input', description: 'add <json> | rm <id> | on <id> | off <id>', required: true }],\n },\n {\n name: 'threshold',\n description: 'Set a rule minimum USD value',\n usage: '/threshold <rule id> <usd>',\n options: [\n { name: 'rule', description: 'Rule id', required: true },\n { name: 'usd', description: 'Minimum USD value', required: true },\n ],\n },\n {\n name: 'watch',\n description: 'Add a token to a rule watchlist',\n usage: '/watch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n {\n name: 'unwatch',\n description: 'Remove a token from a rule watchlist',\n usage: '/unwatch <token address> [rule id]',\n options: [\n { name: 'token', description: 'Token contract address', required: true },\n { name: 'rule', description: 'Rule id (defaults to the first rule)', required: false },\n ],\n },\n { name: 'tier', description: 'Show your tier and its limits', usage: '/tier' },\n { name: 'status', description: 'Show your subscriptions and delivery state', usage: '/status' },\n {\n name: 'link',\n description: 'Link a paying wallet (signature verified)',\n usage: '/link [address] [signature]',\n options: [\n { name: 'address', description: 'Wallet address that pays', required: false },\n { name: 'signature', description: 'Signature of the link message', required: false },\n ],\n },\n { name: 'upgrade', description: 'How to go premium', usage: '/upgrade' },\n]\n\nfunction ruleSummary(rule: Rule): string {\n const parts = [\n `${rule.enabled ? 'on ' : 'off'} ${rule.id}`,\n rule.name ? `\"${rule.name}\"` : null,\n `kinds=${rule.kinds.join('/')}`,\n `pads=${rule.launchpads.join('/')}`,\n rule.minUsd !== undefined ? `minUsd=${rule.minUsd}` : null,\n rule.maxUsd !== undefined ? `maxUsd=${rule.maxUsd}` : null,\n rule.minLiquidityUsd !== undefined ? `minLiq=${rule.minLiquidityUsd}` : null,\n rule.maxLiquidityUsd !== undefined ? `maxLiq=${rule.maxLiquidityUsd}` : null,\n rule.side !== 'any' ? `side=${rule.side}` : null,\n rule.tokens.length > 0 ? `tokens=${rule.tokens.length}` : null,\n rule.deployers.length > 0 ? `deployers=${rule.deployers.length}` : null,\n rule.rateLimit ? `rate=${JSON.stringify(rule.rateLimit)}` : null,\n `filters=${ruleComplexity(rule)}`,\n ].filter((part): part is string => part !== null)\n return parts.join(' ')\n}\n\n/** Build the command router. */\nexport function createCommandRouter(deps: CommandRouterDeps): CommandRouter {\n const now = deps.now ?? Date.now\n const newId = deps.newId ?? randomUUID\n\n function subscriptionsOf(context: CommandContext): Subscription[] {\n return deps.store.listSubscriptionsFor(context.subscriberId)\n }\n\n function firstSubscription(context: CommandContext): Subscription | null {\n return subscriptionsOf(context)[0] ?? null\n }\n\n async function handleSubscribe(context: CommandContext): Promise<CommandReply> {\n const target = context.args[0]?.trim() || context.defaultTarget\n if (!target) {\n return reply(\n 'I could not work out where to send alerts. Pass a target: /subscribe <discord webhook URL or channel id>',\n )\n }\n const existing = subscriptionsOf(context)\n if (existing.some((sub) => sub.target === target)) {\n return reply(`This target is already subscribed. Use /rules to edit it, or /unsubscribe to stop.`)\n }\n\n const decision = await deps.gate.checkAddSubscription(context.subscriberId, existing.length)\n if (!decision.allowed) return reply(`Cannot subscribe: ${decision.reason}`)\n\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const rules = defaultRules().filter((rule) =>\n rule.kinds.every((kind) => policy.allowedKinds.includes(kind)),\n )\n const subscription = parseSubscription({\n id: newId(),\n subscriberId: context.subscriberId,\n platform: context.platform,\n target,\n enabled: true,\n rules: rules.slice(0, policy.maxRulesPerSubscription),\n createdAtMs: now(),\n })\n deps.store.saveSubscription(subscription)\n\n return html(\n [\n `<b>Subscribed.</b> Alerts will arrive here.`,\n `Subscription id: <code>${escapeHtml(subscription.id)}</code>`,\n `Tier: <b>${policy.tier}</b> (delivery delay ${policy.deliveryDelayMs / 1000}s, ${policy.maxAlertsPerHour} alerts/hour)`,\n '',\n 'Starter rules:',\n ...subscription.rules.map((rule) => `• <code>${escapeHtml(ruleSummary(rule))}</code>`),\n '',\n 'Edit them with /threshold, /watch and /rule. See /help.',\n ].join('\\n'),\n `Subscribed. id=${subscription.id} tier=${policy.tier}`,\n )\n }\n\n async function handleUnsubscribe(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions. Use /subscribe to start one.')\n\n const id = context.args[0]?.trim()\n if (!id) {\n if (subs.length > 1) {\n return html(\n [\n 'You have several subscriptions. Name the one to remove:',\n ...subs.map((sub) => `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)}`),\n '',\n 'Usage: /unsubscribe &lt;subscription id&gt;',\n ].join('\\n'),\n 'Name the subscription to remove: ' + subs.map((sub) => sub.id).join(', '),\n )\n }\n const only = subs[0] as Subscription\n deps.store.deleteSubscription(only.id)\n return reply('Unsubscribed. No more alerts will be delivered here.')\n }\n\n const match = subs.find((sub) => sub.id === id)\n if (!match) return reply(`No subscription with id \"${id}\". Use /status to list yours.`)\n deps.store.deleteSubscription(match.id)\n return reply(`Unsubscribed ${match.id}.`)\n }\n\n function handleRules(context: CommandContext): CommandReply {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('You have no subscriptions yet. Use /subscribe to start one.')\n const lines: string[] = []\n for (const sub of subs) {\n lines.push(`<b>${escapeHtml(sub.target)}</b> (<code>${escapeHtml(sub.id)}</code>)`)\n if (sub.rules.length === 0) lines.push(' no rules: nothing will be delivered')\n for (const rule of sub.rules) lines.push(` • <code>${escapeHtml(ruleSummary(rule))}</code>`)\n }\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleRule(context: CommandContext): Promise<CommandReply> {\n const action = context.args[0]?.toLowerCase()\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe, then add rules.')\n\n if (action === 'add') {\n const json = context.rest.slice(context.rest.indexOf('add') + 3).trim()\n if (!json) {\n return html(\n [\n 'Usage: <code>/rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}</code>',\n '',\n 'Fields: id, name, enabled, kinds, launchpads, minUsd, maxUsd, minLiquidityUsd,',\n 'maxLiquidityUsd, tokens, deployers, excludeDeployers, side, reputation, rateLimit.',\n ].join('\\n'),\n 'Usage: /rule add {\"id\":\"big-whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":25000}',\n )\n }\n let parsed: unknown\n try {\n parsed = JSON.parse(json)\n } catch {\n return reply('That is not valid JSON. Example: /rule add {\"id\":\"whales\",\"kinds\":[\"whale_trade\"],\"minUsd\":5000}')\n }\n const result = safeParseRule(parsed)\n if (!result.success) return reply(`Rule rejected: ${formatValidationError(result.error)}`)\n const rule = result.data\n\n if (subscription.rules.some((existing) => existing.id === rule.id)) {\n return reply(`A rule with id \"${rule.id}\" already exists. Remove it first with /rule rm ${rule.id}.`)\n }\n const decision = await deps.gate.checkAddRule(context.subscriberId, rule, subscription.rules.length)\n if (!decision.allowed) return reply(`Rule rejected: ${decision.reason}`)\n\n subscription.rules.push(rule)\n deps.store.saveSubscription(subscription)\n return html(\n `Rule added: <code>${escapeHtml(ruleSummary(rule))}</code>`,\n `Rule added: ${ruleSummary(rule)}`,\n )\n }\n\n if (action === 'rm' || action === 'remove' || action === 'delete') {\n const id = context.args[1]?.trim()\n if (!id) return reply('Usage: /rule rm <rule id>. List them with /rules.')\n const index = subscription.rules.findIndex((rule) => rule.id === id)\n if (index === -1) return reply(`No rule with id \"${id}\". List them with /rules.`)\n subscription.rules.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} removed.`)\n }\n\n if (action === 'on' || action === 'off') {\n const id = context.args[1]?.trim()\n if (!id) return reply(`Usage: /rule ${action} <rule id>. List them with /rules.`)\n const rule = subscription.rules.find((entry) => entry.id === id)\n if (!rule) return reply(`No rule with id \"${id}\". List them with /rules.`)\n rule.enabled = action === 'on'\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${id} is now ${rule.enabled ? 'enabled' : 'disabled'}.`)\n }\n\n return html(\n [\n 'Usage:',\n '• <code>/rule add {json}</code>',\n '• <code>/rule rm &lt;id&gt;</code>',\n '• <code>/rule on &lt;id&gt;</code>',\n '• <code>/rule off &lt;id&gt;</code>',\n ].join('\\n'),\n 'Usage: /rule add {json} | /rule rm <id> | /rule on <id> | /rule off <id>',\n )\n }\n\n async function handleThreshold(context: CommandContext): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const [ruleId, rawUsd] = context.args\n if (!ruleId || !rawUsd) {\n return reply('Usage: /threshold <rule id> <usd>. Example: /threshold whales 5000')\n }\n const rule = subscription.rules.find((entry) => entry.id === ruleId)\n if (!rule) return reply(`No rule with id \"${ruleId}\". List them with /rules.`)\n const usd = Number(rawUsd)\n if (!Number.isFinite(usd) || usd < 0) {\n return reply(`\"${rawUsd}\" is not a valid amount. Use a positive number, for example /threshold ${ruleId} 5000`)\n }\n if (rule.maxUsd !== undefined && usd > rule.maxUsd) {\n return reply(`minUsd ${usd} would exceed this rule's maxUsd ${rule.maxUsd}. Lower it or raise maxUsd first.`)\n }\n rule.minUsd = usd\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) return reply(`Change rejected: ${decision.reason}`)\n deps.store.saveSubscription(subscription)\n return reply(`Rule ${ruleId} now requires at least $${usd} of value.`)\n }\n\n async function handleWatch(context: CommandContext, add: boolean): Promise<CommandReply> {\n const subscription = firstSubscription(context)\n if (!subscription) return reply('Subscribe first with /subscribe.')\n const rawToken = context.args[0]?.trim()\n if (!rawToken) {\n return reply(`Usage: /${add ? 'watch' : 'unwatch'} <token address> [rule id]`)\n }\n if (!/^0x[0-9a-fA-F]{40}$/.test(rawToken)) {\n return reply(`\"${rawToken}\" is not a token address. Pass the 0x contract address from the alert.`)\n }\n const token = getAddress(rawToken as `0x${string}`).toLowerCase()\n\n const ruleId = context.args[1]?.trim()\n const rule = ruleId\n ? subscription.rules.find((entry) => entry.id === ruleId)\n : subscription.rules[0]\n if (!rule) {\n return reply(\n ruleId ? `No rule with id \"${ruleId}\". List them with /rules.` : 'This subscription has no rules yet. Add one with /rule add.',\n )\n }\n\n if (add) {\n if (rule.tokens.includes(token)) return reply(`${rawToken} is already on rule ${rule.id}.`)\n rule.tokens.push(token)\n const decision = await deps.gate.checkRule(context.subscriberId, rule)\n if (!decision.allowed) {\n rule.tokens.pop()\n return reply(`Cannot add: ${decision.reason}`)\n }\n deps.store.saveSubscription(subscription)\n return reply(`Watching ${rawToken} on rule ${rule.id} (${rule.tokens.length} tokens).`)\n }\n\n const index = rule.tokens.indexOf(token)\n if (index === -1) return reply(`${rawToken} is not on rule ${rule.id}.`)\n rule.tokens.splice(index, 1)\n deps.store.saveSubscription(subscription)\n return reply(`Stopped watching ${rawToken} on rule ${rule.id}.`)\n }\n\n async function handleTier(context: CommandContext): Promise<CommandReply> {\n const { entitlement, policy } = await deps.gate.resolve(context.subscriberId)\n const expiry =\n entitlement.expiresAtMs === null\n ? 'no expiry'\n : `expires ${new Date(entitlement.expiresAtMs).toISOString()}`\n const lines = [\n `<b>Tier: ${escapeHtml(policy.tier)}</b> (${escapeHtml(expiry)}, source ${escapeHtml(entitlement.source)})`,\n `Subscriptions: up to ${policy.maxSubscriptions}`,\n `Rules per subscription: up to ${policy.maxRulesPerSubscription}`,\n `Filters per rule: up to ${policy.maxRuleComplexity}`,\n `Watchlist size: up to ${policy.maxWatchlistTokens}`,\n `Alerts per hour: up to ${policy.maxAlertsPerHour}`,\n `Delivery delay: ${policy.deliveryDelayMs / 1000}s`,\n `Liquidity and reputation filters: ${policy.allowOnChainLookupFilters ? 'included' : 'premium only'}`,\n `Event kinds: ${policy.allowedKinds.join(', ')}`,\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleStatus(context: CommandContext): Promise<CommandReply> {\n const subs = subscriptionsOf(context)\n if (subs.length === 0) return reply('No subscriptions. Use /subscribe to start one.')\n const delivered = deps.store.countDeliveriesForSubscriber(context.subscriberId, now() - 3_600_000)\n const { policy } = await deps.gate.resolve(context.subscriberId)\n const lines = [\n `<b>${subs.length} subscription${subs.length === 1 ? '' : 's'}</b>, ${delivered} alerts in the last hour (cap ${policy.maxAlertsPerHour})`,\n ...subs.map(\n (sub) =>\n `• <code>${escapeHtml(sub.id)}</code> → ${escapeHtml(sub.target)} (${sub.rules.length} rules, ${sub.enabled ? 'enabled' : 'disabled'})`,\n ),\n ]\n return html(lines.join('\\n'), lines.join('\\n').replace(/<[^>]+>/g, ''))\n }\n\n async function handleLink(context: CommandContext): Promise<CommandReply> {\n const [address, signature] = context.args\n if (!address) {\n const nonce = deps.store.issueLinkNonce(context.subscriberId, newId())\n const message = linkMessage(context.subscriberId, nonce)\n return html(\n [\n 'Sign this exact message with the wallet that pays, then send',\n `<code>/link &lt;address&gt; &lt;signature&gt;</code>`,\n '',\n `<pre>${escapeHtml(message)}</pre>`,\n ].join('\\n'),\n `Sign this message and reply with /link <address> <signature>:\\n${message}`,\n )\n }\n if (!signature) {\n return reply('Usage: /link <address> <signature>. Send /link on its own to get the message to sign.')\n }\n const nonce = deps.store.getLinkNonce(context.subscriberId)\n if (!nonce) {\n return reply('No pending link request. Send /link on its own first to get a fresh message to sign.')\n }\n const verified = await verifyWalletLink(context.subscriberId, nonce, address, signature)\n if (!verified) {\n return reply('That signature does not match. Sign the exact message from /link with the wallet you named.')\n }\n deps.store.linkWallet(context.subscriberId, verified)\n deps.store.clearLinkNonce(context.subscriberId)\n return reply(`Wallet ${verified} linked. Payments from it now count towards your subscription.`)\n }\n\n function handleUpgrade(): CommandReply {\n if (!deps.upgradeInstructions) {\n return reply(\n 'This deployment does not sell premium access. The operator grants it through configuration; ask them.',\n )\n }\n return html(escapeHtml(deps.upgradeInstructions), deps.upgradeInstructions)\n }\n\n function handleHelp(): CommandReply {\n const header = 'hood-alerts: Robinhood Chain memecoin alerts.'\n const kinds = 'Event kinds: launch, curve_trade, graduation, whale_trade.'\n const lifecycle =\n 'NOXA lists instantly (no bonding curve), so it never produces curve_trade or graduation. The Odyssey produces all four.'\n return html(\n [\n `<b>${escapeHtml(header)}</b>`,\n '',\n ...COMMANDS.map(\n (command) => `<code>${escapeHtml(command.usage)}</code> - ${escapeHtml(command.description)}`,\n ),\n '',\n escapeHtml(kinds),\n escapeHtml(lifecycle),\n ].join('\\n'),\n [\n header,\n '',\n ...COMMANDS.map((command) => `${command.usage} - ${command.description}`),\n '',\n kinds,\n lifecycle,\n ].join('\\n'),\n )\n }\n\n return {\n commands: COMMANDS,\n async handle(command: string, context: CommandContext): Promise<CommandReply> {\n const name = command.replace(/^\\//, '').split('@')[0]?.toLowerCase() ?? ''\n try {\n switch (name) {\n case 'start':\n case 'help':\n return handleHelp()\n case 'subscribe':\n return await handleSubscribe(context)\n case 'unsubscribe':\n return await handleUnsubscribe(context)\n case 'rules':\n return handleRules(context)\n case 'rule':\n return await handleRule(context)\n case 'threshold':\n return await handleThreshold(context)\n case 'watch':\n return await handleWatch(context, true)\n case 'unwatch':\n return await handleWatch(context, false)\n case 'tier':\n return await handleTier(context)\n case 'status':\n return await handleStatus(context)\n case 'link':\n return await handleLink(context)\n case 'upgrade':\n return handleUpgrade()\n default:\n return reply(`Unknown command \"${name}\". Send /help for the list.`)\n }\n } catch (error) {\n // A handler throwing must still answer the user. Silence looks like a\n // dead bot, which is worse than an error message.\n const message = error instanceof Error ? error.message : String(error)\n return reply(`That command failed: ${message}. Try /help, or report this if it keeps happening.`)\n }\n },\n }\n}\n","/** Structured logging: one JSON object per line, machine-readable by default. */\n\n/** Log levels, ordered. */\nexport const LEVELS = ['debug', 'info', 'warn', 'error'] as const\n\n/** A log level. */\nexport type LogLevel = (typeof LEVELS)[number]\n\n/** Structured fields attached to a log line. */\nexport type LogFields = Record<string, unknown>\n\n/** The logger the service uses everywhere. */\nexport interface Logger {\n debug(message: string, fields?: LogFields): void\n info(message: string, fields?: LogFields): void\n warn(message: string, fields?: LogFields): void\n error(message: string, fields?: LogFields): void\n /** A logger that adds `fields` to every line, for per-component context. */\n child(fields: LogFields): Logger\n}\n\n/** Options for {@link createLogger}. */\nexport interface LoggerOptions {\n /** Minimum level emitted. @defaultValue `'info'` */\n level?: LogLevel\n /** Where lines go. @defaultValue `console.log` */\n write?: (line: string) => void\n /** Fields added to every line. */\n base?: LogFields\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/**\n * Serialize a value that may contain a bigint or an Error. Log lines must\n * never throw: a `TypeError: Do not know how to serialize a BigInt` inside an\n * error handler loses the error it was reporting.\n */\nfunction safeStringify(value: unknown): string {\n return JSON.stringify(value, (_key, raw: unknown) => {\n if (typeof raw === 'bigint') return raw.toString()\n if (raw instanceof Error) return { name: raw.name, message: raw.message, stack: raw.stack }\n return raw\n })\n}\n\n/** Build a structured logger. */\nexport function createLogger(options: LoggerOptions = {}): Logger {\n const minimum = LEVELS.indexOf(options.level ?? 'info')\n const write = options.write ?? ((line: string): void => console.log(line))\n const now = options.now ?? Date.now\n const base = options.base ?? {}\n\n const emit = (level: LogLevel, message: string, fields?: LogFields): void => {\n if (LEVELS.indexOf(level) < minimum) return\n write(\n safeStringify({\n time: new Date(now()).toISOString(),\n level,\n message,\n ...base,\n ...(fields ?? {}),\n }),\n )\n }\n\n return {\n debug: (message, fields) => emit('debug', message, fields),\n info: (message, fields) => emit('info', message, fields),\n warn: (message, fields) => emit('warn', message, fields),\n error: (message, fields) => emit('error', message, fields),\n child(fields: LogFields): Logger {\n return createLogger({ ...options, base: { ...base, ...fields } })\n },\n }\n}\n\n/** A logger that discards everything. For tests and for `--quiet`. */\nexport function createSilentLogger(): Logger {\n const silent: Logger = {\n debug: () => undefined,\n info: () => undefined,\n warn: () => undefined,\n error: () => undefined,\n child: () => silent,\n }\n return silent\n}\n","import { createTelegramClient, type TelegramClient, type TelegramClientOptions } from '../notifiers/telegram.js'\nimport type { Logger } from '../service/logger.js'\nimport { createSilentLogger } from '../service/logger.js'\nimport type { CommandContext, CommandRouter } from './commands.js'\n\n/**\n * The Telegram bot: long polling, because it needs no public URL, no TLS\n * certificate and no inbound firewall rule. A self-hoster runs the container\n * and the bot works; a webhook deployment would need all three before the\n * first `/start`.\n *\n * `getUpdates` is called with a long timeout, so the loop is idle rather than\n * busy between messages, and the offset is advanced past every update the loop\n * has handled so a restart never re-processes an old command.\n */\n\n/** Options for {@link createTelegramBot}. */\nexport interface TelegramBotOptions extends TelegramClientOptions {\n router: CommandRouter\n logger?: Logger\n /** Reuse the notifier's client so both share one rate-limit path. */\n client?: TelegramClient\n /** Long-poll timeout in seconds. @defaultValue `30` */\n pollTimeoutSeconds?: number\n /** Persist the update offset so a restart resumes cleanly. */\n offsetStore?: { get(): number | null; set(offset: number): void }\n}\n\n/** A running bot. */\nexport interface TelegramBot {\n /** Poll once and handle whatever arrived. Returns how many updates it handled. */\n pollOnce(): Promise<number>\n /** Publish the command list so Telegram shows the `/` menu. */\n publishCommands(): Promise<boolean>\n /** Start the loop. Resolves when {@link TelegramBot.stop} is called. */\n start(): Promise<void>\n /** Stop the loop after the current poll. */\n stop(): void\n}\n\ninterface TelegramUpdate {\n update_id: number\n message?: {\n message_id: number\n text?: string\n chat?: { id: number }\n from?: { id: number; is_bot?: boolean }\n }\n}\n\nfunction parseCommand(text: string): { command: string; args: string[]; rest: string } | null {\n const trimmed = text.trim()\n if (!trimmed.startsWith('/')) return null\n const [head, ...tail] = trimmed.split(/\\s+/)\n if (!head) return null\n return { command: head, args: tail, rest: trimmed.slice(head.length).trim() }\n}\n\n/**\n * Build the Telegram bot.\n *\n * @example\n * ```ts\n * const bot = createTelegramBot({ botToken: process.env.TELEGRAM_BOT_TOKEN!, router })\n * await bot.publishCommands()\n * await bot.start()\n * ```\n */\nexport function createTelegramBot(options: TelegramBotOptions): TelegramBot {\n const client = options.client ?? createTelegramClient(options)\n const logger = options.logger ?? createSilentLogger()\n const pollTimeout = options.pollTimeoutSeconds ?? 30\n let offset = options.offsetStore?.get() ?? 0\n let running = false\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n const message = update.message\n const text = message?.text\n const chatId = message?.chat?.id\n const fromId = message?.from?.id\n if (!text || chatId === undefined || fromId === undefined || message?.from?.is_bot === true) return\n\n const parsed = parseCommand(text)\n if (!parsed) return\n\n const context: CommandContext = {\n subscriberId: `telegram:${fromId}`,\n platform: 'telegram',\n defaultTarget: String(chatId),\n args: parsed.args,\n rest: parsed.rest,\n }\n const reply = await options.router.handle(parsed.command, context)\n const response = await client.call('sendMessage', {\n chat_id: chatId,\n text: reply.html,\n parse_mode: 'HTML',\n link_preview_options: { is_disabled: true },\n })\n if (!response.ok) {\n logger.warn('telegram reply failed', {\n chatId,\n command: parsed.command,\n error: response.description,\n })\n }\n }\n\n return {\n async publishCommands(): Promise<boolean> {\n const response = await client.call('setMyCommands', {\n commands: options.router.commands.map((command) => ({\n command: command.name,\n description: command.description.slice(0, 256),\n })),\n })\n if (!response.ok) logger.warn('setMyCommands failed', { error: response.description })\n return response.ok\n },\n\n async pollOnce(): Promise<number> {\n const response = await client.call('getUpdates', {\n ...(offset > 0 ? { offset } : {}),\n timeout: pollTimeout,\n allowed_updates: ['message'],\n })\n if (!response.ok) {\n logger.warn('getUpdates failed', { error: response.description })\n return 0\n }\n const updates = Array.isArray(response.result) ? (response.result as TelegramUpdate[]) : []\n for (const update of updates) {\n try {\n await handleUpdate(update)\n } catch (error) {\n // One bad command must not stall the loop or replay forever: log it\n // and still advance past it.\n logger.error('telegram update handler threw', {\n updateId: update.update_id,\n error: error instanceof Error ? error.message : String(error),\n })\n }\n offset = update.update_id + 1\n options.offsetStore?.set(offset)\n }\n return updates.length\n },\n\n async start(): Promise<void> {\n running = true\n logger.info('telegram bot started', { offset })\n while (running) {\n try {\n await this.pollOnce()\n } catch (error) {\n logger.error('telegram poll failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n await new Promise((resolve) => setTimeout(resolve, 2_000))\n }\n }\n logger.info('telegram bot stopped')\n },\n\n stop(): void {\n running = false\n },\n }\n}\n","import { createPublicKey, verify as cryptoVerify } from 'node:crypto'\nimport { resolveFetch } from '../notifiers/http.js'\nimport type { FetchLike } from '../notifiers/types.js'\nimport { truncate } from '../notifiers/escape.js'\nimport type { CommandContext, CommandRouter, CommandSpec } from './commands.js'\n\n/**\n * The Discord bot: slash commands over the HTTP interactions endpoint.\n *\n * Discord offers two ways to receive commands: a persistent gateway\n * WebSocket, or an HTTPS interactions endpoint. This package uses the\n * endpoint, because the service already runs an HTTP server for `/health` and\n * `/metrics`, and an endpoint scales to zero and survives a restart with no\n * reconnect logic. The price is that every request must be signature-verified,\n * which Discord enforces: it sends deliberately invalid signatures during\n * setup and refuses to save an endpoint that answers them with anything other\n * than `401`.\n *\n * Verification is Ed25519 over `timestamp + rawBody`, using the application's\n * public key. Node's `crypto` verifies Ed25519 natively, so this needs no\n * dependency: the raw 32-byte key is wrapped in the fixed SPKI DER prefix for\n * `id-Ed25519` (RFC 8410) and handed to `createPublicKey`.\n */\n\n/** Discord's interaction type numbers, for the two kinds handled here. */\nexport const INTERACTION_TYPE = { PING: 1, APPLICATION_COMMAND: 2 } as const\n\n/** Discord's interaction callback types. */\nexport const CALLBACK_TYPE = { PONG: 1, CHANNEL_MESSAGE_WITH_SOURCE: 4 } as const\n\n/** Discord marks a message ephemeral (only the caller sees it) with this flag. */\nexport const EPHEMERAL_FLAG = 1 << 6\n\n/** RFC 8410 SPKI prefix for an Ed25519 public key. */\nconst ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')\n\n/**\n * Verify a Discord interaction signature.\n *\n * @param publicKey the application's hex public key from the developer portal.\n * @returns `true` only for a signature that verifies. Any malformed input is\n * `false`, never a throw: a crash here would answer Discord with a 500 and\n * fail its endpoint validation just as surely as a wrong answer.\n */\nexport function verifyDiscordSignature(\n publicKey: string,\n signature: string,\n timestamp: string,\n rawBody: string,\n): boolean {\n if (!/^[0-9a-fA-F]{64}$/.test(publicKey)) return false\n if (!/^[0-9a-fA-F]{128}$/.test(signature)) return false\n if (!timestamp) return false\n try {\n const key = createPublicKey({\n key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(publicKey, 'hex')]),\n format: 'der',\n type: 'spki',\n })\n return cryptoVerify(\n null,\n Buffer.from(timestamp + rawBody, 'utf8'),\n key,\n Buffer.from(signature, 'hex'),\n )\n } catch {\n return false\n }\n}\n\n/** A Discord slash-command definition, as the API expects it. */\nexport interface DiscordCommandDefinition {\n name: string\n description: string\n /** Option type 3 is STRING, the only type these commands need. */\n options?: { type: 3; name: string; description: string; required: boolean }[]\n}\n\n/** Convert the shared command catalogue into Discord slash-command definitions. */\nexport function discordCommandDefinitions(\n commands: readonly CommandSpec[],\n): DiscordCommandDefinition[] {\n return commands.map((command) => ({\n name: command.name,\n // Discord caps descriptions at 100 characters and rejects the whole\n // registration if any one is longer.\n description: truncate(command.description, 100),\n ...(command.options && command.options.length > 0\n ? {\n options: command.options.map((option) => ({\n type: 3 as const,\n name: option.name,\n description: truncate(option.description, 100),\n required: option.required,\n })),\n }\n : {}),\n }))\n}\n\n/** Options for {@link registerDiscordCommands}. */\nexport interface RegisterCommandsOptions {\n applicationId: string\n botToken: string\n /** Register to one guild (instant) instead of globally (up to an hour to propagate). */\n guildId?: string\n apiBase?: string\n fetch?: FetchLike\n}\n\n/**\n * Register slash commands with Discord.\n *\n * @returns the number of commands Discord accepted.\n * @throws when Discord rejects the registration, with its own error message\n * attached: a silent failure here means a bot that appears installed and has\n * no commands.\n *\n * @example\n * ```bash\n * DISCORD_APPLICATION_ID=... DISCORD_BOT_TOKEN=... npm run register:discord\n * ```\n */\nexport async function registerDiscordCommands(\n commands: readonly CommandSpec[],\n options: RegisterCommandsOptions,\n): Promise<number> {\n const fetchImpl = resolveFetch(options.fetch)\n const apiBase = (options.apiBase ?? 'https://discord.com/api/v10').replace(/\\/+$/, '')\n const url = options.guildId\n ? `${apiBase}/applications/${options.applicationId}/guilds/${options.guildId}/commands`\n : `${apiBase}/applications/${options.applicationId}/commands`\n\n const response = await fetchImpl(url, {\n method: 'PUT',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bot ${options.botToken}`,\n },\n body: JSON.stringify(discordCommandDefinitions(commands)),\n })\n const raw = await response.text()\n if (!response.ok) {\n throw new Error(`hood-alerts: Discord rejected the command registration (${response.status}): ${raw}`)\n }\n try {\n const parsed: unknown = JSON.parse(raw)\n return Array.isArray(parsed) ? parsed.length : 0\n } catch {\n return 0\n }\n}\n\n/** The shape of an incoming interaction, narrowed to what the router needs. */\ninterface Interaction {\n type: number\n guild_id?: string\n channel_id?: string\n member?: { user?: { id?: string } }\n user?: { id?: string }\n data?: {\n name?: string\n options?: { name: string; value?: unknown }[]\n }\n}\n\n/** What to answer Discord with. */\nexport interface InteractionResponse {\n status: number\n body: unknown\n}\n\n/** Options for {@link createDiscordInteractionHandler}. */\nexport interface DiscordInteractionOptions {\n router: CommandRouter\n /** The application's public key, for signature verification. */\n publicKey: string\n /** Reply only to the caller. @defaultValue `true` */\n ephemeral?: boolean\n}\n\n/**\n * Build the interactions handler. Mount it on the service's HTTP server at\n * whatever path is configured as the application's Interactions Endpoint URL.\n *\n * @example\n * ```ts\n * const interactions = createDiscordInteractionHandler({ router, publicKey })\n * // inside the HTTP server:\n * const answer = await interactions.handle({ signature, timestamp, rawBody })\n * res.writeHead(answer.status, { 'content-type': 'application/json' })\n * res.end(JSON.stringify(answer.body))\n * ```\n */\nexport function createDiscordInteractionHandler(options: DiscordInteractionOptions): {\n handle(request: { signature: string | null; timestamp: string | null; rawBody: string }): Promise<InteractionResponse>\n} {\n const ephemeral = options.ephemeral ?? true\n\n return {\n async handle(request): Promise<InteractionResponse> {\n if (\n !request.signature ||\n !request.timestamp ||\n !verifyDiscordSignature(options.publicKey, request.signature, request.timestamp, request.rawBody)\n ) {\n return { status: 401, body: { error: 'invalid request signature' } }\n }\n\n let interaction: Interaction\n try {\n interaction = JSON.parse(request.rawBody) as Interaction\n } catch {\n return { status: 400, body: { error: 'body is not JSON' } }\n }\n\n if (interaction.type === INTERACTION_TYPE.PING) {\n return { status: 200, body: { type: CALLBACK_TYPE.PONG } }\n }\n if (interaction.type !== INTERACTION_TYPE.APPLICATION_COMMAND) {\n return { status: 400, body: { error: `unsupported interaction type ${interaction.type}` } }\n }\n\n const name = interaction.data?.name\n if (!name) return { status: 400, body: { error: 'interaction carried no command name' } }\n\n const values = (interaction.data?.options ?? [])\n .map((option) => (option.value === undefined ? '' : String(option.value)))\n .filter((value) => value.length > 0)\n // A Discord option value can itself contain spaces (a JSON rule body, a\n // webhook URL), so the router receives the option values as separate\n // args and the joined string as `rest`.\n const rest = values.join(' ')\n const args = values.length === 1 ? (values[0] as string).split(/\\s+/) : values\n\n const userId = interaction.member?.user?.id ?? interaction.user?.id ?? ''\n const context: CommandContext = {\n // A guild pays once for all its channels; a DM is billed to the user.\n subscriberId: `discord:${interaction.guild_id ?? userId}`,\n platform: 'discord',\n defaultTarget: interaction.channel_id ?? '',\n args,\n rest: `${name} ${rest}`.trim(),\n }\n\n const reply = await options.router.handle(name, context)\n return {\n status: 200,\n body: {\n type: CALLBACK_TYPE.CHANNEL_MESSAGE_WITH_SOURCE,\n data: {\n // Discord hard-caps message content at 2000 characters.\n content: truncate(reply.text, 1_900),\n ...(ephemeral ? { flags: EPHEMERAL_FLAG } : {}),\n },\n },\n }\n },\n }\n}\n"],"mappings":"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","names":["nonce","reply","reply"]}
@@ -0,0 +1,76 @@
1
+ // src/events/types.ts
2
+ function isKind(event, kind) {
3
+ return event.kind === kind;
4
+ }
5
+ var EVENT_KIND_LABELS = {
6
+ launch: "New launch",
7
+ curve_trade: "Curve trade",
8
+ graduation: "Graduation",
9
+ whale_trade: "Whale trade"
10
+ };
11
+ var LAUNCHPAD_EVENT_KINDS = {
12
+ noxa: ["launch", "whale_trade"],
13
+ odyssey: ["launch", "curve_trade", "graduation", "whale_trade"]
14
+ };
15
+
16
+ // src/events/logs.ts
17
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
18
+ function isResultTooLarge(error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return /exceeds limit|too many results|response size exceeded|query returned more than/i.test(message);
21
+ }
22
+ function isRetryableRpcError(error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ return /too many requests|rate limit|timed out|timeout|429|503|502|ECONNRESET|socket hang up|fetch failed/i.test(
25
+ message
26
+ );
27
+ }
28
+ async function fetchLogRange(fetcher, from, to, options = {}) {
29
+ const minSpan = options.minSpan ?? 1n;
30
+ const retries = options.retries ?? 5;
31
+ const backoffMs = options.backoffMs ?? 500;
32
+ const sleep = options.sleep ?? defaultSleep;
33
+ const run = async (lo, hi) => {
34
+ let attempt = 0;
35
+ for (; ; ) {
36
+ try {
37
+ return [...await fetcher(lo, hi)];
38
+ } catch (error) {
39
+ if (isResultTooLarge(error)) {
40
+ const span = hi - lo;
41
+ if (span < minSpan || span === 0n) throw error;
42
+ const mid = lo + span / 2n;
43
+ const left = await run(lo, mid);
44
+ const right = await run(mid + 1n, hi);
45
+ return [...left, ...right];
46
+ }
47
+ if (isRetryableRpcError(error) && attempt < retries) {
48
+ await sleep(backoffMs * 2 ** attempt);
49
+ attempt += 1;
50
+ continue;
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+ };
56
+ if (to < from) return [];
57
+ return run(from, to);
58
+ }
59
+ function batchAddresses(addresses, size = 200) {
60
+ const batches = [];
61
+ for (let i = 0; i < addresses.length; i += size) {
62
+ batches.push(addresses.slice(i, i + size));
63
+ }
64
+ return batches;
65
+ }
66
+
67
+ export {
68
+ isKind,
69
+ EVENT_KIND_LABELS,
70
+ LAUNCHPAD_EVENT_KINDS,
71
+ isResultTooLarge,
72
+ isRetryableRpcError,
73
+ fetchLogRange,
74
+ batchAddresses
75
+ };
76
+ //# sourceMappingURL=chunk-3LHCQA4Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/events/types.ts","../src/events/logs.ts"],"sourcesContent":["import type { Address, Hash } from 'viem'\n\n/**\n * The normalized alert event taxonomy for Robinhood Chain memecoins.\n *\n * Two launchpads run on mainnet (chain 4663) and they have genuinely\n * different lifecycles. The taxonomy models that difference instead of\n * pretending they are the same product:\n *\n * - **NOXA** (`fun.noxa.fi/robinhood`) is an *instant* launcher. One\n * transaction deploys the ERC-20, creates a Uniswap v3 pool, seeds\n * single-sided liquidity and locks the LP NFT. There is no bonding curve,\n * so NOXA emits `launch` and (through its pool) `whale_trade`, and it can\n * never emit `curve_trade` or `graduation`.\n * - **The Odyssey** (`theodyssey.fun`) is a pump.fun-style native-ETH bonding\n * curve with virtual reserves. It emits `launch` when a curve opens,\n * `curve_trade` for every buy/sell on the curve, and `graduation` when the\n * curve fills and liquidity migrates to a locked Uniswap v3 pool. After\n * graduation its tokens also produce `whale_trade` from the migrated pool.\n *\n * A `graduation` alert therefore covers The Odyssey only. Claiming NOXA\n * \"graduations\" would be describing an event that does not exist on chain.\n */\n\n/** Which launchpad an event originated from. */\nexport type Launchpad = 'noxa' | 'odyssey'\n\n/** The kinds of alert this package emits. */\nexport type AlertEventKind = 'launch' | 'curve_trade' | 'graduation' | 'whale_trade'\n\n/** Buy or sell, from the trader's perspective. */\nexport type TradeSide = 'buy' | 'sell'\n\n/** Explorer deep links attached to every event. */\nexport interface ExplorerLinks {\n /** The transaction that produced the event. */\n tx: string\n /** The token contract. */\n token: string\n /** The actor (deployer or trader). */\n actor: string\n /** The Uniswap v3 pool, when the event has one. */\n pool?: string\n}\n\n/** Fields every alert event carries. */\nexport interface AlertEventBase {\n /**\n * Stable, content-addressed identity for this event: `kind:txHash:logIndex`.\n * Reprocessing the same block range regenerates the identical id, which is\n * what makes the delivery dedupe table work across restarts and reorg\n * re-reads.\n */\n id: string\n kind: AlertEventKind\n launchpad: Launchpad\n /** The memecoin the event is about. */\n token: Address\n /** Token symbol read from the ERC-20, or `null` when the call reverts. */\n symbol: string | null\n /** Token name read from the ERC-20, or `null` when the call reverts. */\n name: string | null\n /** The wallet responsible: the deployer for launches, the trader for trades. */\n actor: Address\n blockNumber: bigint\n logIndex: number\n transactionHash: Hash\n /**\n * Block timestamp in milliseconds since epoch, or `null` when the event was\n * decoded without a block fetch (the source fills it whenever available).\n */\n timestampMs: number | null\n /**\n * USD value of the event, or `null` when it cannot be derived from a real\n * price source. Never guessed: launches carry the seeded/initial-buy value,\n * trades carry the traded notional, and `null` means \"no USDG or WETH leg to\n * price against\", which rules treat as \"does not satisfy a min-USD filter\".\n */\n usdValue: number | null\n explorer: ExplorerLinks\n}\n\n/**\n * A new token launch.\n *\n * NOXA: decoded from `TokenLaunched` on the launch factory. `pool` is present\n * immediately and `initialBuyAmount` is the deployer's own first buy.\n * Odyssey: decoded from `TokenCreated` on a bonding-curve factory. `pool` is\n * `null` because the curve holds the liquidity until graduation.\n */\nexport interface LaunchEvent extends AlertEventBase {\n kind: 'launch'\n /** The Uniswap v3 pool, or `null` for an Odyssey token still on its curve. */\n pool: Address | null\n /** The quote token of the pool (NOXA only, `null` on Odyssey). */\n pairToken: Address | null\n /** Raw initial buy in the pair token's units (NOXA), else `0n`. */\n initialBuyAmount: bigint\n /**\n * The Uniswap v3 LP position NFT minted for this launch (NOXA), or `null`\n * when the launchpad did not mint one at launch time (every Odyssey token,\n * whose LP appears at graduation). Reputation rules read its owner to prove\n * the LP is locked.\n */\n positionId: bigint | null\n /**\n * `true` when this launchpad lists instantly with no bonding curve, so no\n * `graduation` event will ever follow. Always `true` for NOXA. For Odyssey\n * it is `true` only for launches from the instant factory variant.\n */\n instantListing: boolean\n}\n\n/**\n * A bonding-curve buy or sell. The Odyssey only: NOXA has no curve.\n * Decoded from `Traded` on an Odyssey factory.\n */\nexport interface CurveTradeEvent extends AlertEventBase {\n kind: 'curve_trade'\n launchpad: 'odyssey'\n side: TradeSide\n /** Token amount bought or sold, raw (18 decimals). */\n tokenAmount: bigint\n /** Native ETH paid or received, in wei. */\n quoteAmountWei: bigint\n /** Protocol fee taken on this trade, in wei. */\n feeWei: bigint\n /** Curve virtual quote reserve after the trade, in wei. */\n virtualQuoteWei: bigint\n /** Curve virtual token reserve after the trade, raw. */\n virtualTokenAmount: bigint\n /** Price of one token in ETH implied by this fill, or `null` for a zero-size fill. */\n priceEth: number | null\n}\n\n/**\n * A curve that filled and migrated to a locked Uniswap v3 pool.\n * The Odyssey only. Decoded from `PoolMigrated`.\n */\nexport interface GraduationEvent extends AlertEventBase {\n kind: 'graduation'\n launchpad: 'odyssey'\n /** The Uniswap v3 pool liquidity migrated into. */\n pool: Address\n /** The position manager token id holding the migrated liquidity. */\n positionId: bigint\n /** Uniswap v3 liquidity units minted at migration. */\n liquidity: bigint\n /** Token side used to seed the pool, raw. */\n tokenUsed: bigint\n /** Quote side used to seed the pool, raw (6 decimals when the quote is USDG). */\n quoteUsed: bigint\n}\n\n/**\n * A Uniswap v3 swap on a memecoin pool above the configured USD threshold.\n * Both launchpads produce these: NOXA from block one, Odyssey after\n * graduation.\n */\nexport interface WhaleTradeEvent extends AlertEventBase {\n kind: 'whale_trade'\n pool: Address\n /** The pool's quote token (the non-memecoin side). */\n quoteToken: Address\n /** Quote token symbol, e.g. `WETH` or `USDG`. */\n quoteSymbol: string\n side: TradeSide\n /** Memecoin amount that moved, in human units. */\n tokenAmount: number\n /** Quote amount that moved, in human units. */\n quoteAmount: number\n /** Executed price of the memecoin in quote-token units. */\n price: number\n /** Uniswap v3 fee tier of the pool, in hundredths of a bip (3000 = 0.3%). */\n feeTier: number\n}\n\n/** Any normalized alert event. */\nexport type AlertEvent = LaunchEvent | CurveTradeEvent | GraduationEvent | WhaleTradeEvent\n\n/** Narrow an event to a kind. */\nexport function isKind<K extends AlertEventKind>(\n event: AlertEvent,\n kind: K,\n): event is Extract<AlertEvent, { kind: K }> {\n return event.kind === kind\n}\n\n/**\n * Human-readable one-line label for an event kind, used by the bot and the\n * notifier formatters.\n */\nexport const EVENT_KIND_LABELS: Record<AlertEventKind, string> = {\n launch: 'New launch',\n curve_trade: 'Curve trade',\n graduation: 'Graduation',\n whale_trade: 'Whale trade',\n}\n\n/**\n * Which event kinds each launchpad can actually produce. The rule engine uses\n * this to reject impossible rules (for example: Odyssey-only `graduation`\n * combined with a NOXA-only launchpad filter would never fire).\n */\nexport const LAUNCHPAD_EVENT_KINDS: Record<Launchpad, readonly AlertEventKind[]> = {\n noxa: ['launch', 'whale_trade'],\n odyssey: ['launch', 'curve_trade', 'graduation', 'whale_trade'],\n}\n","import type { Address } from 'viem'\n\n/**\n * Resilient log fetching for Robinhood Chain's public RPC.\n *\n * Two behaviours of the public endpoint drive this helper, both measured\n * against `https://rpc.mainnet.chain.robinhood.com` while building this\n * package:\n *\n * 1. **There is no block-range cap, but there is a 10,000-log result cap.**\n * A query spanning millions of blocks succeeds if it is selective enough\n * (the three Odyssey factories return every log they ever emitted in one\n * call); a query spanning 2,000 blocks fails with\n * `logs matched by query exceeds limit of 10000` if it is not (a topic-only\n * Uniswap v3 `Swap` query does). Fixed-size range chunking therefore\n * cannot guarantee success, so {@link fetchLogRange} bisects the range on\n * that specific error until each half fits.\n * 2. **It rate limits bursts with `Too Many Requests`.** Those are retried\n * with exponential backoff rather than surfaced, because a retryable 429\n * must never advance a caller's block cursor.\n */\n\n/** Options for {@link fetchLogRange}. */\nexport interface FetchLogRangeOptions {\n /**\n * Smallest range the bisector will produce. A single block that still\n * overflows the result cap throws, because there is nothing left to split.\n * @defaultValue `1n`\n */\n minSpan?: bigint\n /** Retries for rate-limit and transient transport errors. @defaultValue `5` */\n retries?: number\n /** Base backoff in ms, doubled per attempt. @defaultValue `500` */\n backoffMs?: number\n /** Sleep injection point for tests. @defaultValue a real timer */\n sleep?: (ms: number) => Promise<void>\n}\n\nconst defaultSleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** `true` when the RPC rejected the query for returning too many logs. */\nexport function isResultTooLarge(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /exceeds limit|too many results|response size exceeded|query returned more than/i.test(message)\n}\n\n/** `true` when the error is a transient rate limit or timeout worth retrying. */\nexport function isRetryableRpcError(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error)\n return /too many requests|rate limit|timed out|timeout|429|503|502|ECONNRESET|socket hang up|fetch failed/i.test(\n message,\n )\n}\n\n/**\n * Fetch logs for `[from, to]` inclusive through `fetcher`, bisecting the range\n * when the RPC reports too many results and retrying transient failures.\n *\n * @throws the underlying error when a single block still overflows the result\n * cap, or when retries are exhausted. Callers must treat a throw as \"range not\n * processed\" and leave their cursor untouched.\n *\n * @example\n * ```ts\n * const logs = await fetchLogRange(\n * (from, to) => hood.public.getLogs({ address: factory, event, fromBlock: from, toBlock: to }),\n * 0n,\n * await hood.public.getBlockNumber(),\n * )\n * ```\n */\nexport async function fetchLogRange<T>(\n fetcher: (from: bigint, to: bigint) => Promise<readonly T[]>,\n from: bigint,\n to: bigint,\n options: FetchLogRangeOptions = {},\n): Promise<T[]> {\n const minSpan = options.minSpan ?? 1n\n const retries = options.retries ?? 5\n const backoffMs = options.backoffMs ?? 500\n const sleep = options.sleep ?? defaultSleep\n\n const run = async (lo: bigint, hi: bigint): Promise<T[]> => {\n let attempt = 0\n for (;;) {\n try {\n return [...(await fetcher(lo, hi))]\n } catch (error) {\n if (isResultTooLarge(error)) {\n const span = hi - lo\n if (span < minSpan || span === 0n) throw error\n const mid = lo + span / 2n\n const left = await run(lo, mid)\n const right = await run(mid + 1n, hi)\n return [...left, ...right]\n }\n if (isRetryableRpcError(error) && attempt < retries) {\n await sleep(backoffMs * 2 ** attempt)\n attempt += 1\n continue\n }\n throw error\n }\n }\n }\n\n if (to < from) return []\n return run(from, to)\n}\n\n/**\n * Split a list of addresses into query-sized batches. The RPC accepts long\n * address arrays, but batching keeps any single response under the result cap\n * and keeps one failing batch from invalidating the whole set.\n */\nexport function batchAddresses(addresses: readonly Address[], size = 200): Address[][] {\n const batches: Address[][] = []\n for (let i = 0; i < addresses.length; i += size) {\n batches.push(addresses.slice(i, i + size) as Address[])\n }\n return batches\n}\n"],"mappings":";AAqLO,SAAS,OACd,OACA,MAC2C;AAC3C,SAAO,MAAM,SAAS;AACxB;AAMO,IAAM,oBAAoD;AAAA,EAC/D,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,aAAa;AACf;AAOO,IAAM,wBAAsE;AAAA,EACjF,MAAM,CAAC,UAAU,aAAa;AAAA,EAC9B,SAAS,CAAC,UAAU,eAAe,cAAc,aAAa;AAChE;;;ACzKA,IAAM,eAAe,CAAC,OAA8B,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAG7F,SAAS,iBAAiB,OAAyB;AACxD,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,kFAAkF,KAAK,OAAO;AACvG;AAGO,SAAS,oBAAoB,OAAyB;AAC3D,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,qGAAqG;AAAA,IAC1G;AAAA,EACF;AACF;AAmBA,eAAsB,cACpB,SACA,MACA,IACA,UAAgC,CAAC,GACnB;AACd,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,QAAQ,QAAQ,SAAS;AAE/B,QAAM,MAAM,OAAO,IAAY,OAA6B;AAC1D,QAAI,UAAU;AACd,eAAS;AACP,UAAI;AACF,eAAO,CAAC,GAAI,MAAM,QAAQ,IAAI,EAAE,CAAE;AAAA,MACpC,SAAS,OAAO;AACd,YAAI,iBAAiB,KAAK,GAAG;AAC3B,gBAAM,OAAO,KAAK;AAClB,cAAI,OAAO,WAAW,SAAS,GAAI,OAAM;AACzC,gBAAM,MAAM,KAAK,OAAO;AACxB,gBAAM,OAAO,MAAM,IAAI,IAAI,GAAG;AAC9B,gBAAM,QAAQ,MAAM,IAAI,MAAM,IAAI,EAAE;AACpC,iBAAO,CAAC,GAAG,MAAM,GAAG,KAAK;AAAA,QAC3B;AACA,YAAI,oBAAoB,KAAK,KAAK,UAAU,SAAS;AACnD,gBAAM,MAAM,YAAY,KAAK,OAAO;AACpC,qBAAW;AACX;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,KAAK,KAAM,QAAO,CAAC;AACvB,SAAO,IAAI,MAAM,EAAE;AACrB;AAOO,SAAS,eAAe,WAA+B,OAAO,KAAkB;AACrF,QAAM,UAAuB,CAAC;AAC9B,WAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK,MAAM;AAC/C,YAAQ,KAAK,UAAU,MAAM,GAAG,IAAI,IAAI,CAAc;AAAA,EACxD;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-3NHLSKZE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,72 @@
1
+ // src/events/pricing.ts
2
+ import { formatUnits } from "viem";
3
+ import {
4
+ MAINNET_ADDRESSES,
5
+ TESTNET_ADDRESSES,
6
+ USDG_DECIMALS,
7
+ quoteSwap
8
+ } from "hoodchain";
9
+ function quoteTokens(client) {
10
+ return client.network === "testnet" ? { weth: TESTNET_ADDRESSES.weth, usdg: TESTNET_ADDRESSES.usdg } : { weth: MAINNET_ADDRESSES.weth, usdg: MAINNET_ADDRESSES.usdg };
11
+ }
12
+ function createPriceOracle(client, options = {}) {
13
+ const ttlMs = options.ttlMs ?? 3e4;
14
+ const probeWei = options.probeWei ?? 10n ** 18n;
15
+ const now = options.now ?? Date.now;
16
+ const { weth, usdg } = quoteTokens(client);
17
+ let cached = null;
18
+ let cachedAt = 0;
19
+ let inflight = null;
20
+ async function refresh() {
21
+ try {
22
+ const quote = await quoteSwap(client, { tokenIn: weth, tokenOut: usdg, amountIn: probeWei });
23
+ const usdgOut = Number(formatUnits(quote.amountOut, USDG_DECIMALS));
24
+ const ethIn = Number(formatUnits(probeWei, 18));
25
+ const rate = ethIn > 0 ? usdgOut / ethIn : 0;
26
+ cached = rate > 0 ? rate : null;
27
+ } catch {
28
+ cached = null;
29
+ }
30
+ cachedAt = now();
31
+ return cached;
32
+ }
33
+ async function ethUsd() {
34
+ if (cachedAt !== 0 && now() - cachedAt < ttlMs) return cached;
35
+ inflight ??= refresh().finally(() => {
36
+ inflight = null;
37
+ });
38
+ return inflight;
39
+ }
40
+ return {
41
+ ethUsd,
42
+ async weiToUsd(wei) {
43
+ const rate = await ethUsd();
44
+ if (rate === null) return null;
45
+ return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * rate;
46
+ },
47
+ usdgToUsd(atomic) {
48
+ return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS));
49
+ }
50
+ };
51
+ }
52
+ function createStaticPriceOracle(ethUsdRate) {
53
+ return {
54
+ async ethUsd() {
55
+ return ethUsdRate;
56
+ },
57
+ async weiToUsd(wei) {
58
+ if (ethUsdRate === null) return null;
59
+ return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * ethUsdRate;
60
+ },
61
+ usdgToUsd(atomic) {
62
+ return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS));
63
+ }
64
+ };
65
+ }
66
+
67
+ export {
68
+ quoteTokens,
69
+ createPriceOracle,
70
+ createStaticPriceOracle
71
+ };
72
+ //# sourceMappingURL=chunk-6JQK5G3Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/events/pricing.ts"],"sourcesContent":["import { formatUnits, type Address } from 'viem'\nimport {\n MAINNET_ADDRESSES,\n TESTNET_ADDRESSES,\n USDG_DECIMALS,\n quoteSwap,\n type HoodClient,\n} from 'hoodchain'\n\n/**\n * USD valuation for alert events.\n *\n * There is no hardcoded price anywhere in this package. Every dollar figure\n * comes from live chain state:\n *\n * - **USDG legs are the unit of account.** USDG is Paxos' Global Dollar, a\n * fully reserved 1:1 USD stablecoin with 6 decimals. A USDG leg of a trade\n * is its own USD value, no oracle needed.\n * - **ETH legs are priced through the chain's own Uniswap v3 liquidity.** The\n * oracle asks `hoodchain`'s `quoteSwap` for the real output of selling 1\n * WETH into USDG across every fee tier and two-hop route, and takes the best\n * route's output as the ETH/USD rate. That is the price a trader would\n * actually get, sourced from the same pools the alerts watch.\n * - **Anything else is `null`.** A memecoin/memecoin pool with no USDG or WETH\n * leg has no honest USD value from on-chain data alone, so the event carries\n * `usdValue: null` and min-USD rules simply do not match it.\n *\n * Rates are cached for a short TTL so a burst of events in one block does not\n * re-quote the router once per event.\n */\n\n/** A source of USD rates for the two priceable legs. */\nexport interface PriceOracle {\n /**\n * USD per 1 ETH, or `null` when no WETH/USDG route has liquidity right now.\n * Never throws: a quote failure degrades to `null`, which downgrades USD\n * filters rather than dropping the alert pipeline.\n */\n ethUsd(): Promise<number | null>\n /** Convert a raw wei amount to USD, or `null` when the rate is unavailable. */\n weiToUsd(wei: bigint): Promise<number | null>\n /** Convert a raw USDG amount (6 decimals) to USD. Always available. */\n usdgToUsd(atomic: bigint): number\n}\n\n/** Options for {@link createPriceOracle}. */\nexport interface PriceOracleOptions {\n /**\n * How long a quoted ETH/USD rate stays fresh, in milliseconds.\n * @defaultValue `30_000`\n */\n ttlMs?: number\n /**\n * Probe size used to quote WETH into USDG. Larger sizes are less sensitive\n * to a thin tick but need deeper liquidity to fill.\n * @defaultValue `1` ETH\n */\n probeWei?: bigint\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\n/** WETH and USDG for the client's network. */\nexport function quoteTokens(client: HoodClient): { weth: Address; usdg: Address } {\n return client.network === 'testnet'\n ? { weth: TESTNET_ADDRESSES.weth, usdg: TESTNET_ADDRESSES.usdg }\n : { weth: MAINNET_ADDRESSES.weth, usdg: MAINNET_ADDRESSES.usdg }\n}\n\n/**\n * Build a caching ETH/USD oracle backed by real Uniswap v3 quotes.\n *\n * @example\n * ```ts\n * import { createHoodClient } from 'hoodchain'\n * import { createPriceOracle } from 'hood-alerts/events'\n *\n * const hood = createHoodClient()\n * const oracle = createPriceOracle(hood)\n * console.log('ETH/USD from chain liquidity:', await oracle.ethUsd())\n * ```\n */\nexport function createPriceOracle(client: HoodClient, options: PriceOracleOptions = {}): PriceOracle {\n const ttlMs = options.ttlMs ?? 30_000\n const probeWei = options.probeWei ?? 10n ** 18n\n const now = options.now ?? Date.now\n const { weth, usdg } = quoteTokens(client)\n\n let cached: number | null = null\n let cachedAt = 0\n let inflight: Promise<number | null> | null = null\n\n async function refresh(): Promise<number | null> {\n try {\n const quote = await quoteSwap(client, { tokenIn: weth, tokenOut: usdg, amountIn: probeWei })\n const usdgOut = Number(formatUnits(quote.amountOut, USDG_DECIMALS))\n const ethIn = Number(formatUnits(probeWei, 18))\n const rate = ethIn > 0 ? usdgOut / ethIn : 0\n cached = rate > 0 ? rate : null\n } catch {\n // No route or no liquidity right now. Report \"unknown\", never a guess.\n cached = null\n }\n cachedAt = now()\n return cached\n }\n\n async function ethUsd(): Promise<number | null> {\n if (cachedAt !== 0 && now() - cachedAt < ttlMs) return cached\n // Coalesce concurrent refreshes so a block full of events makes one quote.\n inflight ??= refresh().finally(() => {\n inflight = null\n })\n return inflight\n }\n\n return {\n ethUsd,\n async weiToUsd(wei: bigint): Promise<number | null> {\n const rate = await ethUsd()\n if (rate === null) return null\n return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * rate\n },\n usdgToUsd(atomic: bigint): number {\n return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS))\n },\n }\n}\n\n/**\n * A fixed-rate oracle. Useful when an operator already has a trusted ETH/USD\n * feed and wants alerts to agree with it, and for deterministic tests.\n */\nexport function createStaticPriceOracle(ethUsdRate: number | null): PriceOracle {\n return {\n async ethUsd() {\n return ethUsdRate\n },\n async weiToUsd(wei: bigint) {\n if (ethUsdRate === null) return null\n return Number(formatUnits(wei < 0n ? -wei : wei, 18)) * ethUsdRate\n },\n usdgToUsd(atomic: bigint) {\n return Number(formatUnits(atomic < 0n ? -atomic : atomic, USDG_DECIMALS))\n },\n }\n}\n"],"mappings":";AAAA,SAAS,mBAAiC;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAwDA,SAAS,YAAY,QAAsD;AAChF,SAAO,OAAO,YAAY,YACtB,EAAE,MAAM,kBAAkB,MAAM,MAAM,kBAAkB,KAAK,IAC7D,EAAE,MAAM,kBAAkB,MAAM,MAAM,kBAAkB,KAAK;AACnE;AAeO,SAAS,kBAAkB,QAAoB,UAA8B,CAAC,GAAgB;AACnG,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,WAAW,QAAQ,YAAY,OAAO;AAC5C,QAAM,MAAM,QAAQ,OAAO,KAAK;AAChC,QAAM,EAAE,MAAM,KAAK,IAAI,YAAY,MAAM;AAEzC,MAAI,SAAwB;AAC5B,MAAI,WAAW;AACf,MAAI,WAA0C;AAE9C,iBAAe,UAAkC;AAC/C,QAAI;AACF,YAAM,QAAQ,MAAM,UAAU,QAAQ,EAAE,SAAS,MAAM,UAAU,MAAM,UAAU,SAAS,CAAC;AAC3F,YAAM,UAAU,OAAO,YAAY,MAAM,WAAW,aAAa,CAAC;AAClE,YAAM,QAAQ,OAAO,YAAY,UAAU,EAAE,CAAC;AAC9C,YAAM,OAAO,QAAQ,IAAI,UAAU,QAAQ;AAC3C,eAAS,OAAO,IAAI,OAAO;AAAA,IAC7B,QAAQ;AAEN,eAAS;AAAA,IACX;AACA,eAAW,IAAI;AACf,WAAO;AAAA,EACT;AAEA,iBAAe,SAAiC;AAC9C,QAAI,aAAa,KAAK,IAAI,IAAI,WAAW,MAAO,QAAO;AAEvD,iBAAa,QAAQ,EAAE,QAAQ,MAAM;AACnC,iBAAW;AAAA,IACb,CAAC;AACD,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,SAAS,KAAqC;AAClD,YAAM,OAAO,MAAM,OAAO;AAC1B,UAAI,SAAS,KAAM,QAAO;AAC1B,aAAO,OAAO,YAAY,MAAM,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC,IAAI;AAAA,IAC1D;AAAA,IACA,UAAU,QAAwB;AAChC,aAAO,OAAO,YAAY,SAAS,KAAK,CAAC,SAAS,QAAQ,aAAa,CAAC;AAAA,IAC1E;AAAA,EACF;AACF;AAMO,SAAS,wBAAwB,YAAwC;AAC9E,SAAO;AAAA,IACL,MAAM,SAAS;AACb,aAAO;AAAA,IACT;AAAA,IACA,MAAM,SAAS,KAAa;AAC1B,UAAI,eAAe,KAAM,QAAO;AAChC,aAAO,OAAO,YAAY,MAAM,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC,IAAI;AAAA,IAC1D;AAAA,IACA,UAAU,QAAgB;AACxB,aAAO,OAAO,YAAY,SAAS,KAAK,CAAC,SAAS,QAAQ,aAAa,CAAC;AAAA,IAC1E;AAAA,EACF;AACF;","names":[]}