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,147 @@
1
+ import {
2
+ fetchLogRange
3
+ } from "./chunk-3LHCQA4Z.js";
4
+
5
+ // src/tiers/entitlements.ts
6
+ import { getAddress, verifyMessage } from "viem";
7
+ import { USDG_DECIMALS } from "hoodchain";
8
+ import { formatUnits } from "viem";
9
+ var free = (subscriberId, source) => ({
10
+ subscriberId,
11
+ tier: "free",
12
+ expiresAtMs: null,
13
+ source
14
+ });
15
+ function createStaticEntitlementProvider(options) {
16
+ const premium = new Set((options.premiumSubscribers ?? []).map((id) => id.trim()).filter(Boolean));
17
+ return {
18
+ async get(subscriberId) {
19
+ if (options.allPremium === true || premium.has(subscriberId)) {
20
+ return { subscriberId, tier: "premium", expiresAtMs: null, source: "static" };
21
+ }
22
+ return free(subscriberId, "static");
23
+ }
24
+ };
25
+ }
26
+ var transferEvent = {
27
+ type: "event",
28
+ name: "Transfer",
29
+ inputs: [
30
+ { name: "from", type: "address", indexed: true },
31
+ { name: "to", type: "address", indexed: true },
32
+ { name: "value", type: "uint256", indexed: false }
33
+ ]
34
+ };
35
+ function accrueExpiry(payments, pricePerPeriodUsdg, periodDays) {
36
+ if (pricePerPeriodUsdg <= 0) return null;
37
+ const periodMs = periodDays * 864e5;
38
+ const ordered = [...payments].sort((a, b) => a.atMs - b.atMs);
39
+ let expiry = null;
40
+ for (const payment of ordered) {
41
+ const periods = Math.floor(payment.amountUsdg / pricePerPeriodUsdg);
42
+ if (periods < 1) continue;
43
+ const start = expiry !== null && expiry > payment.atMs ? expiry : payment.atMs;
44
+ expiry = start + periods * periodMs;
45
+ }
46
+ return expiry;
47
+ }
48
+ function createUsdgEntitlementProvider(options) {
49
+ const ttlMs = options.ttlMs ?? 3e5;
50
+ const now = options.now ?? Date.now;
51
+ const periodDays = options.periodDays ?? 30;
52
+ const usdg = options.client.network === "testnet" ? "0x7E955252E15c84f5768B83c41a71F9eba181802F" : "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
53
+ const cache = /* @__PURE__ */ new Map();
54
+ async function resolve(subscriberId) {
55
+ const wallet = await options.links.walletOf(subscriberId);
56
+ if (!wallet) return free(subscriberId, "usdg:no-linked-wallet");
57
+ const head = await options.client.public.getBlockNumber();
58
+ const logs = await fetchLogRange(
59
+ (from, to) => options.client.public.getLogs({
60
+ address: usdg,
61
+ event: transferEvent,
62
+ args: { from: wallet, to: getAddress(options.receiver) },
63
+ fromBlock: from,
64
+ toBlock: to
65
+ }),
66
+ options.fromBlock ?? 0n,
67
+ head,
68
+ options.logOptions ?? {}
69
+ );
70
+ if (logs.length === 0) return free(subscriberId, "usdg:no-payments");
71
+ const payments = [];
72
+ for (const log of logs) {
73
+ const value = log.args.value;
74
+ if (value === void 0 || value === 0n) continue;
75
+ const block = await options.client.public.getBlock({
76
+ blockNumber: log.blockNumber,
77
+ includeTransactions: false
78
+ });
79
+ payments.push({
80
+ atMs: Number(block.timestamp) * 1e3,
81
+ amountUsdg: Number(formatUnits(value, USDG_DECIMALS))
82
+ });
83
+ }
84
+ const expiresAtMs = accrueExpiry(payments, options.pricePerPeriodUsdg, periodDays);
85
+ if (expiresAtMs === null || expiresAtMs <= now()) {
86
+ return { subscriberId, tier: "free", expiresAtMs, source: "usdg:lapsed" };
87
+ }
88
+ return { subscriberId, tier: "premium", expiresAtMs, source: "usdg" };
89
+ }
90
+ return {
91
+ async get(subscriberId) {
92
+ const hit = cache.get(subscriberId);
93
+ if (hit && now() - hit.at < ttlMs) return hit.entitlement;
94
+ try {
95
+ const entitlement = await resolve(subscriberId);
96
+ cache.set(subscriberId, { entitlement, at: now() });
97
+ return entitlement;
98
+ } catch (error) {
99
+ if (hit) return hit.entitlement;
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return free(subscriberId, `usdg:unavailable (${message})`);
102
+ }
103
+ }
104
+ };
105
+ }
106
+ function chainEntitlementProviders(...providers) {
107
+ return {
108
+ async get(subscriberId) {
109
+ let last = free(subscriberId, "chain:empty");
110
+ for (const provider of providers) {
111
+ const entitlement = await provider.get(subscriberId);
112
+ if (entitlement.tier === "premium") return entitlement;
113
+ last = entitlement;
114
+ }
115
+ return last;
116
+ }
117
+ };
118
+ }
119
+ function linkMessage(subscriberId, nonce) {
120
+ return `hood-alerts wallet link
121
+ subscriber: ${subscriberId}
122
+ nonce: ${nonce}`;
123
+ }
124
+ async function verifyWalletLink(subscriberId, nonce, address, signature) {
125
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return null;
126
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null;
127
+ try {
128
+ const valid = await verifyMessage({
129
+ address: getAddress(address),
130
+ message: linkMessage(subscriberId, nonce),
131
+ signature
132
+ });
133
+ return valid ? getAddress(address) : null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ export {
140
+ createStaticEntitlementProvider,
141
+ accrueExpiry,
142
+ createUsdgEntitlementProvider,
143
+ chainEntitlementProviders,
144
+ linkMessage,
145
+ verifyWalletLink
146
+ };
147
+ //# sourceMappingURL=chunk-W2GUAWUR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tiers/entitlements.ts"],"sourcesContent":["import { getAddress, verifyMessage, type Address } from 'viem'\nimport { USDG_DECIMALS, erc20Abi, type HoodClient } from 'hoodchain'\nimport { formatUnits } from 'viem'\nimport { fetchLogRange, type FetchLogRangeOptions } from '../events/logs.js'\nimport type { Tier } from './policy.js'\n\n/**\n * Entitlements: who is premium, and why.\n *\n * The service resolves a subscriber's tier through exactly one interface, so\n * there is a single place to audit and a single place to swap the commercial\n * model. Two implementations ship:\n *\n * 1. {@link createStaticEntitlementProvider} is the documented default. Premium\n * subscribers come from configuration (`PREMIUM_SUBSCRIBERS` in the\n * environment). It has no dependencies, works offline, and is what a\n * self-hoster running the bot for their own community wants.\n * 2. {@link createUsdgEntitlementProvider} is a working on-chain rail. It reads\n * USDG `Transfer` logs to the operator's receiving address from a wallet\n * the subscriber has linked, and accrues subscription time from those real\n * payments. It needs no facilitator, no card processor and no third-party\n * service: USDG is the chain's own dollar and the ledger is the chain.\n *\n * Wallet linking is signature-verified (EIP-191 `personal_sign` over a\n * server-issued nonce), so a subscriber cannot claim someone else's payments\n * by pasting their address.\n */\n\n/** A resolved entitlement. */\nexport interface Entitlement {\n subscriberId: string\n tier: Tier\n /** When premium lapses, ms since epoch. `null` for free or for a permanent grant. */\n expiresAtMs: number | null\n /** Which provider decided this, for support and for `/tier`. */\n source: string\n}\n\n/** The single entitlement chokepoint. */\nexport interface EntitlementProvider {\n /** Resolve a subscriber's tier. Must never throw: fall back to free. */\n get(subscriberId: string): Promise<Entitlement>\n}\n\nconst free = (subscriberId: string, source: string): Entitlement => ({\n subscriberId,\n tier: 'free',\n expiresAtMs: null,\n source,\n})\n\n/**\n * Configuration-driven entitlements: the documented default.\n *\n * @example\n * ```ts\n * const entitlements = createStaticEntitlementProvider({\n * premiumSubscribers: ['telegram:12345', 'discord:987654321'],\n * })\n * ```\n */\nexport function createStaticEntitlementProvider(options: {\n premiumSubscribers?: readonly string[]\n /** Treat everyone as premium. For a private self-hosted deployment. */\n allPremium?: boolean\n}): EntitlementProvider {\n const premium = new Set((options.premiumSubscribers ?? []).map((id) => id.trim()).filter(Boolean))\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n if (options.allPremium === true || premium.has(subscriberId)) {\n return { subscriberId, tier: 'premium', expiresAtMs: null, source: 'static' }\n }\n return free(subscriberId, 'static')\n },\n }\n}\n\n/** Lookup of the wallet a subscriber has verifiably linked. */\nexport interface WalletLinkStore {\n /** The linked, signature-verified wallet for a subscriber, or `null`. */\n walletOf(subscriberId: string): Promise<Address | null>\n}\n\n/** Options for {@link createUsdgEntitlementProvider}. */\nexport interface UsdgEntitlementOptions {\n client: HoodClient\n /** The operator's receiving address. Payments to any other address are ignored. */\n receiver: Address\n /** Price of one subscription period, in whole USDG. */\n pricePerPeriodUsdg: number\n /** Length of a period in days. @defaultValue `30` */\n periodDays?: number\n /** Where verified wallet links come from. */\n links: WalletLinkStore\n /** First block to scan for payments. @defaultValue `0n` */\n fromBlock?: bigint\n /** Cache lifetime per subscriber, in ms. @defaultValue `300_000` (5 minutes) */\n ttlMs?: number\n /** Retry/bisect options for the payment scan. */\n logOptions?: FetchLogRangeOptions\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nconst transferEvent = {\n type: 'event',\n name: 'Transfer',\n inputs: [\n { name: 'from', type: 'address', indexed: true },\n { name: 'to', type: 'address', indexed: true },\n { name: 'value', type: 'uint256', indexed: false },\n ],\n} as const\n\n/**\n * Accrue an expiry from a payment history. Each payment buys\n * `floor(amount / price)` periods, starting from whenever the current\n * entitlement would have lapsed, so renewing early extends rather than\n * overwrites.\n *\n * Exported because it is the part worth testing on its own: everything else in\n * the provider is a log query.\n */\nexport function accrueExpiry(\n payments: readonly { atMs: number; amountUsdg: number }[],\n pricePerPeriodUsdg: number,\n periodDays: number,\n): number | null {\n if (pricePerPeriodUsdg <= 0) return null\n const periodMs = periodDays * 86_400_000\n const ordered = [...payments].sort((a, b) => a.atMs - b.atMs)\n let expiry: number | null = null\n for (const payment of ordered) {\n const periods = Math.floor(payment.amountUsdg / pricePerPeriodUsdg)\n if (periods < 1) continue\n const start: number = expiry !== null && expiry > payment.atMs ? expiry : payment.atMs\n expiry = start + periods * periodMs\n }\n return expiry\n}\n\n/**\n * On-chain USDG entitlements.\n *\n * @example\n * ```ts\n * const entitlements = createUsdgEntitlementProvider({\n * client: hood,\n * receiver: '0xYourReceivingAddress',\n * pricePerPeriodUsdg: 25,\n * links: store, // the service's SQLite store implements WalletLinkStore\n * })\n * const entitlement = await entitlements.get('telegram:12345')\n * ```\n */\nexport function createUsdgEntitlementProvider(options: UsdgEntitlementOptions): EntitlementProvider {\n const ttlMs = options.ttlMs ?? 300_000\n const now = options.now ?? Date.now\n const periodDays = options.periodDays ?? 30\n const usdg =\n options.client.network === 'testnet'\n ? '0x7E955252E15c84f5768B83c41a71F9eba181802F'\n : '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'\n const cache = new Map<string, { entitlement: Entitlement; at: number }>()\n\n async function resolve(subscriberId: string): Promise<Entitlement> {\n const wallet = await options.links.walletOf(subscriberId)\n if (!wallet) return free(subscriberId, 'usdg:no-linked-wallet')\n\n const head = await options.client.public.getBlockNumber()\n const logs = await fetchLogRange(\n (from, to) =>\n options.client.public.getLogs({\n address: usdg as Address,\n event: transferEvent,\n args: { from: wallet, to: getAddress(options.receiver) },\n fromBlock: from,\n toBlock: to,\n }),\n options.fromBlock ?? 0n,\n head,\n options.logOptions ?? {},\n )\n if (logs.length === 0) return free(subscriberId, 'usdg:no-payments')\n\n const payments: { atMs: number; amountUsdg: number }[] = []\n for (const log of logs) {\n const value = log.args.value as bigint | undefined\n if (value === undefined || value === 0n) continue\n const block = await options.client.public.getBlock({\n blockNumber: log.blockNumber,\n includeTransactions: false,\n })\n payments.push({\n atMs: Number(block.timestamp) * 1000,\n amountUsdg: Number(formatUnits(value, USDG_DECIMALS)),\n })\n }\n\n const expiresAtMs = accrueExpiry(payments, options.pricePerPeriodUsdg, periodDays)\n if (expiresAtMs === null || expiresAtMs <= now()) {\n return { subscriberId, tier: 'free', expiresAtMs, source: 'usdg:lapsed' }\n }\n return { subscriberId, tier: 'premium', expiresAtMs, source: 'usdg' }\n }\n\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n const hit = cache.get(subscriberId)\n if (hit && now() - hit.at < ttlMs) return hit.entitlement\n try {\n const entitlement = await resolve(subscriberId)\n cache.set(subscriberId, { entitlement, at: now() })\n return entitlement\n } catch (error) {\n // An RPC failure must never silently downgrade a paying subscriber to\n // free with no explanation, and must never upgrade a free one. Serve\n // the last known answer if there is one, otherwise free with a source\n // that says exactly what happened.\n if (hit) return hit.entitlement\n const message = error instanceof Error ? error.message : String(error)\n return free(subscriberId, `usdg:unavailable (${message})`)\n }\n },\n }\n}\n\n/**\n * Chain several providers: the first one that returns premium wins. Lets an\n * operator grant comps by configuration while still selling on chain.\n */\nexport function chainEntitlementProviders(\n ...providers: readonly EntitlementProvider[]\n): EntitlementProvider {\n return {\n async get(subscriberId: string): Promise<Entitlement> {\n let last: Entitlement = free(subscriberId, 'chain:empty')\n for (const provider of providers) {\n const entitlement = await provider.get(subscriberId)\n if (entitlement.tier === 'premium') return entitlement\n last = entitlement\n }\n return last\n },\n }\n}\n\n/** The message a subscriber signs to link a wallet. */\nexport function linkMessage(subscriberId: string, nonce: string): string {\n return `hood-alerts wallet link\\nsubscriber: ${subscriberId}\\nnonce: ${nonce}`\n}\n\n/**\n * Verify a wallet link signature (EIP-191 `personal_sign`).\n *\n * The subscriber signs {@link linkMessage} with the wallet that pays, and the\n * service checks the signature before storing the link. Without this step,\n * anyone could paste a paying subscriber's address and inherit their\n * entitlement.\n *\n * @returns the checksummed address on success, `null` on any failure (bad\n * signature shape, wrong signer, malformed address).\n *\n * @example\n * ```ts\n * const nonce = crypto.randomUUID()\n * // the user signs linkMessage('telegram:12345', nonce) in their wallet\n * const address = await verifyWalletLink('telegram:12345', nonce, wallet, signature)\n * if (address) await store.linkWallet('telegram:12345', address)\n * ```\n */\nexport async function verifyWalletLink(\n subscriberId: string,\n nonce: string,\n address: string,\n signature: string,\n): Promise<Address | null> {\n if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) return null\n if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return null\n try {\n const valid = await verifyMessage({\n address: getAddress(address),\n message: linkMessage(subscriberId, nonce),\n signature: signature as `0x${string}`,\n })\n return valid ? getAddress(address) : null\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,355 @@
1
+ import {
2
+ quoteTokens
3
+ } from "./chunk-6JQK5G3Z.js";
4
+ import {
5
+ fetchLogRange
6
+ } from "./chunk-3LHCQA4Z.js";
7
+
8
+ // src/rules/reputation.ts
9
+ import { formatUnits, getAddress } from "viem";
10
+ import {
11
+ MAINNET_ADDRESSES,
12
+ NOXA_ADDRESSES,
13
+ ODYSSEY_ADDRESSES,
14
+ USDG_DECIMALS,
15
+ erc20Abi,
16
+ noxaTokenLaunchedEvent,
17
+ odysseyTokenCreatedEvent
18
+ } from "hoodchain";
19
+ var positionManagerAbi = [
20
+ {
21
+ type: "function",
22
+ name: "ownerOf",
23
+ stateMutability: "view",
24
+ inputs: [{ name: "tokenId", type: "uint256" }],
25
+ outputs: [{ type: "address" }]
26
+ }
27
+ ];
28
+ var ODYSSEY_FACTORIES = [
29
+ ODYSSEY_ADDRESSES.bondingCurveFactory,
30
+ ODYSSEY_ADDRESSES.reflectionFactory,
31
+ ODYSSEY_ADDRESSES.instantFactory
32
+ ];
33
+ function createRpcReputationProvider(client, oracle, options = {}) {
34
+ const ttlMs = options.ttlMs ?? 6e5;
35
+ const maxHistory = options.maxHistory ?? 200;
36
+ const maxInspect = options.maxInspect ?? 25;
37
+ const rugThresholdUsd = options.rugThresholdUsd ?? 50;
38
+ const now = options.now ?? Date.now;
39
+ const cache = /* @__PURE__ */ new Map();
40
+ const inflight = /* @__PURE__ */ new Map();
41
+ const lockCache = /* @__PURE__ */ new Map();
42
+ const { weth, usdg } = quoteTokens(client);
43
+ async function reserveUsd(pool, quoteToken) {
44
+ const quote = quoteToken.toLowerCase();
45
+ if (quote !== weth.toLowerCase() && quote !== usdg.toLowerCase()) return null;
46
+ const balance = await client.public.readContract({
47
+ address: quoteToken,
48
+ abi: erc20Abi,
49
+ functionName: "balanceOf",
50
+ args: [pool]
51
+ });
52
+ if (quote === usdg.toLowerCase()) return Number(formatUnits(balance, USDG_DECIMALS));
53
+ return oracle.weiToUsd(balance);
54
+ }
55
+ async function isLpLocked(positionId) {
56
+ const key = positionId.toString();
57
+ const hit = lockCache.get(key);
58
+ if (hit !== void 0) return hit;
59
+ try {
60
+ const owner = await client.public.readContract({
61
+ address: MAINNET_ADDRESSES.nonfungiblePositionManager,
62
+ abi: positionManagerAbi,
63
+ functionName: "ownerOf",
64
+ args: [positionId]
65
+ });
66
+ const locked = owner.toLowerCase() === NOXA_ADDRESSES.locker.toLowerCase();
67
+ lockCache.set(key, locked);
68
+ return locked;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ async function load(deployer) {
74
+ const head = await client.public.getBlockNumber();
75
+ const [noxaLogs, odysseyLogs] = await Promise.all([
76
+ fetchLogRange(
77
+ (from, to) => client.public.getLogs({
78
+ address: NOXA_ADDRESSES.launchFactory,
79
+ event: noxaTokenLaunchedEvent,
80
+ args: { deployer },
81
+ fromBlock: from,
82
+ toBlock: to
83
+ }),
84
+ NOXA_ADDRESSES.deployBlock,
85
+ head,
86
+ options.logOptions ?? {}
87
+ ),
88
+ fetchLogRange(
89
+ (from, to) => client.public.getLogs({
90
+ address: ODYSSEY_FACTORIES,
91
+ event: odysseyTokenCreatedEvent,
92
+ args: { creator: deployer },
93
+ fromBlock: from,
94
+ toBlock: to
95
+ }),
96
+ NOXA_ADDRESSES.deployBlock,
97
+ head,
98
+ options.logOptions ?? {}
99
+ )
100
+ ]);
101
+ const launches = [];
102
+ for (const log of noxaLogs) {
103
+ const token = log.args.token;
104
+ if (!token) continue;
105
+ const pool = log.args.pool ?? null;
106
+ const pairToken = log.args.pairToken ?? null;
107
+ launches.push({
108
+ launchpad: "noxa",
109
+ token: getAddress(token),
110
+ pool: pool ? getAddress(pool) : null,
111
+ quoteToken: pairToken ? getAddress(pairToken) : null,
112
+ positionId: log.args.positionId ?? null,
113
+ blockNumber: log.blockNumber
114
+ });
115
+ }
116
+ for (const log of odysseyLogs) {
117
+ const token = log.args.token;
118
+ if (!token) continue;
119
+ launches.push({
120
+ launchpad: "odyssey",
121
+ token: getAddress(token),
122
+ pool: null,
123
+ quoteToken: null,
124
+ positionId: null,
125
+ blockNumber: log.blockNumber
126
+ });
127
+ }
128
+ launches.sort((a, b) => a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : 0);
129
+ const kept = launches.slice(-maxHistory);
130
+ const inspectable = kept.slice(-maxInspect);
131
+ let lockedLaunches = 0;
132
+ let ruggedLaunches = 0;
133
+ for (const launch of inspectable) {
134
+ if (launch.positionId !== null && await isLpLocked(launch.positionId)) lockedLaunches += 1;
135
+ if (launch.pool && launch.quoteToken) {
136
+ try {
137
+ const usd = await reserveUsd(launch.pool, launch.quoteToken);
138
+ if (usd !== null && usd < rugThresholdUsd) ruggedLaunches += 1;
139
+ } catch {
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ deployer: getAddress(deployer),
145
+ launches: kept,
146
+ lockedLaunches,
147
+ ruggedLaunches,
148
+ inspected: inspectable.length,
149
+ computedAtMs: now()
150
+ };
151
+ }
152
+ return {
153
+ async get(deployer) {
154
+ const key = deployer.toLowerCase();
155
+ const hit = cache.get(key);
156
+ if (hit && now() - hit.computedAtMs < ttlMs) return hit;
157
+ const pending = inflight.get(key);
158
+ if (pending) return pending;
159
+ const promise = load(deployer).then((record) => {
160
+ cache.set(key, record);
161
+ return record;
162
+ }).finally(() => {
163
+ inflight.delete(key);
164
+ });
165
+ inflight.set(key, promise);
166
+ return promise;
167
+ },
168
+ isLpLocked
169
+ };
170
+ }
171
+ function priorLaunchesBefore(record, blockNumber) {
172
+ return record.launches.filter((launch) => launch.blockNumber < blockNumber).length;
173
+ }
174
+
175
+ // src/rules/engine.ts
176
+ var matchOk = (rule) => ({ matched: true, reason: `rule ${rule.id} matched` });
177
+ var miss = (reason) => ({ matched: false, reason });
178
+ function eventSide(event) {
179
+ if (event.kind === "curve_trade" || event.kind === "whale_trade") return event.side;
180
+ return null;
181
+ }
182
+ function eventPositionId(event) {
183
+ if (event.kind === "launch") return event.positionId;
184
+ if (event.kind === "graduation") return event.positionId;
185
+ return null;
186
+ }
187
+ async function evaluateRule(rule, event, context = {}) {
188
+ if (!rule.enabled) return miss(`rule ${rule.id} is disabled`);
189
+ if (!rule.kinds.includes(event.kind)) {
190
+ return miss(`kind ${event.kind} not in [${rule.kinds.join(", ")}]`);
191
+ }
192
+ if (!rule.launchpads.includes(event.launchpad)) {
193
+ return miss(`launchpad ${event.launchpad} not in [${rule.launchpads.join(", ")}]`);
194
+ }
195
+ const actor = event.actor.toLowerCase();
196
+ if (rule.excludeDeployers.includes(actor)) {
197
+ return miss(`actor ${event.actor} is on the exclude list`);
198
+ }
199
+ if (rule.tokens.length > 0 && !rule.tokens.includes(event.token.toLowerCase())) {
200
+ return miss(`token ${event.token} is not on the watchlist`);
201
+ }
202
+ if (rule.deployers.length > 0 && !rule.deployers.includes(actor)) {
203
+ return miss(`actor ${event.actor} is not on the deployer watchlist`);
204
+ }
205
+ if (rule.side !== "any") {
206
+ const side = eventSide(event);
207
+ if (side === null) return miss(`side filter does not apply to ${event.kind} events`);
208
+ if (side !== rule.side) return miss(`side ${side} is not ${rule.side}`);
209
+ }
210
+ if (rule.minUsd !== void 0) {
211
+ if (event.usdValue === null) return miss("event has no USD value to compare against minUsd");
212
+ if (event.usdValue < rule.minUsd) {
213
+ return miss(`usdValue ${event.usdValue.toFixed(2)} below minUsd ${rule.minUsd}`);
214
+ }
215
+ }
216
+ if (rule.maxUsd !== void 0) {
217
+ if (event.usdValue === null) return miss("event has no USD value to compare against maxUsd");
218
+ if (event.usdValue > rule.maxUsd) {
219
+ return miss(`usdValue ${event.usdValue.toFixed(2)} above maxUsd ${rule.maxUsd}`);
220
+ }
221
+ }
222
+ if (rule.minLiquidityUsd !== void 0 || rule.maxLiquidityUsd !== void 0) {
223
+ if (!context.liquidityUsd) {
224
+ return miss("rule filters on liquidity but no liquidity reader was provided");
225
+ }
226
+ const liquidity = await context.liquidityUsd(event);
227
+ if (liquidity === null) return miss("token liquidity is unknown");
228
+ if (rule.minLiquidityUsd !== void 0 && liquidity < rule.minLiquidityUsd) {
229
+ return miss(`liquidity ${liquidity.toFixed(2)} below minLiquidityUsd ${rule.minLiquidityUsd}`);
230
+ }
231
+ if (rule.maxLiquidityUsd !== void 0 && liquidity > rule.maxLiquidityUsd) {
232
+ return miss(`liquidity ${liquidity.toFixed(2)} above maxLiquidityUsd ${rule.maxLiquidityUsd}`);
233
+ }
234
+ }
235
+ const reputationFilter = rule.reputation;
236
+ if (reputationFilter) {
237
+ const needsRecord = reputationFilter.minPriorLaunches !== void 0 || reputationFilter.maxPriorLaunches !== void 0 || reputationFilter.maxRuggedLaunches !== void 0;
238
+ if (needsRecord) {
239
+ if (!context.reputation) {
240
+ return miss("rule filters on reputation but no reputation provider was configured");
241
+ }
242
+ const record = await context.reputation(event.actor);
243
+ const prior = priorLaunchesBefore(record, event.blockNumber);
244
+ if (reputationFilter.minPriorLaunches !== void 0 && prior < reputationFilter.minPriorLaunches) {
245
+ return miss(`deployer had ${prior} prior launches, needs ${reputationFilter.minPriorLaunches}`);
246
+ }
247
+ if (reputationFilter.maxPriorLaunches !== void 0 && prior > reputationFilter.maxPriorLaunches) {
248
+ return miss(`deployer had ${prior} prior launches, max is ${reputationFilter.maxPriorLaunches}`);
249
+ }
250
+ if (reputationFilter.maxRuggedLaunches !== void 0 && record.ruggedLaunches > reputationFilter.maxRuggedLaunches) {
251
+ return miss(
252
+ `deployer has ${record.ruggedLaunches} drained pools, max is ${reputationFilter.maxRuggedLaunches}`
253
+ );
254
+ }
255
+ }
256
+ if (reputationFilter.requireLpLocked === true) {
257
+ const positionId = eventPositionId(event);
258
+ if (positionId === null) return miss("event carries no LP position to check for a lock");
259
+ if (!context.isLpLocked) {
260
+ return miss("rule requires an LP lock check but no reputation provider was configured");
261
+ }
262
+ if (!await context.isLpLocked(positionId)) {
263
+ return miss(`LP position ${positionId} is not held by the launchpad locker`);
264
+ }
265
+ }
266
+ }
267
+ return matchOk(rule);
268
+ }
269
+ async function matchRules(rules, event, context = {}) {
270
+ const results = await Promise.all(
271
+ rules.map(async (rule) => {
272
+ try {
273
+ return { rule, result: await evaluateRule(rule, event, context) };
274
+ } catch (error) {
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ return { rule, result: miss(`evaluation failed: ${message}`) };
277
+ }
278
+ })
279
+ );
280
+ return results.filter((entry) => entry.result.matched);
281
+ }
282
+
283
+ // src/rules/ratelimit.ts
284
+ var ALLOWED = { allowed: true, reason: null, retryAtMs: null };
285
+ async function checkRateLimit(store, key, config, nowMs) {
286
+ if (!config) return ALLOWED;
287
+ if (config.minIntervalSeconds !== void 0) {
288
+ const last = await store.lastAt(key);
289
+ if (last !== null) {
290
+ const nextAllowed = last + config.minIntervalSeconds * 1e3;
291
+ if (nowMs < nextAllowed) {
292
+ return {
293
+ allowed: false,
294
+ reason: `minIntervalSeconds=${config.minIntervalSeconds} not elapsed`,
295
+ retryAtMs: nextAllowed
296
+ };
297
+ }
298
+ }
299
+ }
300
+ if (config.maxPerHour !== void 0) {
301
+ const windowStart = nowMs - 36e5;
302
+ const used = await store.countSince(key, windowStart);
303
+ if (used >= config.maxPerHour) {
304
+ return {
305
+ allowed: false,
306
+ reason: `maxPerHour=${config.maxPerHour} reached (${used} in the last hour)`,
307
+ retryAtMs: windowStart + 36e5
308
+ };
309
+ }
310
+ }
311
+ return ALLOWED;
312
+ }
313
+ function createMemoryRateLimitStore() {
314
+ const byKey = /* @__PURE__ */ new Map();
315
+ const bySubscriber = /* @__PURE__ */ new Map();
316
+ const push = (map, id, atMs) => {
317
+ const list = map.get(id) ?? [];
318
+ list.push(atMs);
319
+ map.set(
320
+ id,
321
+ list.filter((at) => at >= atMs - 36e5)
322
+ );
323
+ };
324
+ return {
325
+ async countSince(key, sinceMs) {
326
+ return (byKey.get(key) ?? []).filter((at) => at >= sinceMs).length;
327
+ },
328
+ async lastAt(key) {
329
+ const list = byKey.get(key);
330
+ if (!list || list.length === 0) return null;
331
+ return list[list.length - 1] ?? null;
332
+ },
333
+ async record(key, subscriberId, atMs) {
334
+ push(byKey, key, atMs);
335
+ push(bySubscriber, subscriberId, atMs);
336
+ },
337
+ async countForSubscriberSince(subscriberId, sinceMs) {
338
+ return (bySubscriber.get(subscriberId) ?? []).filter((at) => at >= sinceMs).length;
339
+ }
340
+ };
341
+ }
342
+ function rateLimitKey(subscriptionId, ruleId) {
343
+ return `${subscriptionId}:${ruleId}`;
344
+ }
345
+
346
+ export {
347
+ createRpcReputationProvider,
348
+ priorLaunchesBefore,
349
+ evaluateRule,
350
+ matchRules,
351
+ checkRateLimit,
352
+ createMemoryRateLimitStore,
353
+ rateLimitKey
354
+ };
355
+ //# sourceMappingURL=chunk-Y2M3USWZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules/reputation.ts","../src/rules/engine.ts","../src/rules/ratelimit.ts"],"sourcesContent":["import { formatUnits, getAddress, type Address } from 'viem'\nimport {\n MAINNET_ADDRESSES,\n NOXA_ADDRESSES,\n ODYSSEY_ADDRESSES,\n USDG_DECIMALS,\n erc20Abi,\n noxaTokenLaunchedEvent,\n odysseyTokenCreatedEvent,\n type HoodClient,\n} from 'hoodchain'\nimport { fetchLogRange, type FetchLogRangeOptions } from '../events/logs.js'\nimport { quoteTokens, type PriceOracle } from '../events/pricing.js'\nimport type { Launchpad } from '../events/types.js'\n\n/**\n * Deployer reputation, derived entirely from chain state.\n *\n * Nothing here is scraped, self-reported or scored by a model. Three\n * measurements, each with an exact on-chain definition:\n *\n * - **Prior launches**: `TokenLaunched` logs on the NOXA factory with\n * `deployer` indexed to this address, plus `TokenCreated` logs on the three\n * Odyssey factories with `creator` indexed to this address. Both queries are\n * topic-selective, so the RPC serves them across the full chain history in\n * one call each.\n * - **LP locked**: for a NOXA launch, the LP position NFT is locked when\n * `NonfungiblePositionManager.ownerOf(positionId)` is the NOXA locker\n * contract. That is the launchpad's own permanent-lock mechanism, read\n * directly rather than trusted from a UI badge.\n * - **Rugged**: a prior launch whose pool quote-token reserve has since fallen\n * below `rugThresholdUsd`. Liquidity that was seeded and is now gone is the\n * observable footprint of a pull. It is a heuristic and is labelled as one\n * everywhere it surfaces: a token that simply never traded and a token whose\n * liquidity was withdrawn both end up with an empty pool, and only the\n * second is a rug. Pair it with `minPriorLaunches` for a meaningful signal.\n *\n * Results are cached per deployer, because a launch burst from one deployer\n * would otherwise re-run the same history scan per event.\n */\n\n/** One prior launch by a deployer. */\nexport interface PriorLaunch {\n launchpad: Launchpad\n token: Address\n pool: Address | null\n quoteToken: Address | null\n /** NOXA LP position id, when the launch had one. */\n positionId: bigint | null\n blockNumber: bigint\n}\n\n/** A deployer's on-chain track record. */\nexport interface DeployerReputation {\n deployer: Address\n /** Every launch found, oldest first. Capped by `maxHistory`. */\n launches: PriorLaunch[]\n /** Launches whose LP position is held by the NOXA locker. */\n lockedLaunches: number\n /** Launches whose pool reserve is now below the rug threshold. */\n ruggedLaunches: number\n /**\n * How many launches were actually inspected for lock/rug status. Lower than\n * `launches.length` when the history exceeds `maxInspect`, and surfaced so\n * consumers never read a partial scan as a clean record.\n */\n inspected: number\n /** When this record was computed, ms since epoch. */\n computedAtMs: number\n}\n\n/** Reputation lookups for the rule engine. */\nexport interface ReputationProvider {\n get(deployer: Address): Promise<DeployerReputation>\n /** Is a specific LP position locked by the launchpad locker? */\n isLpLocked(positionId: bigint): Promise<boolean>\n}\n\n/** Options for {@link createRpcReputationProvider}. */\nexport interface ReputationProviderOptions {\n /** Cache lifetime per deployer, in ms. @defaultValue `600_000` (10 minutes) */\n ttlMs?: number\n /** Max launches kept per deployer. @defaultValue `200` */\n maxHistory?: number\n /**\n * Max prior launches inspected for lock/rug status per deployer. Each one\n * costs a pool balance read, so this bounds the RPC cost of a rule.\n * @defaultValue `25`\n */\n maxInspect?: number\n /** Pool reserve below this USD value counts as drained. @defaultValue `50` */\n rugThresholdUsd?: number\n /** Retry/bisect options for the history queries. */\n logOptions?: FetchLogRangeOptions\n /** Clock injection point for tests. @defaultValue `Date.now` */\n now?: () => number\n}\n\nconst positionManagerAbi = [\n {\n type: 'function',\n name: 'ownerOf',\n stateMutability: 'view',\n inputs: [{ name: 'tokenId', type: 'uint256' }],\n outputs: [{ type: 'address' }],\n },\n] as const\n\nconst ODYSSEY_FACTORIES: Address[] = [\n ODYSSEY_ADDRESSES.bondingCurveFactory,\n ODYSSEY_ADDRESSES.reflectionFactory,\n ODYSSEY_ADDRESSES.instantFactory,\n]\n\n/**\n * Build an RPC-backed reputation provider.\n *\n * @example\n * ```ts\n * const reputation = createRpcReputationProvider(hood, oracle)\n * const record = await reputation.get('0xDeployer…')\n * console.log(record.launches.length, 'prior launches,', record.ruggedLaunches, 'drained')\n * ```\n */\nexport function createRpcReputationProvider(\n client: HoodClient,\n oracle: PriceOracle,\n options: ReputationProviderOptions = {},\n): ReputationProvider {\n const ttlMs = options.ttlMs ?? 600_000\n const maxHistory = options.maxHistory ?? 200\n const maxInspect = options.maxInspect ?? 25\n const rugThresholdUsd = options.rugThresholdUsd ?? 50\n const now = options.now ?? Date.now\n const cache = new Map<string, DeployerReputation>()\n const inflight = new Map<string, Promise<DeployerReputation>>()\n const lockCache = new Map<string, boolean>()\n const { weth, usdg } = quoteTokens(client)\n\n async function reserveUsd(pool: Address, quoteToken: Address): Promise<number | null> {\n const quote = quoteToken.toLowerCase()\n if (quote !== weth.toLowerCase() && quote !== usdg.toLowerCase()) return null\n const balance = await client.public.readContract({\n address: quoteToken,\n abi: erc20Abi,\n functionName: 'balanceOf',\n args: [pool],\n })\n if (quote === usdg.toLowerCase()) return Number(formatUnits(balance, USDG_DECIMALS))\n return oracle.weiToUsd(balance)\n }\n\n async function isLpLocked(positionId: bigint): Promise<boolean> {\n const key = positionId.toString()\n const hit = lockCache.get(key)\n if (hit !== undefined) return hit\n try {\n const owner = await client.public.readContract({\n address: MAINNET_ADDRESSES.nonfungiblePositionManager,\n abi: positionManagerAbi,\n functionName: 'ownerOf',\n args: [positionId],\n })\n const locked = owner.toLowerCase() === NOXA_ADDRESSES.locker.toLowerCase()\n lockCache.set(key, locked)\n return locked\n } catch {\n // A burned or non-existent position reverts. Unknown is not locked.\n return false\n }\n }\n\n async function load(deployer: Address): Promise<DeployerReputation> {\n const head = await client.public.getBlockNumber()\n const [noxaLogs, odysseyLogs] = await Promise.all([\n fetchLogRange(\n (from, to) =>\n client.public.getLogs({\n address: NOXA_ADDRESSES.launchFactory,\n event: noxaTokenLaunchedEvent,\n args: { deployer },\n fromBlock: from,\n toBlock: to,\n }),\n NOXA_ADDRESSES.deployBlock,\n head,\n options.logOptions ?? {},\n ),\n fetchLogRange(\n (from, to) =>\n client.public.getLogs({\n address: ODYSSEY_FACTORIES,\n event: odysseyTokenCreatedEvent,\n args: { creator: deployer },\n fromBlock: from,\n toBlock: to,\n }),\n NOXA_ADDRESSES.deployBlock,\n head,\n options.logOptions ?? {},\n ),\n ])\n\n const launches: PriorLaunch[] = []\n for (const log of noxaLogs) {\n const token = log.args.token as Address | undefined\n if (!token) continue\n const pool = (log.args.pool as Address | undefined) ?? null\n const pairToken = (log.args.pairToken as Address | undefined) ?? null\n launches.push({\n launchpad: 'noxa',\n token: getAddress(token),\n pool: pool ? getAddress(pool) : null,\n quoteToken: pairToken ? getAddress(pairToken) : null,\n positionId: (log.args.positionId as bigint | undefined) ?? null,\n blockNumber: log.blockNumber,\n })\n }\n for (const log of odysseyLogs) {\n const token = log.args.token as Address | undefined\n if (!token) continue\n launches.push({\n launchpad: 'odyssey',\n token: getAddress(token),\n pool: null,\n quoteToken: null,\n positionId: null,\n blockNumber: log.blockNumber,\n })\n }\n launches.sort((a, b) => (a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : 0))\n const kept = launches.slice(-maxHistory)\n\n // Inspect the most recent launches for lock and drain status.\n const inspectable = kept.slice(-maxInspect)\n let lockedLaunches = 0\n let ruggedLaunches = 0\n for (const launch of inspectable) {\n if (launch.positionId !== null && (await isLpLocked(launch.positionId))) lockedLaunches += 1\n if (launch.pool && launch.quoteToken) {\n try {\n const usd = await reserveUsd(launch.pool, launch.quoteToken)\n if (usd !== null && usd < rugThresholdUsd) ruggedLaunches += 1\n } catch {\n // An unreadable pool is not evidence of a rug. Skip it.\n }\n }\n }\n\n return {\n deployer: getAddress(deployer),\n launches: kept,\n lockedLaunches,\n ruggedLaunches,\n inspected: inspectable.length,\n computedAtMs: now(),\n }\n }\n\n return {\n async get(deployer: Address): Promise<DeployerReputation> {\n const key = deployer.toLowerCase()\n const hit = cache.get(key)\n if (hit && now() - hit.computedAtMs < ttlMs) return hit\n const pending = inflight.get(key)\n if (pending) return pending\n const promise = load(deployer)\n .then((record) => {\n cache.set(key, record)\n return record\n })\n .finally(() => {\n inflight.delete(key)\n })\n inflight.set(key, promise)\n return promise\n },\n isLpLocked,\n }\n}\n\n/**\n * Count a deployer's launches that happened strictly before a block. Rules ask\n * \"how many had they launched *at the time of this event*\", which is not the\n * same as \"how many have they launched now\", and only the first is stable when\n * a historical range is re-processed after a restart.\n */\nexport function priorLaunchesBefore(record: DeployerReputation, blockNumber: bigint): number {\n return record.launches.filter((launch) => launch.blockNumber < blockNumber).length\n}\n","import type { Address } from 'viem'\nimport type { AlertEvent } from '../events/types.js'\nimport { priorLaunchesBefore, type DeployerReputation } from './reputation.js'\nimport type { Rule } from './schema.js'\n\n/**\n * The rule engine: does this event satisfy this rule?\n *\n * Two properties matter more than the filter list itself.\n *\n * **Lazy, ordered evaluation.** Cheap in-memory filters (kind, launchpad,\n * watchlists, USD bounds) run before anything that costs an RPC call\n * (liquidity, reputation). A rule that rejects an event on its kind never\n * triggers a pool balance read. With thousands of subscriptions this is the\n * difference between a service that keeps up with the chain and one that does\n * not.\n *\n * **Unknown is not a pass.** Every USD and liquidity figure can legitimately\n * be `null` (no USDG or WETH leg to price against, an unreadable pool). A\n * filter that asks for a measurement the chain did not give us does not match.\n * The alternative, treating unknown as zero or as infinity, silently satisfies\n * either `minUsd` or `maxLiquidityUsd` and ships alerts that are simply wrong.\n */\n\n/** Lazily-invoked lookups the engine uses only when a rule needs them. */\nexport interface EvaluationContext {\n /** USD liquidity of the event token's deepest known pool. */\n liquidityUsd?: (event: AlertEvent) => Promise<number | null>\n /** On-chain track record for a deployer. */\n reputation?: (deployer: Address) => Promise<DeployerReputation>\n /** Is this LP position held by the launchpad locker? */\n isLpLocked?: (positionId: bigint) => Promise<boolean>\n}\n\n/** The outcome of evaluating one rule against one event. */\nexport interface MatchResult {\n matched: boolean\n /**\n * Why. On a match this names the rule; on a miss it names the first filter\n * that rejected the event. The bot's `/status` and the service's debug log\n * both surface this, so \"why didn't I get an alert\" is answerable.\n */\n reason: string\n}\n\nconst matchOk = (rule: Rule): MatchResult => ({ matched: true, reason: `rule ${rule.id} matched` })\nconst miss = (reason: string): MatchResult => ({ matched: false, reason })\n\nfunction eventSide(event: AlertEvent): 'buy' | 'sell' | null {\n if (event.kind === 'curve_trade' || event.kind === 'whale_trade') return event.side\n return null\n}\n\nfunction eventPositionId(event: AlertEvent): bigint | null {\n if (event.kind === 'launch') return event.positionId\n if (event.kind === 'graduation') return event.positionId\n return null\n}\n\n/**\n * Evaluate a rule against an event.\n *\n * @example\n * ```ts\n * const rule = parseRule({ id: 'whales', kinds: ['whale_trade'], minUsd: 5000 })\n * const result = await evaluateRule(rule, event)\n * if (!result.matched) console.log('skipped:', result.reason)\n * ```\n */\nexport async function evaluateRule(\n rule: Rule,\n event: AlertEvent,\n context: EvaluationContext = {},\n): Promise<MatchResult> {\n if (!rule.enabled) return miss(`rule ${rule.id} is disabled`)\n\n if (!rule.kinds.includes(event.kind)) {\n return miss(`kind ${event.kind} not in [${rule.kinds.join(', ')}]`)\n }\n if (!rule.launchpads.includes(event.launchpad)) {\n return miss(`launchpad ${event.launchpad} not in [${rule.launchpads.join(', ')}]`)\n }\n\n const actor = event.actor.toLowerCase()\n if (rule.excludeDeployers.includes(actor)) {\n return miss(`actor ${event.actor} is on the exclude list`)\n }\n if (rule.tokens.length > 0 && !rule.tokens.includes(event.token.toLowerCase())) {\n return miss(`token ${event.token} is not on the watchlist`)\n }\n if (rule.deployers.length > 0 && !rule.deployers.includes(actor)) {\n return miss(`actor ${event.actor} is not on the deployer watchlist`)\n }\n\n if (rule.side !== 'any') {\n const side = eventSide(event)\n if (side === null) return miss(`side filter does not apply to ${event.kind} events`)\n if (side !== rule.side) return miss(`side ${side} is not ${rule.side}`)\n }\n\n if (rule.minUsd !== undefined) {\n if (event.usdValue === null) return miss('event has no USD value to compare against minUsd')\n if (event.usdValue < rule.minUsd) {\n return miss(`usdValue ${event.usdValue.toFixed(2)} below minUsd ${rule.minUsd}`)\n }\n }\n if (rule.maxUsd !== undefined) {\n if (event.usdValue === null) return miss('event has no USD value to compare against maxUsd')\n if (event.usdValue > rule.maxUsd) {\n return miss(`usdValue ${event.usdValue.toFixed(2)} above maxUsd ${rule.maxUsd}`)\n }\n }\n\n // Everything below costs at least one RPC read.\n if (rule.minLiquidityUsd !== undefined || rule.maxLiquidityUsd !== undefined) {\n if (!context.liquidityUsd) {\n return miss('rule filters on liquidity but no liquidity reader was provided')\n }\n const liquidity = await context.liquidityUsd(event)\n if (liquidity === null) return miss('token liquidity is unknown')\n if (rule.minLiquidityUsd !== undefined && liquidity < rule.minLiquidityUsd) {\n return miss(`liquidity ${liquidity.toFixed(2)} below minLiquidityUsd ${rule.minLiquidityUsd}`)\n }\n if (rule.maxLiquidityUsd !== undefined && liquidity > rule.maxLiquidityUsd) {\n return miss(`liquidity ${liquidity.toFixed(2)} above maxLiquidityUsd ${rule.maxLiquidityUsd}`)\n }\n }\n\n const reputationFilter = rule.reputation\n if (reputationFilter) {\n const needsRecord =\n reputationFilter.minPriorLaunches !== undefined ||\n reputationFilter.maxPriorLaunches !== undefined ||\n reputationFilter.maxRuggedLaunches !== undefined\n\n if (needsRecord) {\n if (!context.reputation) {\n return miss('rule filters on reputation but no reputation provider was configured')\n }\n const record = await context.reputation(event.actor)\n const prior = priorLaunchesBefore(record, event.blockNumber)\n if (\n reputationFilter.minPriorLaunches !== undefined &&\n prior < reputationFilter.minPriorLaunches\n ) {\n return miss(`deployer had ${prior} prior launches, needs ${reputationFilter.minPriorLaunches}`)\n }\n if (\n reputationFilter.maxPriorLaunches !== undefined &&\n prior > reputationFilter.maxPriorLaunches\n ) {\n return miss(`deployer had ${prior} prior launches, max is ${reputationFilter.maxPriorLaunches}`)\n }\n if (\n reputationFilter.maxRuggedLaunches !== undefined &&\n record.ruggedLaunches > reputationFilter.maxRuggedLaunches\n ) {\n return miss(\n `deployer has ${record.ruggedLaunches} drained pools, max is ${reputationFilter.maxRuggedLaunches}`,\n )\n }\n }\n\n if (reputationFilter.requireLpLocked === true) {\n const positionId = eventPositionId(event)\n if (positionId === null) return miss('event carries no LP position to check for a lock')\n if (!context.isLpLocked) {\n return miss('rule requires an LP lock check but no reputation provider was configured')\n }\n if (!(await context.isLpLocked(positionId))) {\n return miss(`LP position ${positionId} is not held by the launchpad locker`)\n }\n }\n }\n\n return matchOk(rule)\n}\n\n/**\n * Evaluate every rule against an event and return the rules that matched,\n * with their reasons. Rules are independent: one throwing lookup does not\n * suppress the others.\n */\nexport async function matchRules(\n rules: readonly Rule[],\n event: AlertEvent,\n context: EvaluationContext = {},\n): Promise<{ rule: Rule; result: MatchResult }[]> {\n const results = await Promise.all(\n rules.map(async (rule) => {\n try {\n return { rule, result: await evaluateRule(rule, event, context) }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { rule, result: miss(`evaluation failed: ${message}`) }\n }\n }),\n )\n return results.filter((entry) => entry.result.matched)\n}\n","import type { RateLimitConfig } from './schema.js'\n\n/**\n * Rate limiting is a *storage* question, not an in-memory counter question:\n * a service that resets its limits on every deploy is not rate limited. The\n * engine talks to this interface, the service backs it with the same SQLite\n * file that holds the delivery log, and tests back it with a map.\n */\nexport interface RateLimitStore {\n /** How many deliveries were recorded for `key` at or after `sinceMs`. */\n countSince(key: string, sinceMs: number): Promise<number>\n /** When `key` last delivered, or `null` if never. */\n lastAt(key: string): Promise<number | null>\n /**\n * Record a delivery against both its rule bucket and its subscriber. The\n * subscriber attribution is what the tier throughput cap counts, so the two\n * are written together and can never drift apart.\n */\n record(key: string, subscriberId: string, atMs: number): Promise<void>\n /** How many alerts a subscriber received since `sinceMs`, for tier caps. */\n countForSubscriberSince(subscriberId: string, sinceMs: number): Promise<number>\n}\n\n/** Why a rate limit rejected a delivery, or `null` when it allowed it. */\nexport interface RateLimitDecision {\n allowed: boolean\n /** Human-readable reason, safe to log and to show in `/status`. */\n reason: string | null\n /** When the next delivery would be allowed, ms since epoch, if known. */\n retryAtMs: number | null\n}\n\nconst ALLOWED: RateLimitDecision = { allowed: true, reason: null, retryAtMs: null }\n\n/**\n * Check a rule's rate limit without consuming it. Consumption happens through\n * {@link RateLimitStore.record} after a delivery attempt, so a rule that\n * matches but fails entitlement checks does not burn quota.\n */\nexport async function checkRateLimit(\n store: RateLimitStore,\n key: string,\n config: RateLimitConfig | undefined,\n nowMs: number,\n): Promise<RateLimitDecision> {\n if (!config) return ALLOWED\n\n if (config.minIntervalSeconds !== undefined) {\n const last = await store.lastAt(key)\n if (last !== null) {\n const nextAllowed = last + config.minIntervalSeconds * 1000\n if (nowMs < nextAllowed) {\n return {\n allowed: false,\n reason: `minIntervalSeconds=${config.minIntervalSeconds} not elapsed`,\n retryAtMs: nextAllowed,\n }\n }\n }\n }\n\n if (config.maxPerHour !== undefined) {\n const windowStart = nowMs - 3_600_000\n const used = await store.countSince(key, windowStart)\n if (used >= config.maxPerHour) {\n return {\n allowed: false,\n reason: `maxPerHour=${config.maxPerHour} reached (${used} in the last hour)`,\n retryAtMs: windowStart + 3_600_000,\n }\n }\n }\n\n return ALLOWED\n}\n\n/** An in-memory {@link RateLimitStore}. Correct, but resets with the process. */\nexport function createMemoryRateLimitStore(): RateLimitStore {\n const byKey = new Map<string, number[]>()\n const bySubscriber = new Map<string, number[]>()\n\n const push = (map: Map<string, number[]>, id: string, atMs: number): void => {\n const list = map.get(id) ?? []\n list.push(atMs)\n // Keep the window bounded: nothing older than an hour affects a decision.\n map.set(\n id,\n list.filter((at) => at >= atMs - 3_600_000),\n )\n }\n\n return {\n async countSince(key: string, sinceMs: number): Promise<number> {\n return (byKey.get(key) ?? []).filter((at) => at >= sinceMs).length\n },\n async lastAt(key: string): Promise<number | null> {\n const list = byKey.get(key)\n if (!list || list.length === 0) return null\n return list[list.length - 1] ?? null\n },\n async record(key: string, subscriberId: string, atMs: number): Promise<void> {\n push(byKey, key, atMs)\n push(bySubscriber, subscriberId, atMs)\n },\n async countForSubscriberSince(subscriberId: string, sinceMs: number): Promise<number> {\n return (bySubscriber.get(subscriberId) ?? []).filter((at) => at >= sinceMs).length\n },\n }\n}\n\n/** Canonical rate-limit key: one bucket per rule per subscription. */\nexport function rateLimitKey(subscriptionId: string, ruleId: string): string {\n return `${subscriptionId}:${ruleId}`\n}\n"],"mappings":"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","names":[]}