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,185 @@
1
+ import { Address } from 'viem';
2
+ import { a as PoolRegistry } from './registry-D3We8Lg6.js';
3
+ import { a as RateLimitStore } from './ratelimit-CGLlXNIS.js';
4
+ import { S as Subscription, P as Platform } from './schema-DuDMPqpq.js';
5
+ import { WalletLinkStore } from './tiers/index.js';
6
+
7
+ /**
8
+ * All durable service state, in one SQLite file, with no native dependency.
9
+ *
10
+ * The three invariants this schema exists to guarantee, in order of how badly
11
+ * they bite when they are missing:
12
+ *
13
+ * 1. **A restart never double-sends.** Every potential delivery has a
14
+ * deterministic primary key (`eventId:subscriptionId:ruleId`), and
15
+ * `enqueue` is an `INSERT` that reports a uniqueness violation as "already
16
+ * queued". Re-processing a block range after a crash regenerates the exact
17
+ * same keys, so the second pass inserts nothing.
18
+ * 2. **A restart never silently skips a range.** The block cursor advances
19
+ * only after every event in a chunk has been enqueued and committed. A
20
+ * crash mid-chunk leaves the cursor pointing at the start of that chunk, so
21
+ * the range is simply re-read.
22
+ * 3. **A crash between enqueue and send does not lose the alert.** Enqueued
23
+ * rows live in the outbox until they are delivered or dead-lettered. On
24
+ * startup, rows left in `sending` (a crash mid-flight) are returned to
25
+ * `pending` and retried.
26
+ *
27
+ * The one honest caveat: if the process dies after the platform accepted a
28
+ * message but before the row was marked `sent`, that alert is delivered twice.
29
+ * Neither the Telegram nor the Discord send API exposes a client-supplied
30
+ * idempotency key, so this window cannot be closed from here. Everything
31
+ * outside it is exactly once.
32
+ */
33
+ /** Delivery lifecycle. */
34
+ type OutboxStatus = 'pending' | 'sending' | 'sent' | 'failed' | 'dead';
35
+ /** A queued delivery. */
36
+ interface OutboxRow {
37
+ id: string;
38
+ subscriptionId: string;
39
+ subscriberId: string;
40
+ ruleId: string;
41
+ eventId: string;
42
+ eventJson: string;
43
+ platform: Platform;
44
+ target: string;
45
+ status: OutboxStatus;
46
+ /** Earliest delivery time, ms since epoch. Carries the free-tier delay. */
47
+ notBeforeMs: number;
48
+ attempts: number;
49
+ lastError: string | null;
50
+ createdAtMs: number;
51
+ updatedAtMs: number;
52
+ }
53
+ /** Aggregate counters for `/metrics`. */
54
+ interface StoreMetrics {
55
+ subscriptions: number;
56
+ enabledSubscriptions: number;
57
+ outbox: Record<OutboxStatus, number>;
58
+ pools: number;
59
+ deliveriesLastHour: number;
60
+ cursors: {
61
+ sourceId: string;
62
+ nextBlock: bigint;
63
+ }[];
64
+ }
65
+ /** The service's durable state. */
66
+ declare class AlertStore {
67
+ private readonly db;
68
+ private readonly clock;
69
+ constructor(path: string, clock?: () => number);
70
+ /** Deterministic outbox key. The whole dedupe guarantee rests on this. */
71
+ static deliveryKey(eventId: string, subscriptionId: string, ruleId: string): string;
72
+ /** The next unprocessed block for a source, or `null` when never persisted. */
73
+ getCursor(sourceId: string): bigint | null;
74
+ /** Persist the next unprocessed block. Call only after the range is enqueued. */
75
+ setCursor(sourceId: string, nextBlock: bigint): void;
76
+ /** Every persisted cursor, for `/metrics` and for operator inspection. */
77
+ listCursors(): {
78
+ sourceId: string;
79
+ nextBlock: bigint;
80
+ }[];
81
+ /** Insert or replace a subscription. */
82
+ saveSubscription(subscription: Subscription): void;
83
+ private hydrate;
84
+ /** One subscription by id. */
85
+ getSubscription(id: string): Subscription | null;
86
+ /** Every subscription owned by a subscriber. */
87
+ listSubscriptionsFor(subscriberId: string): Subscription[];
88
+ /** Every enabled subscription, for the dispatch fan-out. */
89
+ listEnabledSubscriptions(): Subscription[];
90
+ /** Remove a subscription. Returns `true` when a row was deleted. */
91
+ deleteSubscription(id: string): boolean;
92
+ /** Every token on any enabled subscription's watchlist, deduped. */
93
+ watchlistTokens(): Address[];
94
+ /**
95
+ * Queue a delivery.
96
+ *
97
+ * @returns `true` when this call created the row, `false` when it already
98
+ * existed (a replayed block range, a duplicate log). The boolean is the
99
+ * dedupe signal the dispatcher counts.
100
+ */
101
+ enqueue(row: Omit<OutboxRow, 'status' | 'attempts' | 'lastError' | 'createdAtMs' | 'updatedAtMs'>): boolean;
102
+ private static toOutboxRow;
103
+ /**
104
+ * Claim due deliveries, moving them to `sending` in one transaction so a
105
+ * second flush pass cannot pick up the same rows.
106
+ */
107
+ claimDue(nowMs: number, limit: number): OutboxRow[];
108
+ /** Mark a delivery sent. */
109
+ markSent(id: string): void;
110
+ /** Record a retryable failure and schedule the next attempt. */
111
+ markFailed(id: string, attempts: number, error: string, nextAttemptAtMs: number): void;
112
+ /** Give up on a delivery (permanent rejection, or attempts exhausted). */
113
+ markDead(id: string, attempts: number, error: string): void;
114
+ /**
115
+ * Return rows abandoned mid-flight by a crash to the queue.
116
+ *
117
+ * @returns how many rows were recovered.
118
+ */
119
+ recoverInFlight(): number;
120
+ /** Delete delivered rows older than `beforeMs`, keeping the file bounded. */
121
+ pruneOutbox(beforeMs: number): number;
122
+ /** Delete delivery history older than `beforeMs`. */
123
+ pruneDeliveries(beforeMs: number): number;
124
+ /** A {@link RateLimitStore} backed by the `deliveries` table. */
125
+ rateLimitStore(): RateLimitStore;
126
+ /** Record a delivery for both rate limiting and throughput accounting. */
127
+ recordDelivery(key: string, subscriberId: string, atMs: number): void;
128
+ /** How many alerts a subscriber received since `sinceMs`. */
129
+ countDeliveriesForSubscriber(subscriberId: string, sinceMs: number): number;
130
+ /** A {@link PoolRegistry} backed by the `pools` table. */
131
+ poolRegistry(): PoolRegistry;
132
+ private static toPoolEntry;
133
+ /** Store a signature-verified wallet link. */
134
+ linkWallet(subscriberId: string, address: Address): void;
135
+ /** A {@link WalletLinkStore} view of the links table. */
136
+ walletLinks(): WalletLinkStore;
137
+ /** Issue (or reissue) the nonce a subscriber must sign to link a wallet. */
138
+ issueLinkNonce(subscriberId: string, nonce: string): string;
139
+ /** The outstanding link nonce for a subscriber, or `null`. */
140
+ getLinkNonce(subscriberId: string): string | null;
141
+ /** Consume a link nonce so a signature cannot be replayed. */
142
+ clearLinkNonce(subscriberId: string): void;
143
+ /** Read a free-form service setting. */
144
+ getMeta(key: string): string | null;
145
+ /** Write a free-form service setting. */
146
+ setMeta(key: string, value: string): void;
147
+ /** Aggregate counters for `/metrics`. */
148
+ metrics(): StoreMetrics;
149
+ /** Close the database. Safe to call twice. */
150
+ close(): void;
151
+ }
152
+
153
+ /** Structured logging: one JSON object per line, machine-readable by default. */
154
+ /** Log levels, ordered. */
155
+ declare const LEVELS: readonly ["debug", "info", "warn", "error"];
156
+ /** A log level. */
157
+ type LogLevel = (typeof LEVELS)[number];
158
+ /** Structured fields attached to a log line. */
159
+ type LogFields = Record<string, unknown>;
160
+ /** The logger the service uses everywhere. */
161
+ interface Logger {
162
+ debug(message: string, fields?: LogFields): void;
163
+ info(message: string, fields?: LogFields): void;
164
+ warn(message: string, fields?: LogFields): void;
165
+ error(message: string, fields?: LogFields): void;
166
+ /** A logger that adds `fields` to every line, for per-component context. */
167
+ child(fields: LogFields): Logger;
168
+ }
169
+ /** Options for {@link createLogger}. */
170
+ interface LoggerOptions {
171
+ /** Minimum level emitted. @defaultValue `'info'` */
172
+ level?: LogLevel;
173
+ /** Where lines go. @defaultValue `console.log` */
174
+ write?: (line: string) => void;
175
+ /** Fields added to every line. */
176
+ base?: LogFields;
177
+ /** Clock injection point for tests. @defaultValue `Date.now` */
178
+ now?: () => number;
179
+ }
180
+ /** Build a structured logger. */
181
+ declare function createLogger(options?: LoggerOptions): Logger;
182
+ /** A logger that discards everything. For tests and for `--quiet`. */
183
+ declare function createSilentLogger(): Logger;
184
+
185
+ export { AlertStore as A, LEVELS as L, type OutboxRow as O, type StoreMetrics as S, type LogFields as a, type LogLevel as b, type Logger as c, type LoggerOptions as d, type OutboxStatus as e, createLogger as f, createSilentLogger as g };
@@ -0,0 +1,66 @@
1
+ import { Address } from 'viem';
2
+
3
+ /**
4
+ * Resilient log fetching for Robinhood Chain's public RPC.
5
+ *
6
+ * Two behaviours of the public endpoint drive this helper, both measured
7
+ * against `https://rpc.mainnet.chain.robinhood.com` while building this
8
+ * package:
9
+ *
10
+ * 1. **There is no block-range cap, but there is a 10,000-log result cap.**
11
+ * A query spanning millions of blocks succeeds if it is selective enough
12
+ * (the three Odyssey factories return every log they ever emitted in one
13
+ * call); a query spanning 2,000 blocks fails with
14
+ * `logs matched by query exceeds limit of 10000` if it is not (a topic-only
15
+ * Uniswap v3 `Swap` query does). Fixed-size range chunking therefore
16
+ * cannot guarantee success, so {@link fetchLogRange} bisects the range on
17
+ * that specific error until each half fits.
18
+ * 2. **It rate limits bursts with `Too Many Requests`.** Those are retried
19
+ * with exponential backoff rather than surfaced, because a retryable 429
20
+ * must never advance a caller's block cursor.
21
+ */
22
+ /** Options for {@link fetchLogRange}. */
23
+ interface FetchLogRangeOptions {
24
+ /**
25
+ * Smallest range the bisector will produce. A single block that still
26
+ * overflows the result cap throws, because there is nothing left to split.
27
+ * @defaultValue `1n`
28
+ */
29
+ minSpan?: bigint;
30
+ /** Retries for rate-limit and transient transport errors. @defaultValue `5` */
31
+ retries?: number;
32
+ /** Base backoff in ms, doubled per attempt. @defaultValue `500` */
33
+ backoffMs?: number;
34
+ /** Sleep injection point for tests. @defaultValue a real timer */
35
+ sleep?: (ms: number) => Promise<void>;
36
+ }
37
+ /** `true` when the RPC rejected the query for returning too many logs. */
38
+ declare function isResultTooLarge(error: unknown): boolean;
39
+ /** `true` when the error is a transient rate limit or timeout worth retrying. */
40
+ declare function isRetryableRpcError(error: unknown): boolean;
41
+ /**
42
+ * Fetch logs for `[from, to]` inclusive through `fetcher`, bisecting the range
43
+ * when the RPC reports too many results and retrying transient failures.
44
+ *
45
+ * @throws the underlying error when a single block still overflows the result
46
+ * cap, or when retries are exhausted. Callers must treat a throw as "range not
47
+ * processed" and leave their cursor untouched.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const logs = await fetchLogRange(
52
+ * (from, to) => hood.public.getLogs({ address: factory, event, fromBlock: from, toBlock: to }),
53
+ * 0n,
54
+ * await hood.public.getBlockNumber(),
55
+ * )
56
+ * ```
57
+ */
58
+ declare function fetchLogRange<T>(fetcher: (from: bigint, to: bigint) => Promise<readonly T[]>, from: bigint, to: bigint, options?: FetchLogRangeOptions): Promise<T[]>;
59
+ /**
60
+ * Split a list of addresses into query-sized batches. The RPC accepts long
61
+ * address arrays, but batching keeps any single response under the result cap
62
+ * and keeps one failing batch from invalidating the whole set.
63
+ */
64
+ declare function batchAddresses(addresses: readonly Address[], size?: number): Address[][];
65
+
66
+ export { type FetchLogRangeOptions as F, isRetryableRpcError as a, batchAddresses as b, fetchLogRange as f, isResultTooLarge as i };
@@ -0,0 +1,66 @@
1
+ import { Address } from 'viem';
2
+
3
+ /**
4
+ * Resilient log fetching for Robinhood Chain's public RPC.
5
+ *
6
+ * Two behaviours of the public endpoint drive this helper, both measured
7
+ * against `https://rpc.mainnet.chain.robinhood.com` while building this
8
+ * package:
9
+ *
10
+ * 1. **There is no block-range cap, but there is a 10,000-log result cap.**
11
+ * A query spanning millions of blocks succeeds if it is selective enough
12
+ * (the three Odyssey factories return every log they ever emitted in one
13
+ * call); a query spanning 2,000 blocks fails with
14
+ * `logs matched by query exceeds limit of 10000` if it is not (a topic-only
15
+ * Uniswap v3 `Swap` query does). Fixed-size range chunking therefore
16
+ * cannot guarantee success, so {@link fetchLogRange} bisects the range on
17
+ * that specific error until each half fits.
18
+ * 2. **It rate limits bursts with `Too Many Requests`.** Those are retried
19
+ * with exponential backoff rather than surfaced, because a retryable 429
20
+ * must never advance a caller's block cursor.
21
+ */
22
+ /** Options for {@link fetchLogRange}. */
23
+ interface FetchLogRangeOptions {
24
+ /**
25
+ * Smallest range the bisector will produce. A single block that still
26
+ * overflows the result cap throws, because there is nothing left to split.
27
+ * @defaultValue `1n`
28
+ */
29
+ minSpan?: bigint;
30
+ /** Retries for rate-limit and transient transport errors. @defaultValue `5` */
31
+ retries?: number;
32
+ /** Base backoff in ms, doubled per attempt. @defaultValue `500` */
33
+ backoffMs?: number;
34
+ /** Sleep injection point for tests. @defaultValue a real timer */
35
+ sleep?: (ms: number) => Promise<void>;
36
+ }
37
+ /** `true` when the RPC rejected the query for returning too many logs. */
38
+ declare function isResultTooLarge(error: unknown): boolean;
39
+ /** `true` when the error is a transient rate limit or timeout worth retrying. */
40
+ declare function isRetryableRpcError(error: unknown): boolean;
41
+ /**
42
+ * Fetch logs for `[from, to]` inclusive through `fetcher`, bisecting the range
43
+ * when the RPC reports too many results and retrying transient failures.
44
+ *
45
+ * @throws the underlying error when a single block still overflows the result
46
+ * cap, or when retries are exhausted. Callers must treat a throw as "range not
47
+ * processed" and leave their cursor untouched.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const logs = await fetchLogRange(
52
+ * (from, to) => hood.public.getLogs({ address: factory, event, fromBlock: from, toBlock: to }),
53
+ * 0n,
54
+ * await hood.public.getBlockNumber(),
55
+ * )
56
+ * ```
57
+ */
58
+ declare function fetchLogRange<T>(fetcher: (from: bigint, to: bigint) => Promise<readonly T[]>, from: bigint, to: bigint, options?: FetchLogRangeOptions): Promise<T[]>;
59
+ /**
60
+ * Split a list of addresses into query-sized batches. The RPC accepts long
61
+ * address arrays, but batching keeps any single response under the result cap
62
+ * and keeps one failing batch from invalidating the whole set.
63
+ */
64
+ declare function batchAddresses(addresses: readonly Address[], size?: number): Address[][];
65
+
66
+ export { type FetchLogRangeOptions as F, isRetryableRpcError as a, batchAddresses as b, fetchLogRange as f, isResultTooLarge as i };