hood-alerts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +369 -0
- package/dist/bot/index.cjs +1008 -0
- package/dist/bot/index.cjs.map +1 -0
- package/dist/bot/index.d.cts +241 -0
- package/dist/bot/index.d.ts +241 -0
- package/dist/bot/index.js +30 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/chunk-32GS6XVE.js +732 -0
- package/dist/chunk-32GS6XVE.js.map +1 -0
- package/dist/chunk-3LHCQA4Z.js +76 -0
- package/dist/chunk-3LHCQA4Z.js.map +1 -0
- package/dist/chunk-3NHLSKZE.js +1 -0
- package/dist/chunk-3NHLSKZE.js.map +1 -0
- package/dist/chunk-6JQK5G3Z.js +72 -0
- package/dist/chunk-6JQK5G3Z.js.map +1 -0
- package/dist/chunk-CBNZ6AZT.js +598 -0
- package/dist/chunk-CBNZ6AZT.js.map +1 -0
- package/dist/chunk-CIURPPOW.js +1 -0
- package/dist/chunk-CIURPPOW.js.map +1 -0
- package/dist/chunk-IUOEGBF6.js +49 -0
- package/dist/chunk-IUOEGBF6.js.map +1 -0
- package/dist/chunk-NBMINJ5E.js +230 -0
- package/dist/chunk-NBMINJ5E.js.map +1 -0
- package/dist/chunk-O2RY2SWN.js +1 -0
- package/dist/chunk-O2RY2SWN.js.map +1 -0
- package/dist/chunk-PLMVKTQM.js +1 -0
- package/dist/chunk-PLMVKTQM.js.map +1 -0
- package/dist/chunk-RSTLW7XH.js +130 -0
- package/dist/chunk-RSTLW7XH.js.map +1 -0
- package/dist/chunk-SOBZ2FPP.js +355 -0
- package/dist/chunk-SOBZ2FPP.js.map +1 -0
- package/dist/chunk-VC5MKEY2.js +170 -0
- package/dist/chunk-VC5MKEY2.js.map +1 -0
- package/dist/chunk-W2GUAWUR.js +147 -0
- package/dist/chunk-W2GUAWUR.js.map +1 -0
- package/dist/chunk-Y2M3USWZ.js +355 -0
- package/dist/chunk-Y2M3USWZ.js.map +1 -0
- package/dist/chunk-Z4DJPESQ.js +1216 -0
- package/dist/chunk-Z4DJPESQ.js.map +1 -0
- package/dist/chunk-ZDM5VNWB.js +1 -0
- package/dist/chunk-ZDM5VNWB.js.map +1 -0
- package/dist/events/index.cjs +820 -0
- package/dist/events/index.cjs.map +1 -0
- package/dist/events/index.d.cts +58 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.js +69 -0
- package/dist/events/index.js.map +1 -0
- package/dist/format-6RG4OcyE.d.ts +57 -0
- package/dist/format-B7d40f65.d.cts +57 -0
- package/dist/index.cjs +4122 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/dist/logger-3phvV_fl.d.cts +185 -0
- package/dist/logger-CYza_0eU.d.ts +185 -0
- package/dist/logs-D7sYBvSy.d.cts +66 -0
- package/dist/logs-D7sYBvSy.d.ts +66 -0
- package/dist/notifiers/index.cjs +620 -0
- package/dist/notifiers/index.cjs.map +1 -0
- package/dist/notifiers/index.d.cts +186 -0
- package/dist/notifiers/index.d.ts +186 -0
- package/dist/notifiers/index.js +70 -0
- package/dist/notifiers/index.js.map +1 -0
- package/dist/pricing-BumgLpvB.d.cts +79 -0
- package/dist/pricing-BumgLpvB.d.ts +79 -0
- package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
- package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
- package/dist/registry-D3We8Lg6.d.ts +50 -0
- package/dist/registry-DePA3IyS.d.cts +50 -0
- package/dist/rules/index.cjs +614 -0
- package/dist/rules/index.cjs.map +1 -0
- package/dist/rules/index.d.cts +166 -0
- package/dist/rules/index.d.ts +166 -0
- package/dist/rules/index.js +60 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/schema-DuDMPqpq.d.ts +183 -0
- package/dist/schema-NnIdKFl2.d.cts +183 -0
- package/dist/service/index.cjs +3774 -0
- package/dist/service/index.cjs.map +1 -0
- package/dist/service/index.d.cts +262 -0
- package/dist/service/index.d.ts +262 -0
- package/dist/service/index.js +45 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/main.cjs +3759 -0
- package/dist/service/main.cjs.map +1 -0
- package/dist/service/main.d.cts +1 -0
- package/dist/service/main.d.ts +1 -0
- package/dist/service/main.js +60 -0
- package/dist/service/main.js.map +1 -0
- package/dist/sources-CAaQR6-N.d.ts +159 -0
- package/dist/sources-CpDOUlzx.d.cts +159 -0
- package/dist/telegram-CpodYJrP.d.cts +64 -0
- package/dist/telegram-pvphmBwn.d.ts +64 -0
- package/dist/tiers/index.cjs +471 -0
- package/dist/tiers/index.cjs.map +1 -0
- package/dist/tiers/index.d.cts +231 -0
- package/dist/tiers/index.d.ts +231 -0
- package/dist/tiers/index.js +34 -0
- package/dist/tiers/index.js.map +1 -0
- package/dist/types-C0ETog04.d.ts +109 -0
- package/dist/types-CcoKpStA.d.cts +109 -0
- package/dist/types-DFPpTMWo.d.cts +186 -0
- package/dist/types-DFPpTMWo.d.ts +186 -0
- package/package.json +114 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createService,
|
|
4
|
+
loadConfig
|
|
5
|
+
} from "../chunk-Z4DJPESQ.js";
|
|
6
|
+
import "../chunk-CBNZ6AZT.js";
|
|
7
|
+
import "../chunk-Y2M3USWZ.js";
|
|
8
|
+
import "../chunk-6JQK5G3Z.js";
|
|
9
|
+
import "../chunk-SOBZ2FPP.js";
|
|
10
|
+
import "../chunk-32GS6XVE.js";
|
|
11
|
+
import "../chunk-NBMINJ5E.js";
|
|
12
|
+
import "../chunk-RSTLW7XH.js";
|
|
13
|
+
import "../chunk-W2GUAWUR.js";
|
|
14
|
+
import "../chunk-VC5MKEY2.js";
|
|
15
|
+
import "../chunk-3LHCQA4Z.js";
|
|
16
|
+
|
|
17
|
+
// src/service/main.ts
|
|
18
|
+
var GRACE_MS = 15e3;
|
|
19
|
+
async function main() {
|
|
20
|
+
const config = loadConfig();
|
|
21
|
+
const service = createService({ config });
|
|
22
|
+
let shuttingDown = false;
|
|
23
|
+
const shutdown = (signal) => {
|
|
24
|
+
if (shuttingDown) {
|
|
25
|
+
service.logger.warn("second signal received, exiting now", { signal });
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
shuttingDown = true;
|
|
29
|
+
service.logger.info("shutdown signal received", { signal });
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
service.logger.error("graceful shutdown timed out, exiting", { graceMs: GRACE_MS });
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}, GRACE_MS);
|
|
34
|
+
timer.unref();
|
|
35
|
+
service.stop().then(() => process.exit(0)).catch((error) => {
|
|
36
|
+
service.logger.error("shutdown failed", {
|
|
37
|
+
error: error instanceof Error ? error.message : String(error)
|
|
38
|
+
});
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
43
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
44
|
+
process.on("unhandledRejection", (reason) => {
|
|
45
|
+
service.logger.error("unhandled rejection", {
|
|
46
|
+
error: reason instanceof Error ? reason.message : String(reason)
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
process.on("uncaughtException", (error) => {
|
|
50
|
+
service.logger.error("uncaught exception, shutting down", { error });
|
|
51
|
+
shutdown("uncaughtException");
|
|
52
|
+
});
|
|
53
|
+
await service.start();
|
|
54
|
+
}
|
|
55
|
+
main().catch((error) => {
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
console.error(JSON.stringify({ level: "error", message: `hood-alerts failed to start: ${message}` }));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/service/main.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { loadConfig } from './config.js'\nimport { createService } from './runner.js'\n\n/**\n * The service entry point.\n *\n * Shutdown is graceful and bounded: SIGINT/SIGTERM stop the loops, close the\n * HTTP server and close SQLite. A second signal, or a shutdown that overruns\n * the grace period, exits immediately, because a container that ignores\n * SIGTERM gets SIGKILLed anyway and losing the clean close is worse than\n * exiting a few seconds early.\n */\n\nconst GRACE_MS = 15_000\n\nasync function main(): Promise<void> {\n const config = loadConfig()\n const service = createService({ config })\n\n let shuttingDown = false\n const shutdown = (signal: string): void => {\n if (shuttingDown) {\n service.logger.warn('second signal received, exiting now', { signal })\n process.exit(1)\n }\n shuttingDown = true\n service.logger.info('shutdown signal received', { signal })\n const timer = setTimeout(() => {\n service.logger.error('graceful shutdown timed out, exiting', { graceMs: GRACE_MS })\n process.exit(1)\n }, GRACE_MS)\n timer.unref()\n service\n .stop()\n .then(() => process.exit(0))\n .catch((error: unknown) => {\n service.logger.error('shutdown failed', {\n error: error instanceof Error ? error.message : String(error),\n })\n process.exit(1)\n })\n }\n\n process.on('SIGINT', () => shutdown('SIGINT'))\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n process.on('unhandledRejection', (reason) => {\n service.logger.error('unhandled rejection', {\n error: reason instanceof Error ? reason.message : String(reason),\n })\n })\n process.on('uncaughtException', (error: Error) => {\n service.logger.error('uncaught exception, shutting down', { error })\n shutdown('uncaughtException')\n })\n\n await service.start()\n}\n\nmain().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n console.error(JSON.stringify({ level: 'error', message: `hood-alerts failed to start: ${message}` }))\n process.exit(1)\n})\n"],"mappings":";;;;;;;;;;;;;;;;;AAcA,IAAM,WAAW;AAEjB,eAAe,OAAsB;AACnC,QAAM,SAAS,WAAW;AAC1B,QAAM,UAAU,cAAc,EAAE,OAAO,CAAC;AAExC,MAAI,eAAe;AACnB,QAAM,WAAW,CAAC,WAAyB;AACzC,QAAI,cAAc;AAChB,cAAQ,OAAO,KAAK,uCAAuC,EAAE,OAAO,CAAC;AACrE,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,mBAAe;AACf,YAAQ,OAAO,KAAK,4BAA4B,EAAE,OAAO,CAAC;AAC1D,UAAM,QAAQ,WAAW,MAAM;AAC7B,cAAQ,OAAO,MAAM,wCAAwC,EAAE,SAAS,SAAS,CAAC;AAClF,cAAQ,KAAK,CAAC;AAAA,IAChB,GAAG,QAAQ;AACX,UAAM,MAAM;AACZ,YACG,KAAK,EACL,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC,EAC1B,MAAM,CAAC,UAAmB;AACzB,cAAQ,OAAO,MAAM,mBAAmB;AAAA,QACtC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC9D,CAAC;AACD,cAAQ,KAAK,CAAC;AAAA,IAChB,CAAC;AAAA,EACL;AAEA,UAAQ,GAAG,UAAU,MAAM,SAAS,QAAQ,CAAC;AAC7C,UAAQ,GAAG,WAAW,MAAM,SAAS,SAAS,CAAC;AAC/C,UAAQ,GAAG,sBAAsB,CAAC,WAAW;AAC3C,YAAQ,OAAO,MAAM,uBAAuB;AAAA,MAC1C,OAAO,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AAAA,IACjE,CAAC;AAAA,EACH,CAAC;AACD,UAAQ,GAAG,qBAAqB,CAAC,UAAiB;AAChD,YAAQ,OAAO,MAAM,qCAAqC,EAAE,MAAM,CAAC;AACnE,aAAS,mBAAmB;AAAA,EAC9B,CAAC;AAED,QAAM,QAAQ,MAAM;AACtB;AAEA,KAAK,EAAE,MAAM,CAAC,UAAmB;AAC/B,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAQ,MAAM,KAAK,UAAU,EAAE,OAAO,SAAS,SAAS,gCAAgC,OAAO,GAAG,CAAC,CAAC;AACpG,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Address, Hash } from 'viem';
|
|
2
|
+
import { HoodClient } from 'hoodchain';
|
|
3
|
+
import { F as FetchLogRangeOptions } from './logs-D7sYBvSy.js';
|
|
4
|
+
import { P as PriceOracle } from './pricing-BumgLpvB.js';
|
|
5
|
+
import { a as PoolRegistry } from './registry-D3We8Lg6.js';
|
|
6
|
+
import { A as AlertEvent, e as Launchpad } from './types-DFPpTMWo.js';
|
|
7
|
+
|
|
8
|
+
/** ERC-20 display metadata, with `null` for anything the contract does not answer. */
|
|
9
|
+
interface TokenMeta {
|
|
10
|
+
address: Address;
|
|
11
|
+
symbol: string | null;
|
|
12
|
+
name: string | null;
|
|
13
|
+
/** Decimals, defaulting to 18 when the call reverts (the launchpad standard). */
|
|
14
|
+
decimals: number;
|
|
15
|
+
}
|
|
16
|
+
/** Cached reader for ERC-20 display metadata. */
|
|
17
|
+
interface TokenMetaReader {
|
|
18
|
+
get(token: Address): Promise<TokenMeta>;
|
|
19
|
+
/** Prime the cache without a network round trip (used by the service store). */
|
|
20
|
+
prime(meta: TokenMeta): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a token metadata reader.
|
|
24
|
+
*
|
|
25
|
+
* Metadata is immutable in practice for launchpad tokens, so entries are
|
|
26
|
+
* cached for the process lifetime. A token whose `symbol()`/`name()` revert
|
|
27
|
+
* (some minimal proxies omit them) resolves to `null` fields rather than
|
|
28
|
+
* throwing, because a missing symbol must never drop an alert.
|
|
29
|
+
*/
|
|
30
|
+
declare function createTokenMetaReader(client: HoodClient, maxEntries?: number): TokenMetaReader;
|
|
31
|
+
|
|
32
|
+
/** Cached block-timestamp reader. */
|
|
33
|
+
interface BlockTimeReader {
|
|
34
|
+
/**
|
|
35
|
+
* Timestamp of `blockNumber` in ms, or `null` when it could not be read
|
|
36
|
+
* within the caller's budget. Events carry `timestampMs: null` in that case
|
|
37
|
+
* rather than a fabricated "now".
|
|
38
|
+
*/
|
|
39
|
+
get(blockNumber: bigint): Promise<number | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve timestamps for a set of blocks, bounded by `maxLookups`. Blocks
|
|
42
|
+
* past the budget resolve to `null`: a backfill of a million blocks must not
|
|
43
|
+
* turn into a million `eth_getBlockByNumber` calls.
|
|
44
|
+
*/
|
|
45
|
+
getMany(blocks: readonly bigint[], maxLookups: number): Promise<Map<bigint, number | null>>;
|
|
46
|
+
}
|
|
47
|
+
/** Build a block-timestamp reader with a bounded LRU cache. */
|
|
48
|
+
declare function createBlockTimeReader(client: HoodClient, maxEntries?: number): BlockTimeReader;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The event sources: one per on-chain surface, each responsible for turning a
|
|
52
|
+
* confirmed block range into normalized {@link AlertEvent}s.
|
|
53
|
+
*
|
|
54
|
+
* Sources are deliberately *pull* based (`poll(from, to)`) rather than
|
|
55
|
+
* push/subscription based. The service owns the block cursor and persists it,
|
|
56
|
+
* so a source that is asked for the same range twice must produce identical
|
|
57
|
+
* events, and it does: every event id is derived from `txHash` and `logIndex`.
|
|
58
|
+
* That is what makes crash recovery safe.
|
|
59
|
+
*/
|
|
60
|
+
/** A pollable on-chain source of alert events. */
|
|
61
|
+
interface EventSource {
|
|
62
|
+
/** Stable identifier, used as the persisted cursor key. Never change it. */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Human label for logs and `/metrics`. */
|
|
65
|
+
label: string;
|
|
66
|
+
/** Earliest block this source can produce events from. */
|
|
67
|
+
startBlock: bigint;
|
|
68
|
+
/** Decode `[from, to]` inclusive into events. Must be deterministic. */
|
|
69
|
+
poll(from: bigint, to: bigint): Promise<AlertEvent[]>;
|
|
70
|
+
}
|
|
71
|
+
/** Wiring shared by every source. */
|
|
72
|
+
interface SourceContext {
|
|
73
|
+
client: HoodClient;
|
|
74
|
+
oracle: PriceOracle;
|
|
75
|
+
tokens: TokenMetaReader;
|
|
76
|
+
registry: PoolRegistry;
|
|
77
|
+
/** Optional shared block-timestamp reader (one is created when omitted). */
|
|
78
|
+
blockTimes?: BlockTimeReader;
|
|
79
|
+
/** Max `eth_getBlockByNumber` calls per poll, for timestamps. @defaultValue `25` */
|
|
80
|
+
maxBlockTimeLookupsPerPoll?: number;
|
|
81
|
+
/** Retry/bisect options passed to {@link fetchLogRange}. */
|
|
82
|
+
logOptions?: FetchLogRangeOptions;
|
|
83
|
+
/**
|
|
84
|
+
* Tokens that must always be in the whale watcher's pool set (subscriber
|
|
85
|
+
* watchlists). Called once per whale poll.
|
|
86
|
+
*/
|
|
87
|
+
pinnedTokens?: () => Promise<readonly Address[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Max pools the whale watcher queries per poll, newest first.
|
|
90
|
+
* @defaultValue `400`
|
|
91
|
+
*/
|
|
92
|
+
whalePoolLimit?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Service-wide floor for whale trades in USD. Swaps below this never become
|
|
95
|
+
* events at all, which keeps the delivery pipeline from doing per-subscriber
|
|
96
|
+
* work on dust. Per-subscription `minUsd` rules are applied on top and can
|
|
97
|
+
* only be stricter.
|
|
98
|
+
* @defaultValue `1000`
|
|
99
|
+
*/
|
|
100
|
+
whaleMinUsd?: number;
|
|
101
|
+
/** Pools per address-filtered `eth_getLogs` call. @defaultValue `200` */
|
|
102
|
+
whalePoolBatchSize?: number;
|
|
103
|
+
}
|
|
104
|
+
/** Deterministic event id: reprocessing a range regenerates the same value. */
|
|
105
|
+
declare function eventId(kind: string, transactionHash: Hash, logIndex: number): string;
|
|
106
|
+
/**
|
|
107
|
+
* Build every event source for a mainnet client.
|
|
108
|
+
*
|
|
109
|
+
* @throws when the client targets testnet: neither launchpad exists there, so
|
|
110
|
+
* silently returning an empty source list would look like "no launches".
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { createHoodClient } from 'hoodchain'
|
|
115
|
+
* import {
|
|
116
|
+
* createEventSources, createPriceOracle, createTokenMetaReader, createMemoryPoolRegistry,
|
|
117
|
+
* } from 'hood-alerts/events'
|
|
118
|
+
*
|
|
119
|
+
* const hood = createHoodClient()
|
|
120
|
+
* const sources = createEventSources({
|
|
121
|
+
* client: hood,
|
|
122
|
+
* oracle: createPriceOracle(hood),
|
|
123
|
+
* tokens: createTokenMetaReader(hood),
|
|
124
|
+
* registry: createMemoryPoolRegistry(),
|
|
125
|
+
* })
|
|
126
|
+
* const head = await hood.public.getBlockNumber()
|
|
127
|
+
* for (const source of sources) {
|
|
128
|
+
* console.log(source.label, (await source.poll(head - 2_000n, head)).length)
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function createEventSources(ctx: SourceContext): EventSource[];
|
|
133
|
+
/** NOXA `TokenLaunched`: an instant listing with a live Uniswap v3 pool. */
|
|
134
|
+
declare function createNoxaLaunchSource(ctx: SourceContext): EventSource;
|
|
135
|
+
/** The Odyssey `TokenCreated`: a bonding curve opening. */
|
|
136
|
+
declare function createOdysseyLaunchSource(ctx: SourceContext): EventSource;
|
|
137
|
+
/** The Odyssey `Traded`: every bonding-curve buy and sell. */
|
|
138
|
+
declare function createOdysseyCurveTradeSource(ctx: SourceContext): EventSource;
|
|
139
|
+
/**
|
|
140
|
+
* The Odyssey `PoolMigrated`: the curve filled and liquidity moved to a locked
|
|
141
|
+
* Uniswap v3 pool. This is the only real graduation event on the chain. NOXA
|
|
142
|
+
* cannot emit one, by construction.
|
|
143
|
+
*/
|
|
144
|
+
declare function createOdysseyGraduationSource(ctx: SourceContext): EventSource;
|
|
145
|
+
/**
|
|
146
|
+
* Uniswap v3 swaps on known memecoin pools, above the service-wide USD floor.
|
|
147
|
+
*
|
|
148
|
+
* The pool set comes from {@link PoolRegistry}: NOXA pools are known from
|
|
149
|
+
* launch, Odyssey pools from graduation. That is a deliberate design choice,
|
|
150
|
+
* not a shortcut. A topic-only `Swap` query across the whole chain overflows
|
|
151
|
+
* the RPC's 10,000-log result cap inside 2,000 blocks, so "watch every pool"
|
|
152
|
+
* is not something the public endpoint can serve. Watching the launchpad pool
|
|
153
|
+
* set is both cheaper and exactly the product: memecoin whale alerts.
|
|
154
|
+
*/
|
|
155
|
+
declare function createWhaleTradeSource(ctx: SourceContext): EventSource;
|
|
156
|
+
/** Every launchpad an event source can attribute an event to. */
|
|
157
|
+
declare const SUPPORTED_LAUNCHPADS: readonly Launchpad[];
|
|
158
|
+
|
|
159
|
+
export { type BlockTimeReader as B, type EventSource as E, SUPPORTED_LAUNCHPADS as S, type TokenMeta as T, type SourceContext as a, type TokenMetaReader as b, createBlockTimeReader as c, createEventSources as d, createNoxaLaunchSource as e, createOdysseyCurveTradeSource as f, createOdysseyGraduationSource as g, createOdysseyLaunchSource as h, createTokenMetaReader as i, createWhaleTradeSource as j, eventId as k };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Address, Hash } from 'viem';
|
|
2
|
+
import { HoodClient } from 'hoodchain';
|
|
3
|
+
import { F as FetchLogRangeOptions } from './logs-D7sYBvSy.cjs';
|
|
4
|
+
import { P as PriceOracle } from './pricing-BumgLpvB.cjs';
|
|
5
|
+
import { a as PoolRegistry } from './registry-DePA3IyS.cjs';
|
|
6
|
+
import { A as AlertEvent, e as Launchpad } from './types-DFPpTMWo.cjs';
|
|
7
|
+
|
|
8
|
+
/** ERC-20 display metadata, with `null` for anything the contract does not answer. */
|
|
9
|
+
interface TokenMeta {
|
|
10
|
+
address: Address;
|
|
11
|
+
symbol: string | null;
|
|
12
|
+
name: string | null;
|
|
13
|
+
/** Decimals, defaulting to 18 when the call reverts (the launchpad standard). */
|
|
14
|
+
decimals: number;
|
|
15
|
+
}
|
|
16
|
+
/** Cached reader for ERC-20 display metadata. */
|
|
17
|
+
interface TokenMetaReader {
|
|
18
|
+
get(token: Address): Promise<TokenMeta>;
|
|
19
|
+
/** Prime the cache without a network round trip (used by the service store). */
|
|
20
|
+
prime(meta: TokenMeta): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a token metadata reader.
|
|
24
|
+
*
|
|
25
|
+
* Metadata is immutable in practice for launchpad tokens, so entries are
|
|
26
|
+
* cached for the process lifetime. A token whose `symbol()`/`name()` revert
|
|
27
|
+
* (some minimal proxies omit them) resolves to `null` fields rather than
|
|
28
|
+
* throwing, because a missing symbol must never drop an alert.
|
|
29
|
+
*/
|
|
30
|
+
declare function createTokenMetaReader(client: HoodClient, maxEntries?: number): TokenMetaReader;
|
|
31
|
+
|
|
32
|
+
/** Cached block-timestamp reader. */
|
|
33
|
+
interface BlockTimeReader {
|
|
34
|
+
/**
|
|
35
|
+
* Timestamp of `blockNumber` in ms, or `null` when it could not be read
|
|
36
|
+
* within the caller's budget. Events carry `timestampMs: null` in that case
|
|
37
|
+
* rather than a fabricated "now".
|
|
38
|
+
*/
|
|
39
|
+
get(blockNumber: bigint): Promise<number | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve timestamps for a set of blocks, bounded by `maxLookups`. Blocks
|
|
42
|
+
* past the budget resolve to `null`: a backfill of a million blocks must not
|
|
43
|
+
* turn into a million `eth_getBlockByNumber` calls.
|
|
44
|
+
*/
|
|
45
|
+
getMany(blocks: readonly bigint[], maxLookups: number): Promise<Map<bigint, number | null>>;
|
|
46
|
+
}
|
|
47
|
+
/** Build a block-timestamp reader with a bounded LRU cache. */
|
|
48
|
+
declare function createBlockTimeReader(client: HoodClient, maxEntries?: number): BlockTimeReader;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The event sources: one per on-chain surface, each responsible for turning a
|
|
52
|
+
* confirmed block range into normalized {@link AlertEvent}s.
|
|
53
|
+
*
|
|
54
|
+
* Sources are deliberately *pull* based (`poll(from, to)`) rather than
|
|
55
|
+
* push/subscription based. The service owns the block cursor and persists it,
|
|
56
|
+
* so a source that is asked for the same range twice must produce identical
|
|
57
|
+
* events, and it does: every event id is derived from `txHash` and `logIndex`.
|
|
58
|
+
* That is what makes crash recovery safe.
|
|
59
|
+
*/
|
|
60
|
+
/** A pollable on-chain source of alert events. */
|
|
61
|
+
interface EventSource {
|
|
62
|
+
/** Stable identifier, used as the persisted cursor key. Never change it. */
|
|
63
|
+
id: string;
|
|
64
|
+
/** Human label for logs and `/metrics`. */
|
|
65
|
+
label: string;
|
|
66
|
+
/** Earliest block this source can produce events from. */
|
|
67
|
+
startBlock: bigint;
|
|
68
|
+
/** Decode `[from, to]` inclusive into events. Must be deterministic. */
|
|
69
|
+
poll(from: bigint, to: bigint): Promise<AlertEvent[]>;
|
|
70
|
+
}
|
|
71
|
+
/** Wiring shared by every source. */
|
|
72
|
+
interface SourceContext {
|
|
73
|
+
client: HoodClient;
|
|
74
|
+
oracle: PriceOracle;
|
|
75
|
+
tokens: TokenMetaReader;
|
|
76
|
+
registry: PoolRegistry;
|
|
77
|
+
/** Optional shared block-timestamp reader (one is created when omitted). */
|
|
78
|
+
blockTimes?: BlockTimeReader;
|
|
79
|
+
/** Max `eth_getBlockByNumber` calls per poll, for timestamps. @defaultValue `25` */
|
|
80
|
+
maxBlockTimeLookupsPerPoll?: number;
|
|
81
|
+
/** Retry/bisect options passed to {@link fetchLogRange}. */
|
|
82
|
+
logOptions?: FetchLogRangeOptions;
|
|
83
|
+
/**
|
|
84
|
+
* Tokens that must always be in the whale watcher's pool set (subscriber
|
|
85
|
+
* watchlists). Called once per whale poll.
|
|
86
|
+
*/
|
|
87
|
+
pinnedTokens?: () => Promise<readonly Address[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Max pools the whale watcher queries per poll, newest first.
|
|
90
|
+
* @defaultValue `400`
|
|
91
|
+
*/
|
|
92
|
+
whalePoolLimit?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Service-wide floor for whale trades in USD. Swaps below this never become
|
|
95
|
+
* events at all, which keeps the delivery pipeline from doing per-subscriber
|
|
96
|
+
* work on dust. Per-subscription `minUsd` rules are applied on top and can
|
|
97
|
+
* only be stricter.
|
|
98
|
+
* @defaultValue `1000`
|
|
99
|
+
*/
|
|
100
|
+
whaleMinUsd?: number;
|
|
101
|
+
/** Pools per address-filtered `eth_getLogs` call. @defaultValue `200` */
|
|
102
|
+
whalePoolBatchSize?: number;
|
|
103
|
+
}
|
|
104
|
+
/** Deterministic event id: reprocessing a range regenerates the same value. */
|
|
105
|
+
declare function eventId(kind: string, transactionHash: Hash, logIndex: number): string;
|
|
106
|
+
/**
|
|
107
|
+
* Build every event source for a mainnet client.
|
|
108
|
+
*
|
|
109
|
+
* @throws when the client targets testnet: neither launchpad exists there, so
|
|
110
|
+
* silently returning an empty source list would look like "no launches".
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { createHoodClient } from 'hoodchain'
|
|
115
|
+
* import {
|
|
116
|
+
* createEventSources, createPriceOracle, createTokenMetaReader, createMemoryPoolRegistry,
|
|
117
|
+
* } from 'hood-alerts/events'
|
|
118
|
+
*
|
|
119
|
+
* const hood = createHoodClient()
|
|
120
|
+
* const sources = createEventSources({
|
|
121
|
+
* client: hood,
|
|
122
|
+
* oracle: createPriceOracle(hood),
|
|
123
|
+
* tokens: createTokenMetaReader(hood),
|
|
124
|
+
* registry: createMemoryPoolRegistry(),
|
|
125
|
+
* })
|
|
126
|
+
* const head = await hood.public.getBlockNumber()
|
|
127
|
+
* for (const source of sources) {
|
|
128
|
+
* console.log(source.label, (await source.poll(head - 2_000n, head)).length)
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
declare function createEventSources(ctx: SourceContext): EventSource[];
|
|
133
|
+
/** NOXA `TokenLaunched`: an instant listing with a live Uniswap v3 pool. */
|
|
134
|
+
declare function createNoxaLaunchSource(ctx: SourceContext): EventSource;
|
|
135
|
+
/** The Odyssey `TokenCreated`: a bonding curve opening. */
|
|
136
|
+
declare function createOdysseyLaunchSource(ctx: SourceContext): EventSource;
|
|
137
|
+
/** The Odyssey `Traded`: every bonding-curve buy and sell. */
|
|
138
|
+
declare function createOdysseyCurveTradeSource(ctx: SourceContext): EventSource;
|
|
139
|
+
/**
|
|
140
|
+
* The Odyssey `PoolMigrated`: the curve filled and liquidity moved to a locked
|
|
141
|
+
* Uniswap v3 pool. This is the only real graduation event on the chain. NOXA
|
|
142
|
+
* cannot emit one, by construction.
|
|
143
|
+
*/
|
|
144
|
+
declare function createOdysseyGraduationSource(ctx: SourceContext): EventSource;
|
|
145
|
+
/**
|
|
146
|
+
* Uniswap v3 swaps on known memecoin pools, above the service-wide USD floor.
|
|
147
|
+
*
|
|
148
|
+
* The pool set comes from {@link PoolRegistry}: NOXA pools are known from
|
|
149
|
+
* launch, Odyssey pools from graduation. That is a deliberate design choice,
|
|
150
|
+
* not a shortcut. A topic-only `Swap` query across the whole chain overflows
|
|
151
|
+
* the RPC's 10,000-log result cap inside 2,000 blocks, so "watch every pool"
|
|
152
|
+
* is not something the public endpoint can serve. Watching the launchpad pool
|
|
153
|
+
* set is both cheaper and exactly the product: memecoin whale alerts.
|
|
154
|
+
*/
|
|
155
|
+
declare function createWhaleTradeSource(ctx: SourceContext): EventSource;
|
|
156
|
+
/** Every launchpad an event source can attribute an event to. */
|
|
157
|
+
declare const SUPPORTED_LAUNCHPADS: readonly Launchpad[];
|
|
158
|
+
|
|
159
|
+
export { type BlockTimeReader as B, type EventSource as E, SUPPORTED_LAUNCHPADS as S, type TokenMeta as T, type SourceContext as a, type TokenMetaReader as b, createBlockTimeReader as c, createEventSources as d, createNoxaLaunchSource as e, createOdysseyCurveTradeSource as f, createOdysseyGraduationSource as g, createOdysseyLaunchSource as h, createTokenMetaReader as i, createWhaleTradeSource as j, eventId as k };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { d as NotifierHttpOptions, N as Notifier } from './types-CcoKpStA.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Bot API adapter.
|
|
5
|
+
*
|
|
6
|
+
* Rate limiting is the part everyone gets wrong. Telegram does not use a
|
|
7
|
+
* header budget: it answers `429` with a JSON body
|
|
8
|
+
* `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 7",
|
|
9
|
+
* "parameters":{"retry_after":7}}` and expects the caller to wait exactly that
|
|
10
|
+
* many *seconds* before retrying. This adapter honours `parameters.retry_after`
|
|
11
|
+
* (falling back to the `Retry-After` header, then to exponential backoff), caps
|
|
12
|
+
* the total wait at `maxWaitMs` so one throttled chat cannot stall the whole
|
|
13
|
+
* dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a
|
|
14
|
+
* blocked bot stops being retried forever.
|
|
15
|
+
*/
|
|
16
|
+
/** A raw Telegram Bot API response. */
|
|
17
|
+
interface TelegramResponse {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
status: number;
|
|
20
|
+
/** `result` on success. */
|
|
21
|
+
result: unknown;
|
|
22
|
+
/** `description` on failure. */
|
|
23
|
+
description: string | null;
|
|
24
|
+
/** `parameters.retry_after` in seconds, when Telegram sent one. */
|
|
25
|
+
retryAfterSeconds: number | null;
|
|
26
|
+
}
|
|
27
|
+
/** Options for {@link createTelegramClient}. */
|
|
28
|
+
interface TelegramClientOptions extends NotifierHttpOptions {
|
|
29
|
+
/** Bot token from @BotFather, `<id>:<secret>`. */
|
|
30
|
+
botToken: string;
|
|
31
|
+
/** API origin. Override for a local Bot API server. @defaultValue `https://api.telegram.org` */
|
|
32
|
+
apiBase?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A thin, rate-limit-aware Telegram Bot API client. */
|
|
35
|
+
interface TelegramClient {
|
|
36
|
+
/** Call any Bot API method. Retries 429 and 5xx, never retries 4xx. */
|
|
37
|
+
call(method: string, params: Record<string, unknown>): Promise<TelegramResponse>;
|
|
38
|
+
/** How many requests the last {@link TelegramClient.call} needed. */
|
|
39
|
+
readonly lastAttempts: number;
|
|
40
|
+
}
|
|
41
|
+
/** Build a Telegram Bot API client. */
|
|
42
|
+
declare function createTelegramClient(options: TelegramClientOptions): TelegramClient;
|
|
43
|
+
/** Options for {@link createTelegramNotifier}. */
|
|
44
|
+
interface TelegramNotifierOptions extends TelegramClientOptions {
|
|
45
|
+
/** Suppress link previews. @defaultValue `true` (alerts carry several links). */
|
|
46
|
+
disableWebPagePreview?: boolean;
|
|
47
|
+
/** Deliver silently, without a notification sound. @defaultValue `false` */
|
|
48
|
+
disableNotification?: boolean;
|
|
49
|
+
/** Reuse an existing client instead of building one (the bot shares its client). */
|
|
50
|
+
client?: TelegramClient;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the Telegram delivery adapter.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN! })
|
|
58
|
+
* const result = await telegram.send('-1001234567890', renderAlert(event))
|
|
59
|
+
* if (!result.ok) console.error(result.error, 'retryable:', result.retryable)
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function createTelegramNotifier(options: TelegramNotifierOptions): Notifier;
|
|
63
|
+
|
|
64
|
+
export { type TelegramClient as T, type TelegramClientOptions as a, type TelegramNotifierOptions as b, type TelegramResponse as c, createTelegramClient as d, createTelegramNotifier as e };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { d as NotifierHttpOptions, N as Notifier } from './types-C0ETog04.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram Bot API adapter.
|
|
5
|
+
*
|
|
6
|
+
* Rate limiting is the part everyone gets wrong. Telegram does not use a
|
|
7
|
+
* header budget: it answers `429` with a JSON body
|
|
8
|
+
* `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 7",
|
|
9
|
+
* "parameters":{"retry_after":7}}` and expects the caller to wait exactly that
|
|
10
|
+
* many *seconds* before retrying. This adapter honours `parameters.retry_after`
|
|
11
|
+
* (falling back to the `Retry-After` header, then to exponential backoff), caps
|
|
12
|
+
* the total wait at `maxWaitMs` so one throttled chat cannot stall the whole
|
|
13
|
+
* dispatch loop, and treats `400`/`403` as permanent so a deleted chat or a
|
|
14
|
+
* blocked bot stops being retried forever.
|
|
15
|
+
*/
|
|
16
|
+
/** A raw Telegram Bot API response. */
|
|
17
|
+
interface TelegramResponse {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
status: number;
|
|
20
|
+
/** `result` on success. */
|
|
21
|
+
result: unknown;
|
|
22
|
+
/** `description` on failure. */
|
|
23
|
+
description: string | null;
|
|
24
|
+
/** `parameters.retry_after` in seconds, when Telegram sent one. */
|
|
25
|
+
retryAfterSeconds: number | null;
|
|
26
|
+
}
|
|
27
|
+
/** Options for {@link createTelegramClient}. */
|
|
28
|
+
interface TelegramClientOptions extends NotifierHttpOptions {
|
|
29
|
+
/** Bot token from @BotFather, `<id>:<secret>`. */
|
|
30
|
+
botToken: string;
|
|
31
|
+
/** API origin. Override for a local Bot API server. @defaultValue `https://api.telegram.org` */
|
|
32
|
+
apiBase?: string;
|
|
33
|
+
}
|
|
34
|
+
/** A thin, rate-limit-aware Telegram Bot API client. */
|
|
35
|
+
interface TelegramClient {
|
|
36
|
+
/** Call any Bot API method. Retries 429 and 5xx, never retries 4xx. */
|
|
37
|
+
call(method: string, params: Record<string, unknown>): Promise<TelegramResponse>;
|
|
38
|
+
/** How many requests the last {@link TelegramClient.call} needed. */
|
|
39
|
+
readonly lastAttempts: number;
|
|
40
|
+
}
|
|
41
|
+
/** Build a Telegram Bot API client. */
|
|
42
|
+
declare function createTelegramClient(options: TelegramClientOptions): TelegramClient;
|
|
43
|
+
/** Options for {@link createTelegramNotifier}. */
|
|
44
|
+
interface TelegramNotifierOptions extends TelegramClientOptions {
|
|
45
|
+
/** Suppress link previews. @defaultValue `true` (alerts carry several links). */
|
|
46
|
+
disableWebPagePreview?: boolean;
|
|
47
|
+
/** Deliver silently, without a notification sound. @defaultValue `false` */
|
|
48
|
+
disableNotification?: boolean;
|
|
49
|
+
/** Reuse an existing client instead of building one (the bot shares its client). */
|
|
50
|
+
client?: TelegramClient;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the Telegram delivery adapter.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const telegram = createTelegramNotifier({ botToken: process.env.TELEGRAM_BOT_TOKEN! })
|
|
58
|
+
* const result = await telegram.send('-1001234567890', renderAlert(event))
|
|
59
|
+
* if (!result.ok) console.error(result.error, 'retryable:', result.retryable)
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function createTelegramNotifier(options: TelegramNotifierOptions): Notifier;
|
|
63
|
+
|
|
64
|
+
export { type TelegramClient as T, type TelegramClientOptions as a, type TelegramNotifierOptions as b, type TelegramResponse as c, createTelegramClient as d, createTelegramNotifier as e };
|