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,614 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/rules/index.ts
|
|
21
|
+
var rules_exports = {};
|
|
22
|
+
__export(rules_exports, {
|
|
23
|
+
addressSchema: () => addressSchema,
|
|
24
|
+
canEmit: () => canEmit,
|
|
25
|
+
checkRateLimit: () => checkRateLimit,
|
|
26
|
+
createMemoryRateLimitStore: () => createMemoryRateLimitStore,
|
|
27
|
+
createRpcReputationProvider: () => createRpcReputationProvider,
|
|
28
|
+
defaultRules: () => defaultRules,
|
|
29
|
+
evaluateRule: () => evaluateRule,
|
|
30
|
+
eventKindSchema: () => eventKindSchema,
|
|
31
|
+
formatValidationError: () => formatValidationError,
|
|
32
|
+
launchpadSchema: () => launchpadSchema,
|
|
33
|
+
matchRules: () => matchRules,
|
|
34
|
+
parseRule: () => parseRule,
|
|
35
|
+
parseSubscription: () => parseSubscription,
|
|
36
|
+
platformSchema: () => platformSchema,
|
|
37
|
+
priorLaunchesBefore: () => priorLaunchesBefore,
|
|
38
|
+
rateLimitKey: () => rateLimitKey,
|
|
39
|
+
rateLimitSchema: () => rateLimitSchema,
|
|
40
|
+
reputationFilterSchema: () => reputationFilterSchema,
|
|
41
|
+
ruleComplexity: () => ruleComplexity,
|
|
42
|
+
ruleSchema: () => ruleSchema,
|
|
43
|
+
safeParseRule: () => safeParseRule,
|
|
44
|
+
safeParseSubscription: () => safeParseSubscription,
|
|
45
|
+
sideSchema: () => sideSchema,
|
|
46
|
+
subscriptionSchema: () => subscriptionSchema,
|
|
47
|
+
usesOnChainLookups: () => usesOnChainLookups
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(rules_exports);
|
|
50
|
+
|
|
51
|
+
// src/rules/schema.ts
|
|
52
|
+
var import_zod = require("zod");
|
|
53
|
+
|
|
54
|
+
// src/events/types.ts
|
|
55
|
+
var LAUNCHPAD_EVENT_KINDS = {
|
|
56
|
+
noxa: ["launch", "whale_trade"],
|
|
57
|
+
odyssey: ["launch", "curve_trade", "graduation", "whale_trade"]
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/rules/schema.ts
|
|
61
|
+
var addressPattern = /^0x[0-9a-fA-F]{40}$/;
|
|
62
|
+
var addressSchema = import_zod.z.string().regex(addressPattern, "must be a 0x-prefixed 20-byte address").transform((value) => value.toLowerCase());
|
|
63
|
+
var eventKindSchema = import_zod.z.enum(["launch", "curve_trade", "graduation", "whale_trade"]);
|
|
64
|
+
var launchpadSchema = import_zod.z.enum(["noxa", "odyssey"]);
|
|
65
|
+
var sideSchema = import_zod.z.enum(["buy", "sell", "any"]);
|
|
66
|
+
var reputationFilterSchema = import_zod.z.object({
|
|
67
|
+
/** Require the deployer to have launched at least this many tokens before. */
|
|
68
|
+
minPriorLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
|
|
69
|
+
/** Reject serial deployers above this prior-launch count. */
|
|
70
|
+
maxPriorLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
|
|
71
|
+
/**
|
|
72
|
+
* Reject a deployer with more than this many prior launches whose pool
|
|
73
|
+
* liquidity has since been drained below the rug threshold.
|
|
74
|
+
*/
|
|
75
|
+
maxRuggedLaunches: import_zod.z.number().int().min(0).max(1e4).optional(),
|
|
76
|
+
/**
|
|
77
|
+
* Require this launch's own LP position to be held by the launchpad's
|
|
78
|
+
* locker contract. Only meaningful for `launch` events that carry a pool
|
|
79
|
+
* (NOXA); events without a pool never satisfy it.
|
|
80
|
+
*/
|
|
81
|
+
requireLpLocked: import_zod.z.boolean().optional()
|
|
82
|
+
}).strict().refine(
|
|
83
|
+
(value) => value.minPriorLaunches === void 0 || value.maxPriorLaunches === void 0 || value.minPriorLaunches <= value.maxPriorLaunches,
|
|
84
|
+
{ message: "minPriorLaunches must not exceed maxPriorLaunches" }
|
|
85
|
+
);
|
|
86
|
+
var rateLimitSchema = import_zod.z.object({
|
|
87
|
+
/** Hard cap on alerts this rule may deliver per rolling hour. */
|
|
88
|
+
maxPerHour: import_zod.z.number().int().min(1).max(1e4).optional(),
|
|
89
|
+
/** Minimum gap between two alerts from this rule, in seconds. */
|
|
90
|
+
minIntervalSeconds: import_zod.z.number().int().min(1).max(86400).optional()
|
|
91
|
+
}).strict();
|
|
92
|
+
var ruleSchema = import_zod.z.object({
|
|
93
|
+
/** Stable id, unique within a subscription. Used in bot commands. */
|
|
94
|
+
id: import_zod.z.string().min(1).max(32).regex(/^[a-z0-9][a-z0-9_-]*$/, "must be lowercase letters, digits, hyphen or underscore"),
|
|
95
|
+
/** Human label shown in `/rules`. */
|
|
96
|
+
name: import_zod.z.string().min(1).max(64).optional(),
|
|
97
|
+
enabled: import_zod.z.boolean().default(true),
|
|
98
|
+
/** Event kinds this rule reacts to. Defaults to all four. */
|
|
99
|
+
kinds: import_zod.z.array(eventKindSchema).min(1).max(4).default(["launch", "curve_trade", "graduation", "whale_trade"]),
|
|
100
|
+
/** Launchpads this rule reacts to. Defaults to both. */
|
|
101
|
+
launchpads: import_zod.z.array(launchpadSchema).min(1).max(2).default(["noxa", "odyssey"]),
|
|
102
|
+
/** Minimum USD value of the event. Events with an unknown value never pass. */
|
|
103
|
+
minUsd: import_zod.z.number().min(0).max(1e12).optional(),
|
|
104
|
+
/** Maximum USD value of the event. */
|
|
105
|
+
maxUsd: import_zod.z.number().min(0).max(1e12).optional(),
|
|
106
|
+
/** Minimum USD liquidity of the token's deepest known pool. */
|
|
107
|
+
minLiquidityUsd: import_zod.z.number().min(0).max(1e12).optional(),
|
|
108
|
+
/** Maximum USD liquidity of the token's deepest known pool. */
|
|
109
|
+
maxLiquidityUsd: import_zod.z.number().min(0).max(1e12).optional(),
|
|
110
|
+
/** Watchlist. When non-empty, only these tokens match. */
|
|
111
|
+
tokens: import_zod.z.array(addressSchema).max(500).default([]),
|
|
112
|
+
/** Deployer watchlist. When non-empty, only these actors match. */
|
|
113
|
+
deployers: import_zod.z.array(addressSchema).max(500).default([]),
|
|
114
|
+
/** Deployers to always reject, applied after every other filter. */
|
|
115
|
+
excludeDeployers: import_zod.z.array(addressSchema).max(500).default([]),
|
|
116
|
+
/** Trade direction. Ignored by `launch` and `graduation` events. */
|
|
117
|
+
side: sideSchema.default("any"),
|
|
118
|
+
reputation: reputationFilterSchema.optional(),
|
|
119
|
+
rateLimit: rateLimitSchema.optional()
|
|
120
|
+
}).strict().refine((rule) => rule.minUsd === void 0 || rule.maxUsd === void 0 || rule.minUsd <= rule.maxUsd, {
|
|
121
|
+
message: "minUsd must not exceed maxUsd",
|
|
122
|
+
path: ["minUsd"]
|
|
123
|
+
}).refine(
|
|
124
|
+
(rule) => rule.minLiquidityUsd === void 0 || rule.maxLiquidityUsd === void 0 || rule.minLiquidityUsd <= rule.maxLiquidityUsd,
|
|
125
|
+
{ message: "minLiquidityUsd must not exceed maxLiquidityUsd", path: ["minLiquidityUsd"] }
|
|
126
|
+
).refine((rule) => rule.kinds.some((kind) => rule.launchpads.some((pad) => canEmit(pad, kind))), {
|
|
127
|
+
message: "this combination can never fire: NOXA is an instant launcher with no bonding curve, so it emits no curve_trade and no graduation",
|
|
128
|
+
path: ["kinds"]
|
|
129
|
+
});
|
|
130
|
+
var platformSchema = import_zod.z.enum(["telegram", "discord"]);
|
|
131
|
+
var subscriptionSchema = import_zod.z.object({
|
|
132
|
+
id: import_zod.z.string().min(1).max(64),
|
|
133
|
+
/**
|
|
134
|
+
* The account that owns this subscription, and the unit entitlements are
|
|
135
|
+
* checked against. For Telegram this is the user id, for Discord the guild
|
|
136
|
+
* id (a server pays once for its channels) or user id for a DM.
|
|
137
|
+
*/
|
|
138
|
+
subscriberId: import_zod.z.string().min(1).max(128),
|
|
139
|
+
platform: platformSchema,
|
|
140
|
+
/**
|
|
141
|
+
* Where alerts go: a Telegram chat id, a Discord channel id, or a Discord
|
|
142
|
+
* webhook URL. Validated per platform by the notifier, not here, because
|
|
143
|
+
* the same subscription record serves all three shapes.
|
|
144
|
+
*/
|
|
145
|
+
target: import_zod.z.string().min(1).max(512),
|
|
146
|
+
enabled: import_zod.z.boolean().default(true),
|
|
147
|
+
rules: import_zod.z.array(ruleSchema).max(100).default([]),
|
|
148
|
+
createdAtMs: import_zod.z.number().int().nonnegative()
|
|
149
|
+
}).strict().refine((sub) => new Set(sub.rules.map((rule) => rule.id)).size === sub.rules.length, {
|
|
150
|
+
message: "rule ids must be unique within a subscription",
|
|
151
|
+
path: ["rules"]
|
|
152
|
+
});
|
|
153
|
+
function canEmit(launchpad, kind) {
|
|
154
|
+
return LAUNCHPAD_EVENT_KINDS[launchpad].includes(kind);
|
|
155
|
+
}
|
|
156
|
+
function ruleComplexity(rule) {
|
|
157
|
+
let score = 0;
|
|
158
|
+
if (rule.minUsd !== void 0) score += 1;
|
|
159
|
+
if (rule.maxUsd !== void 0) score += 1;
|
|
160
|
+
if (rule.minLiquidityUsd !== void 0) score += 1;
|
|
161
|
+
if (rule.maxLiquidityUsd !== void 0) score += 1;
|
|
162
|
+
if (rule.tokens.length > 0) score += 1;
|
|
163
|
+
if (rule.deployers.length > 0) score += 1;
|
|
164
|
+
if (rule.excludeDeployers.length > 0) score += 1;
|
|
165
|
+
if (rule.side !== "any") score += 1;
|
|
166
|
+
if (rule.reputation) score += Object.values(rule.reputation).filter((v) => v !== void 0).length;
|
|
167
|
+
if (rule.rateLimit) score += 1;
|
|
168
|
+
return score;
|
|
169
|
+
}
|
|
170
|
+
function usesOnChainLookups(rule) {
|
|
171
|
+
return rule.minLiquidityUsd !== void 0 || rule.maxLiquidityUsd !== void 0 || rule.reputation !== void 0 && Object.values(rule.reputation).some((value) => value !== void 0);
|
|
172
|
+
}
|
|
173
|
+
function parseRule(input) {
|
|
174
|
+
return ruleSchema.parse(input);
|
|
175
|
+
}
|
|
176
|
+
function safeParseRule(input) {
|
|
177
|
+
return ruleSchema.safeParse(input);
|
|
178
|
+
}
|
|
179
|
+
function parseSubscription(input) {
|
|
180
|
+
return subscriptionSchema.parse(input);
|
|
181
|
+
}
|
|
182
|
+
function safeParseSubscription(input) {
|
|
183
|
+
return subscriptionSchema.safeParse(input);
|
|
184
|
+
}
|
|
185
|
+
function formatValidationError(error) {
|
|
186
|
+
return error.issues.map((issue) => {
|
|
187
|
+
const path = issue.path.join(".");
|
|
188
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
189
|
+
}).join("; ");
|
|
190
|
+
}
|
|
191
|
+
function defaultRules() {
|
|
192
|
+
return [
|
|
193
|
+
parseRule({ id: "launches", name: "New launches", kinds: ["launch"] }),
|
|
194
|
+
parseRule({
|
|
195
|
+
id: "graduations",
|
|
196
|
+
name: "Odyssey graduations",
|
|
197
|
+
kinds: ["graduation"],
|
|
198
|
+
launchpads: ["odyssey"]
|
|
199
|
+
}),
|
|
200
|
+
parseRule({ id: "whales", name: "Whale trades over $5k", kinds: ["whale_trade"], minUsd: 5e3 })
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/rules/reputation.ts
|
|
205
|
+
var import_viem2 = require("viem");
|
|
206
|
+
var import_hoodchain2 = require("hoodchain");
|
|
207
|
+
|
|
208
|
+
// src/events/logs.ts
|
|
209
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
210
|
+
function isResultTooLarge(error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
return /exceeds limit|too many results|response size exceeded|query returned more than/i.test(message);
|
|
213
|
+
}
|
|
214
|
+
function isRetryableRpcError(error) {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return /too many requests|rate limit|timed out|timeout|429|503|502|ECONNRESET|socket hang up|fetch failed/i.test(
|
|
217
|
+
message
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
async function fetchLogRange(fetcher, from, to, options = {}) {
|
|
221
|
+
const minSpan = options.minSpan ?? 1n;
|
|
222
|
+
const retries = options.retries ?? 5;
|
|
223
|
+
const backoffMs = options.backoffMs ?? 500;
|
|
224
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
225
|
+
const run = async (lo, hi) => {
|
|
226
|
+
let attempt = 0;
|
|
227
|
+
for (; ; ) {
|
|
228
|
+
try {
|
|
229
|
+
return [...await fetcher(lo, hi)];
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (isResultTooLarge(error)) {
|
|
232
|
+
const span = hi - lo;
|
|
233
|
+
if (span < minSpan || span === 0n) throw error;
|
|
234
|
+
const mid = lo + span / 2n;
|
|
235
|
+
const left = await run(lo, mid);
|
|
236
|
+
const right = await run(mid + 1n, hi);
|
|
237
|
+
return [...left, ...right];
|
|
238
|
+
}
|
|
239
|
+
if (isRetryableRpcError(error) && attempt < retries) {
|
|
240
|
+
await sleep(backoffMs * 2 ** attempt);
|
|
241
|
+
attempt += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
if (to < from) return [];
|
|
249
|
+
return run(from, to);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/events/pricing.ts
|
|
253
|
+
var import_viem = require("viem");
|
|
254
|
+
var import_hoodchain = require("hoodchain");
|
|
255
|
+
function quoteTokens(client) {
|
|
256
|
+
return client.network === "testnet" ? { weth: import_hoodchain.TESTNET_ADDRESSES.weth, usdg: import_hoodchain.TESTNET_ADDRESSES.usdg } : { weth: import_hoodchain.MAINNET_ADDRESSES.weth, usdg: import_hoodchain.MAINNET_ADDRESSES.usdg };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/rules/reputation.ts
|
|
260
|
+
var positionManagerAbi = [
|
|
261
|
+
{
|
|
262
|
+
type: "function",
|
|
263
|
+
name: "ownerOf",
|
|
264
|
+
stateMutability: "view",
|
|
265
|
+
inputs: [{ name: "tokenId", type: "uint256" }],
|
|
266
|
+
outputs: [{ type: "address" }]
|
|
267
|
+
}
|
|
268
|
+
];
|
|
269
|
+
var ODYSSEY_FACTORIES = [
|
|
270
|
+
import_hoodchain2.ODYSSEY_ADDRESSES.bondingCurveFactory,
|
|
271
|
+
import_hoodchain2.ODYSSEY_ADDRESSES.reflectionFactory,
|
|
272
|
+
import_hoodchain2.ODYSSEY_ADDRESSES.instantFactory
|
|
273
|
+
];
|
|
274
|
+
function createRpcReputationProvider(client, oracle, options = {}) {
|
|
275
|
+
const ttlMs = options.ttlMs ?? 6e5;
|
|
276
|
+
const maxHistory = options.maxHistory ?? 200;
|
|
277
|
+
const maxInspect = options.maxInspect ?? 25;
|
|
278
|
+
const rugThresholdUsd = options.rugThresholdUsd ?? 50;
|
|
279
|
+
const now = options.now ?? Date.now;
|
|
280
|
+
const cache = /* @__PURE__ */ new Map();
|
|
281
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
282
|
+
const lockCache = /* @__PURE__ */ new Map();
|
|
283
|
+
const { weth, usdg } = quoteTokens(client);
|
|
284
|
+
async function reserveUsd(pool, quoteToken) {
|
|
285
|
+
const quote = quoteToken.toLowerCase();
|
|
286
|
+
if (quote !== weth.toLowerCase() && quote !== usdg.toLowerCase()) return null;
|
|
287
|
+
const balance = await client.public.readContract({
|
|
288
|
+
address: quoteToken,
|
|
289
|
+
abi: import_hoodchain2.erc20Abi,
|
|
290
|
+
functionName: "balanceOf",
|
|
291
|
+
args: [pool]
|
|
292
|
+
});
|
|
293
|
+
if (quote === usdg.toLowerCase()) return Number((0, import_viem2.formatUnits)(balance, import_hoodchain2.USDG_DECIMALS));
|
|
294
|
+
return oracle.weiToUsd(balance);
|
|
295
|
+
}
|
|
296
|
+
async function isLpLocked(positionId) {
|
|
297
|
+
const key = positionId.toString();
|
|
298
|
+
const hit = lockCache.get(key);
|
|
299
|
+
if (hit !== void 0) return hit;
|
|
300
|
+
try {
|
|
301
|
+
const owner = await client.public.readContract({
|
|
302
|
+
address: import_hoodchain2.MAINNET_ADDRESSES.nonfungiblePositionManager,
|
|
303
|
+
abi: positionManagerAbi,
|
|
304
|
+
functionName: "ownerOf",
|
|
305
|
+
args: [positionId]
|
|
306
|
+
});
|
|
307
|
+
const locked = owner.toLowerCase() === import_hoodchain2.NOXA_ADDRESSES.locker.toLowerCase();
|
|
308
|
+
lockCache.set(key, locked);
|
|
309
|
+
return locked;
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function load(deployer) {
|
|
315
|
+
const head = await client.public.getBlockNumber();
|
|
316
|
+
const [noxaLogs, odysseyLogs] = await Promise.all([
|
|
317
|
+
fetchLogRange(
|
|
318
|
+
(from, to) => client.public.getLogs({
|
|
319
|
+
address: import_hoodchain2.NOXA_ADDRESSES.launchFactory,
|
|
320
|
+
event: import_hoodchain2.noxaTokenLaunchedEvent,
|
|
321
|
+
args: { deployer },
|
|
322
|
+
fromBlock: from,
|
|
323
|
+
toBlock: to
|
|
324
|
+
}),
|
|
325
|
+
import_hoodchain2.NOXA_ADDRESSES.deployBlock,
|
|
326
|
+
head,
|
|
327
|
+
options.logOptions ?? {}
|
|
328
|
+
),
|
|
329
|
+
fetchLogRange(
|
|
330
|
+
(from, to) => client.public.getLogs({
|
|
331
|
+
address: ODYSSEY_FACTORIES,
|
|
332
|
+
event: import_hoodchain2.odysseyTokenCreatedEvent,
|
|
333
|
+
args: { creator: deployer },
|
|
334
|
+
fromBlock: from,
|
|
335
|
+
toBlock: to
|
|
336
|
+
}),
|
|
337
|
+
import_hoodchain2.NOXA_ADDRESSES.deployBlock,
|
|
338
|
+
head,
|
|
339
|
+
options.logOptions ?? {}
|
|
340
|
+
)
|
|
341
|
+
]);
|
|
342
|
+
const launches = [];
|
|
343
|
+
for (const log of noxaLogs) {
|
|
344
|
+
const token = log.args.token;
|
|
345
|
+
if (!token) continue;
|
|
346
|
+
const pool = log.args.pool ?? null;
|
|
347
|
+
const pairToken = log.args.pairToken ?? null;
|
|
348
|
+
launches.push({
|
|
349
|
+
launchpad: "noxa",
|
|
350
|
+
token: (0, import_viem2.getAddress)(token),
|
|
351
|
+
pool: pool ? (0, import_viem2.getAddress)(pool) : null,
|
|
352
|
+
quoteToken: pairToken ? (0, import_viem2.getAddress)(pairToken) : null,
|
|
353
|
+
positionId: log.args.positionId ?? null,
|
|
354
|
+
blockNumber: log.blockNumber
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
for (const log of odysseyLogs) {
|
|
358
|
+
const token = log.args.token;
|
|
359
|
+
if (!token) continue;
|
|
360
|
+
launches.push({
|
|
361
|
+
launchpad: "odyssey",
|
|
362
|
+
token: (0, import_viem2.getAddress)(token),
|
|
363
|
+
pool: null,
|
|
364
|
+
quoteToken: null,
|
|
365
|
+
positionId: null,
|
|
366
|
+
blockNumber: log.blockNumber
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
launches.sort((a, b) => a.blockNumber < b.blockNumber ? -1 : a.blockNumber > b.blockNumber ? 1 : 0);
|
|
370
|
+
const kept = launches.slice(-maxHistory);
|
|
371
|
+
const inspectable = kept.slice(-maxInspect);
|
|
372
|
+
let lockedLaunches = 0;
|
|
373
|
+
let ruggedLaunches = 0;
|
|
374
|
+
for (const launch of inspectable) {
|
|
375
|
+
if (launch.positionId !== null && await isLpLocked(launch.positionId)) lockedLaunches += 1;
|
|
376
|
+
if (launch.pool && launch.quoteToken) {
|
|
377
|
+
try {
|
|
378
|
+
const usd = await reserveUsd(launch.pool, launch.quoteToken);
|
|
379
|
+
if (usd !== null && usd < rugThresholdUsd) ruggedLaunches += 1;
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
deployer: (0, import_viem2.getAddress)(deployer),
|
|
386
|
+
launches: kept,
|
|
387
|
+
lockedLaunches,
|
|
388
|
+
ruggedLaunches,
|
|
389
|
+
inspected: inspectable.length,
|
|
390
|
+
computedAtMs: now()
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
async get(deployer) {
|
|
395
|
+
const key = deployer.toLowerCase();
|
|
396
|
+
const hit = cache.get(key);
|
|
397
|
+
if (hit && now() - hit.computedAtMs < ttlMs) return hit;
|
|
398
|
+
const pending = inflight.get(key);
|
|
399
|
+
if (pending) return pending;
|
|
400
|
+
const promise = load(deployer).then((record) => {
|
|
401
|
+
cache.set(key, record);
|
|
402
|
+
return record;
|
|
403
|
+
}).finally(() => {
|
|
404
|
+
inflight.delete(key);
|
|
405
|
+
});
|
|
406
|
+
inflight.set(key, promise);
|
|
407
|
+
return promise;
|
|
408
|
+
},
|
|
409
|
+
isLpLocked
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function priorLaunchesBefore(record, blockNumber) {
|
|
413
|
+
return record.launches.filter((launch) => launch.blockNumber < blockNumber).length;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/rules/engine.ts
|
|
417
|
+
var matchOk = (rule) => ({ matched: true, reason: `rule ${rule.id} matched` });
|
|
418
|
+
var miss = (reason) => ({ matched: false, reason });
|
|
419
|
+
function eventSide(event) {
|
|
420
|
+
if (event.kind === "curve_trade" || event.kind === "whale_trade") return event.side;
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
function eventPositionId(event) {
|
|
424
|
+
if (event.kind === "launch") return event.positionId;
|
|
425
|
+
if (event.kind === "graduation") return event.positionId;
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
async function evaluateRule(rule, event, context = {}) {
|
|
429
|
+
if (!rule.enabled) return miss(`rule ${rule.id} is disabled`);
|
|
430
|
+
if (!rule.kinds.includes(event.kind)) {
|
|
431
|
+
return miss(`kind ${event.kind} not in [${rule.kinds.join(", ")}]`);
|
|
432
|
+
}
|
|
433
|
+
if (!rule.launchpads.includes(event.launchpad)) {
|
|
434
|
+
return miss(`launchpad ${event.launchpad} not in [${rule.launchpads.join(", ")}]`);
|
|
435
|
+
}
|
|
436
|
+
const actor = event.actor.toLowerCase();
|
|
437
|
+
if (rule.excludeDeployers.includes(actor)) {
|
|
438
|
+
return miss(`actor ${event.actor} is on the exclude list`);
|
|
439
|
+
}
|
|
440
|
+
if (rule.tokens.length > 0 && !rule.tokens.includes(event.token.toLowerCase())) {
|
|
441
|
+
return miss(`token ${event.token} is not on the watchlist`);
|
|
442
|
+
}
|
|
443
|
+
if (rule.deployers.length > 0 && !rule.deployers.includes(actor)) {
|
|
444
|
+
return miss(`actor ${event.actor} is not on the deployer watchlist`);
|
|
445
|
+
}
|
|
446
|
+
if (rule.side !== "any") {
|
|
447
|
+
const side = eventSide(event);
|
|
448
|
+
if (side === null) return miss(`side filter does not apply to ${event.kind} events`);
|
|
449
|
+
if (side !== rule.side) return miss(`side ${side} is not ${rule.side}`);
|
|
450
|
+
}
|
|
451
|
+
if (rule.minUsd !== void 0) {
|
|
452
|
+
if (event.usdValue === null) return miss("event has no USD value to compare against minUsd");
|
|
453
|
+
if (event.usdValue < rule.minUsd) {
|
|
454
|
+
return miss(`usdValue ${event.usdValue.toFixed(2)} below minUsd ${rule.minUsd}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (rule.maxUsd !== void 0) {
|
|
458
|
+
if (event.usdValue === null) return miss("event has no USD value to compare against maxUsd");
|
|
459
|
+
if (event.usdValue > rule.maxUsd) {
|
|
460
|
+
return miss(`usdValue ${event.usdValue.toFixed(2)} above maxUsd ${rule.maxUsd}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (rule.minLiquidityUsd !== void 0 || rule.maxLiquidityUsd !== void 0) {
|
|
464
|
+
if (!context.liquidityUsd) {
|
|
465
|
+
return miss("rule filters on liquidity but no liquidity reader was provided");
|
|
466
|
+
}
|
|
467
|
+
const liquidity = await context.liquidityUsd(event);
|
|
468
|
+
if (liquidity === null) return miss("token liquidity is unknown");
|
|
469
|
+
if (rule.minLiquidityUsd !== void 0 && liquidity < rule.minLiquidityUsd) {
|
|
470
|
+
return miss(`liquidity ${liquidity.toFixed(2)} below minLiquidityUsd ${rule.minLiquidityUsd}`);
|
|
471
|
+
}
|
|
472
|
+
if (rule.maxLiquidityUsd !== void 0 && liquidity > rule.maxLiquidityUsd) {
|
|
473
|
+
return miss(`liquidity ${liquidity.toFixed(2)} above maxLiquidityUsd ${rule.maxLiquidityUsd}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const reputationFilter = rule.reputation;
|
|
477
|
+
if (reputationFilter) {
|
|
478
|
+
const needsRecord = reputationFilter.minPriorLaunches !== void 0 || reputationFilter.maxPriorLaunches !== void 0 || reputationFilter.maxRuggedLaunches !== void 0;
|
|
479
|
+
if (needsRecord) {
|
|
480
|
+
if (!context.reputation) {
|
|
481
|
+
return miss("rule filters on reputation but no reputation provider was configured");
|
|
482
|
+
}
|
|
483
|
+
const record = await context.reputation(event.actor);
|
|
484
|
+
const prior = priorLaunchesBefore(record, event.blockNumber);
|
|
485
|
+
if (reputationFilter.minPriorLaunches !== void 0 && prior < reputationFilter.minPriorLaunches) {
|
|
486
|
+
return miss(`deployer had ${prior} prior launches, needs ${reputationFilter.minPriorLaunches}`);
|
|
487
|
+
}
|
|
488
|
+
if (reputationFilter.maxPriorLaunches !== void 0 && prior > reputationFilter.maxPriorLaunches) {
|
|
489
|
+
return miss(`deployer had ${prior} prior launches, max is ${reputationFilter.maxPriorLaunches}`);
|
|
490
|
+
}
|
|
491
|
+
if (reputationFilter.maxRuggedLaunches !== void 0 && record.ruggedLaunches > reputationFilter.maxRuggedLaunches) {
|
|
492
|
+
return miss(
|
|
493
|
+
`deployer has ${record.ruggedLaunches} drained pools, max is ${reputationFilter.maxRuggedLaunches}`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (reputationFilter.requireLpLocked === true) {
|
|
498
|
+
const positionId = eventPositionId(event);
|
|
499
|
+
if (positionId === null) return miss("event carries no LP position to check for a lock");
|
|
500
|
+
if (!context.isLpLocked) {
|
|
501
|
+
return miss("rule requires an LP lock check but no reputation provider was configured");
|
|
502
|
+
}
|
|
503
|
+
if (!await context.isLpLocked(positionId)) {
|
|
504
|
+
return miss(`LP position ${positionId} is not held by the launchpad locker`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return matchOk(rule);
|
|
509
|
+
}
|
|
510
|
+
async function matchRules(rules, event, context = {}) {
|
|
511
|
+
const results = await Promise.all(
|
|
512
|
+
rules.map(async (rule) => {
|
|
513
|
+
try {
|
|
514
|
+
return { rule, result: await evaluateRule(rule, event, context) };
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
return { rule, result: miss(`evaluation failed: ${message}`) };
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
return results.filter((entry) => entry.result.matched);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/rules/ratelimit.ts
|
|
525
|
+
var ALLOWED = { allowed: true, reason: null, retryAtMs: null };
|
|
526
|
+
async function checkRateLimit(store, key, config, nowMs) {
|
|
527
|
+
if (!config) return ALLOWED;
|
|
528
|
+
if (config.minIntervalSeconds !== void 0) {
|
|
529
|
+
const last = await store.lastAt(key);
|
|
530
|
+
if (last !== null) {
|
|
531
|
+
const nextAllowed = last + config.minIntervalSeconds * 1e3;
|
|
532
|
+
if (nowMs < nextAllowed) {
|
|
533
|
+
return {
|
|
534
|
+
allowed: false,
|
|
535
|
+
reason: `minIntervalSeconds=${config.minIntervalSeconds} not elapsed`,
|
|
536
|
+
retryAtMs: nextAllowed
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (config.maxPerHour !== void 0) {
|
|
542
|
+
const windowStart = nowMs - 36e5;
|
|
543
|
+
const used = await store.countSince(key, windowStart);
|
|
544
|
+
if (used >= config.maxPerHour) {
|
|
545
|
+
return {
|
|
546
|
+
allowed: false,
|
|
547
|
+
reason: `maxPerHour=${config.maxPerHour} reached (${used} in the last hour)`,
|
|
548
|
+
retryAtMs: windowStart + 36e5
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return ALLOWED;
|
|
553
|
+
}
|
|
554
|
+
function createMemoryRateLimitStore() {
|
|
555
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
556
|
+
const bySubscriber = /* @__PURE__ */ new Map();
|
|
557
|
+
const push = (map, id, atMs) => {
|
|
558
|
+
const list = map.get(id) ?? [];
|
|
559
|
+
list.push(atMs);
|
|
560
|
+
map.set(
|
|
561
|
+
id,
|
|
562
|
+
list.filter((at) => at >= atMs - 36e5)
|
|
563
|
+
);
|
|
564
|
+
};
|
|
565
|
+
return {
|
|
566
|
+
async countSince(key, sinceMs) {
|
|
567
|
+
return (byKey.get(key) ?? []).filter((at) => at >= sinceMs).length;
|
|
568
|
+
},
|
|
569
|
+
async lastAt(key) {
|
|
570
|
+
const list = byKey.get(key);
|
|
571
|
+
if (!list || list.length === 0) return null;
|
|
572
|
+
return list[list.length - 1] ?? null;
|
|
573
|
+
},
|
|
574
|
+
async record(key, subscriberId, atMs) {
|
|
575
|
+
push(byKey, key, atMs);
|
|
576
|
+
push(bySubscriber, subscriberId, atMs);
|
|
577
|
+
},
|
|
578
|
+
async countForSubscriberSince(subscriberId, sinceMs) {
|
|
579
|
+
return (bySubscriber.get(subscriberId) ?? []).filter((at) => at >= sinceMs).length;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function rateLimitKey(subscriptionId, ruleId) {
|
|
584
|
+
return `${subscriptionId}:${ruleId}`;
|
|
585
|
+
}
|
|
586
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
587
|
+
0 && (module.exports = {
|
|
588
|
+
addressSchema,
|
|
589
|
+
canEmit,
|
|
590
|
+
checkRateLimit,
|
|
591
|
+
createMemoryRateLimitStore,
|
|
592
|
+
createRpcReputationProvider,
|
|
593
|
+
defaultRules,
|
|
594
|
+
evaluateRule,
|
|
595
|
+
eventKindSchema,
|
|
596
|
+
formatValidationError,
|
|
597
|
+
launchpadSchema,
|
|
598
|
+
matchRules,
|
|
599
|
+
parseRule,
|
|
600
|
+
parseSubscription,
|
|
601
|
+
platformSchema,
|
|
602
|
+
priorLaunchesBefore,
|
|
603
|
+
rateLimitKey,
|
|
604
|
+
rateLimitSchema,
|
|
605
|
+
reputationFilterSchema,
|
|
606
|
+
ruleComplexity,
|
|
607
|
+
ruleSchema,
|
|
608
|
+
safeParseRule,
|
|
609
|
+
safeParseSubscription,
|
|
610
|
+
sideSchema,
|
|
611
|
+
subscriptionSchema,
|
|
612
|
+
usesOnChainLookups
|
|
613
|
+
});
|
|
614
|
+
//# sourceMappingURL=index.cjs.map
|