@gearbox-protocol/cli-utils 5.67.1 → 5.68.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.
@@ -1,6 +1,6 @@
1
1
  export declare class CensoredString<V extends string = string> {
2
2
  #private;
3
- static transform<T extends string>(value: T): CensoredString<T>;
3
+ static transform<T extends string>(value: T | CensoredString<T>): CensoredString<T>;
4
4
  constructor(value: V);
5
5
  get value(): V;
6
6
  toString(): string;
@@ -2,6 +2,9 @@ export class CensoredString {
2
2
  #value;
3
3
  #censored;
4
4
  static transform(value) {
5
+ if (value instanceof CensoredString) {
6
+ return value;
7
+ }
5
8
  return new CensoredString(value);
6
9
  }
7
10
  constructor(value) {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export declare class CensoredURL {
5
5
  #private;
6
- static transform(value: string): CensoredURL;
6
+ static transform(value: string | CensoredURL): CensoredURL;
7
7
  constructor(url: string);
8
8
  get value(): string;
9
9
  toString(): string;
@@ -5,6 +5,9 @@ export class CensoredURL {
5
5
  #value;
6
6
  #censored;
7
7
  static transform(value) {
8
+ if (value instanceof CensoredURL) {
9
+ return value;
10
+ }
8
11
  return new CensoredURL(value);
9
12
  }
10
13
  constructor(url) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./CensoredString.js";
2
2
  export * from "./CensoredURL.js";
3
3
  export * from "./createRevolverTransport.js";
4
+ export * from "./notifications/index.js";
4
5
  export * from "./providers.js";
5
6
  export * from "./providers-schema.js";
6
7
  export * from "./resolveYamlFiles.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./CensoredString.js";
2
2
  export * from "./CensoredURL.js";
3
3
  export * from "./createRevolverTransport.js";
4
+ export * from "./notifications/index.js";
4
5
  export * from "./providers.js";
5
6
  export * from "./providers-schema.js";
6
7
  export * from "./resolveYamlFiles.js";
@@ -0,0 +1,19 @@
1
+ import { type ILogger } from "@gearbox-protocol/sdk";
2
+ import type { Address } from "viem";
3
+ import { type SignlServiceOptions } from "./SignlNotifier.js";
4
+ import type { NotificationConfig } from "./schema.js";
5
+ import type { INotificationService, NotificationLike } from "./types.js";
6
+ export interface NotificationsServiceOptions {
7
+ notifications: Array<NotificationConfig & {
8
+ recipient?: Address;
9
+ }>;
10
+ signl?: SignlServiceOptions;
11
+ }
12
+ export declare class NotificationsService implements INotificationService {
13
+ #private;
14
+ constructor(options: NotificationsServiceOptions, logger?: ILogger);
15
+ registerRecipient(address: Address, options: NotificationConfig): void;
16
+ signl(message: string): void;
17
+ alert(msg: NotificationLike): void;
18
+ notify(msg: NotificationLike): void;
19
+ }
@@ -0,0 +1,66 @@
1
+ import { ADDRESS_0X0, AddressMap } from "@gearbox-protocol/sdk";
2
+ import { createNotifier } from "./createNotifier.js";
3
+ import SignlNotifier from "./SignlNotifier.js";
4
+ export class NotificationsService {
5
+ #logger;
6
+ #notifiers = new AddressMap();
7
+ #signlNotifier;
8
+ constructor(options, logger) {
9
+ this.#logger = logger;
10
+ for (const n of options.notifications) {
11
+ const recipient = n.recipient ?? ADDRESS_0X0;
12
+ const notifier = createNotifier(n, logger);
13
+ if (notifier) {
14
+ this.#notifiers.upsert(recipient, notifier);
15
+ }
16
+ }
17
+ if (options.signl) {
18
+ this.#signlNotifier = new SignlNotifier(options.signl);
19
+ }
20
+ else {
21
+ this.#logger?.warn("signl notifier not configured");
22
+ }
23
+ }
24
+ registerRecipient(address, options) {
25
+ const notifier = createNotifier(options, this.#logger);
26
+ if (notifier) {
27
+ this.#notifiers.upsert(address, notifier);
28
+ }
29
+ }
30
+ signl(message) {
31
+ this.#signlNotifier?.alert(message);
32
+ }
33
+ alert(msg) {
34
+ this.#notify(msg, "alert");
35
+ }
36
+ notify(msg) {
37
+ this.#notify(msg, "notification");
38
+ }
39
+ #notify(msg, severity) {
40
+ const message = this.#toNotification(msg);
41
+ for (const [addr, notifier] of this.#notifiers.entries()) {
42
+ const recipient = addr === ADDRESS_0X0 ? undefined : addr;
43
+ const recipientNotification = message.messageFor(recipient);
44
+ if (recipientNotification) {
45
+ notifier.notify(this.#toDedupableNotification(recipientNotification), severity);
46
+ }
47
+ }
48
+ }
49
+ #toNotification(message) {
50
+ // global notification without deduplication
51
+ if (typeof message === "string" || !("messageFor" in message)) {
52
+ return {
53
+ messageFor: (recipient) => (recipient ? undefined : message),
54
+ };
55
+ }
56
+ return message;
57
+ }
58
+ #toDedupableNotification(msg) {
59
+ if (typeof msg === "string") {
60
+ return {
61
+ plain: msg,
62
+ };
63
+ }
64
+ return msg;
65
+ }
66
+ }
@@ -0,0 +1,12 @@
1
+ import type { ILogger } from "@gearbox-protocol/sdk";
2
+ export interface SignlServiceOptions {
3
+ apiKey: string;
4
+ teamId: string;
5
+ retryInterval?: number;
6
+ retryCount?: number;
7
+ }
8
+ export default class SignlNotifier {
9
+ #private;
10
+ constructor(opts: SignlServiceOptions, logger?: ILogger);
11
+ alert(message: string): void;
12
+ }
@@ -0,0 +1,47 @@
1
+ import { withRetry } from "viem";
2
+ const DEFAULT_RETRY_INTERVAL = 1000;
3
+ const DEFAULT_RETRY_COUNT = 5;
4
+ export default class SignlNotifier {
5
+ #logger;
6
+ #apiKey;
7
+ #teamId;
8
+ #retryInterval;
9
+ #retryCount;
10
+ constructor(opts, logger) {
11
+ this.#apiKey = opts.apiKey;
12
+ this.#teamId = opts.teamId;
13
+ this.#retryInterval = opts.retryInterval ?? DEFAULT_RETRY_INTERVAL;
14
+ this.#retryCount = opts.retryCount ?? DEFAULT_RETRY_COUNT;
15
+ this.#logger = logger;
16
+ }
17
+ alert(message) {
18
+ void this.#sendAlert(message).catch(e => this.#logger?.error(e));
19
+ }
20
+ async #sendAlert(message) {
21
+ const text = message.slice(0, 64); // it's to be shown in mobile notifications anyways
22
+ this.#logger?.debug("sending signl alert");
23
+ await withRetry(async () => {
24
+ const resp = await fetch(`https://connect.signl4.com/api/v2/alerts?x-s4-api-key=${this.#apiKey}`, {
25
+ method: "POST",
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ },
29
+ body: JSON.stringify({
30
+ category: "emergency",
31
+ severity: 0,
32
+ teamId: this.#teamId,
33
+ text,
34
+ title: "GEARBOX ALERT",
35
+ }),
36
+ });
37
+ if (resp.ok) {
38
+ this.#logger?.warn("signl alert sent");
39
+ const data = await resp.json();
40
+ this.#logger?.info(`signl alert response: ${JSON.stringify(data)}`);
41
+ }
42
+ else {
43
+ throw new Error(`signl api error ${resp.status}: ${resp.statusText}`);
44
+ }
45
+ }, { delay: this.#retryInterval, retryCount: this.#retryCount });
46
+ }
47
+ }
@@ -0,0 +1,8 @@
1
+ import type { ILogger } from "@gearbox-protocol/sdk";
2
+ import type { SlackConfig } from "./schema.js";
3
+ import type { IDedupableNotification, INotifier, NotificationSeverity } from "./types.js";
4
+ export declare class SlackNotifier implements INotifier {
5
+ #private;
6
+ constructor(config: SlackConfig, logger?: ILogger);
7
+ notify(message: IDedupableNotification, severity: NotificationSeverity): void;
8
+ }
@@ -0,0 +1,38 @@
1
+ // TODO: no throttling or deduplication yet
2
+ export class SlackNotifier {
3
+ #config;
4
+ #logger;
5
+ constructor(config, logger) {
6
+ this.#config = config;
7
+ this.#logger = logger;
8
+ }
9
+ notify(message, severity) {
10
+ if (this.#config.alertsOnly && severity === "notification") {
11
+ return;
12
+ }
13
+ const emoji = severity === "alert" ? "🚨" : "ℹ️";
14
+ const messageText = typeof message === "string" ? message : message.plain;
15
+ void this.#sendToSlack(`${emoji} Alert: ${messageText}`);
16
+ }
17
+ async #sendToSlack(text) {
18
+ try {
19
+ this.#logger?.debug(`Sending message to Slack: ${text}`);
20
+ // Use fetch instead of axios to avoid adding dependencies
21
+ const response = await fetch(this.#config.webhookUrl.value, {
22
+ method: "POST",
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ },
26
+ body: JSON.stringify({ text }),
27
+ });
28
+ if (!response.ok) {
29
+ throw new Error(`HTTP error! status: ${response.status}`);
30
+ }
31
+ this.#logger?.debug("Slack message sent successfully");
32
+ }
33
+ catch (error) {
34
+ this.#logger?.error("Error sending Slack message:", error);
35
+ throw error;
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,14 @@
1
+ import type { ILogger } from "@gearbox-protocol/sdk";
2
+ import type { TelegramConfig } from "./schema.js";
3
+ import type { IDedupableNotification, INotifier, NotificationSeverity } from "./types.js";
4
+ export type TelegramNotifierOptions = TelegramConfig & {
5
+ retryInterval?: number;
6
+ retryCount?: number;
7
+ maxChunkSize?: number;
8
+ fetchFn?: typeof fetch;
9
+ };
10
+ export declare class TelegramNotifier implements INotifier {
11
+ #private;
12
+ constructor(opts: TelegramNotifierOptions, logger?: ILogger);
13
+ notify(message: IDedupableNotification, severity: NotificationSeverity): void;
14
+ }
@@ -0,0 +1,119 @@
1
+ import { md } from "@vlad-yakovlev/telegram-md";
2
+ import Queue from "p-queue";
3
+ import { withRetry } from "viem";
4
+ import { ThrottleManager } from "./ThrottleManager.js";
5
+ const DEFAULT_RETRY_INTERVAL = 1000;
6
+ const DEFAULT_RETRY_COUNT = 5;
7
+ const DEFAULT_MAX_CHUNK_SIZE = 4096;
8
+ export class TelegramNotifier {
9
+ #logger;
10
+ #alertsChannel;
11
+ #notificationsChannel;
12
+ #botToken;
13
+ #prefix;
14
+ #retryInterval;
15
+ #retryCount;
16
+ #maxChunkSize;
17
+ #fetchFn;
18
+ #queue = new Queue({
19
+ concurrency: 1,
20
+ // https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this
21
+ // 20 messages/minute limit
22
+ interval: 60_000,
23
+ intervalCap: 20,
24
+ strict: true,
25
+ });
26
+ #throttleManager = new ThrottleManager();
27
+ constructor(opts, logger) {
28
+ this.#logger = logger;
29
+ this.#alertsChannel = opts.alertsChannel.value;
30
+ this.#botToken = opts.token.value;
31
+ this.#notificationsChannel = opts.notificationsChannel?.value;
32
+ this.#prefix = opts.prefix;
33
+ this.#retryInterval = opts.retryInterval ?? DEFAULT_RETRY_INTERVAL;
34
+ this.#retryCount = opts.retryCount ?? DEFAULT_RETRY_COUNT;
35
+ this.#maxChunkSize = opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
36
+ this.#fetchFn = opts.fetchFn ?? fetch;
37
+ }
38
+ notify(message, severity) {
39
+ const dest = severity === "alert" ? this.#alertsChannel : this.#notificationsChannel;
40
+ if (!dest) {
41
+ return;
42
+ }
43
+ this.#enqueueTelegram(message, dest, severity === "alert");
44
+ }
45
+ async #enqueueTelegram(message, channelId, severe = false) {
46
+ if (!this.#throttleManager.canSend(message.dedupeKey)) {
47
+ return;
48
+ }
49
+ const payloads = this.#toTelegramPayloads(message);
50
+ for (const payload of payloads) {
51
+ this.#queue.add(() => this.#sendToTelegram(payload, channelId, severe).catch(e => this.#logger?.error(e)), {
52
+ priority: severe ? 100 : 0,
53
+ timeout: this.#retryInterval * this.#retryCount * 2,
54
+ });
55
+ }
56
+ this.#throttleManager.onSent(message.dedupeKey, message.throttleFn);
57
+ }
58
+ async #sendToTelegram(payload, channelId, severe = false) {
59
+ const severity = severe ? "alert" : "notification";
60
+ this.#logger?.debug(`sending telegram ${severity} to channel ${channelId}...`);
61
+ const url = `https://api.telegram.org/bot${this.#botToken}/sendMessage`;
62
+ try {
63
+ await withRetry(async () => {
64
+ const resp = await this.#fetchFn(url, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ },
69
+ body: JSON.stringify({
70
+ ...payload,
71
+ chat_id: channelId,
72
+ }),
73
+ });
74
+ if (resp.ok) {
75
+ this.#logger?.debug(`telegram ${severity} sent successfully`);
76
+ }
77
+ else {
78
+ const txt = await resp.text();
79
+ throw new Error(`api error ${resp.status}: ${txt}`);
80
+ }
81
+ }, { delay: this.#retryInterval, retryCount: this.#retryCount });
82
+ }
83
+ catch (e) {
84
+ this.#logger?.error(`cannot send telegram ${severity}: ${e}`);
85
+ }
86
+ }
87
+ #toTelegramPayloads(message) {
88
+ const { md: mdMessage, plain } = message;
89
+ if (!mdMessage) {
90
+ return this.#slicePlainText(plain);
91
+ }
92
+ let text = mdMessage;
93
+ if (this.#prefix) {
94
+ text = md.join([md.bold(this.#prefix), text], " ");
95
+ }
96
+ const mdText = text.toString();
97
+ if (mdText.length > this.#maxChunkSize) {
98
+ return this.#slicePlainText(plain);
99
+ }
100
+ return [{ text: mdText, parse_mode: "MarkdownV2" }];
101
+ }
102
+ /**
103
+ * Telegram has a limit on message size (default 4096 characters).
104
+ * This function slices the text into chunks respecting the limit.
105
+ * Each chunk is prefixed with the prefix (if set), ensuring the total length <= maxChunkSize.
106
+ * @param text - The text to slice.
107
+ * @returns An array of texts.
108
+ */
109
+ #slicePlainText(text) {
110
+ const chunks = [];
111
+ const prefix = this.#prefix ? `${this.#prefix} ` : "";
112
+ const maxChunkSize = this.#maxChunkSize - prefix.length;
113
+ for (let i = 0; i < text.length; i += maxChunkSize) {
114
+ const chunkText = text.slice(i, i + maxChunkSize);
115
+ chunks.push({ text: `${prefix}${chunkText}` });
116
+ }
117
+ return chunks;
118
+ }
119
+ }
@@ -0,0 +1,18 @@
1
+ import type { ThrottleFn } from "./types.js";
2
+ export declare function defaultBackoffFn(prevWait: number): number;
3
+ /**
4
+ * Manages per-feed throttling with exponential backoff.
5
+ * Each dedupe key has its own independent throttle timer.
6
+ */
7
+ export declare class ThrottleManager {
8
+ #private;
9
+ /**
10
+ * Checks if it's permitted to send a message for the given dedupe key
11
+ * @returns
12
+ */
13
+ canSend(dedupeKey?: string): boolean;
14
+ /**
15
+ * Records that notification was sent for the given dedupe key
16
+ */
17
+ onSent(dedupeKey?: string, throttleFn?: ThrottleFn): void;
18
+ }
@@ -0,0 +1,40 @@
1
+ import { LRUCache } from "lru-cache";
2
+ export function defaultBackoffFn(prevWait) {
3
+ return Math.min(prevWait ? prevWait * 2 : 10 * 60 * 1000, // 10 minutes
4
+ 24 * 60 * 60 * 1000);
5
+ }
6
+ /**
7
+ * Manages per-feed throttling with exponential backoff.
8
+ * Each dedupe key has its own independent throttle timer.
9
+ */
10
+ export class ThrottleManager {
11
+ #state = new LRUCache({ max: 5000 });
12
+ /**
13
+ * Checks if it's permitted to send a message for the given dedupe key
14
+ * @returns
15
+ */
16
+ canSend(dedupeKey) {
17
+ if (!dedupeKey) {
18
+ return true;
19
+ }
20
+ const state = this.#getState(dedupeKey);
21
+ const timeSinceLastAlert = Date.now() - state.lastAlertTime;
22
+ return timeSinceLastAlert >= state.currentWait;
23
+ }
24
+ /**
25
+ * Records that notification was sent for the given dedupe key
26
+ */
27
+ onSent(dedupeKey, throttleFn = defaultBackoffFn) {
28
+ if (!dedupeKey) {
29
+ return;
30
+ }
31
+ const state = this.#getState(dedupeKey);
32
+ this.#state.set(dedupeKey, {
33
+ lastAlertTime: Date.now(),
34
+ currentWait: throttleFn(state.currentWait),
35
+ });
36
+ }
37
+ #getState(dedupeKey) {
38
+ return this.#state.get(dedupeKey) ?? { lastAlertTime: 0, currentWait: 0 };
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ import type { ILogger } from "@gearbox-protocol/sdk";
2
+ import { NotificationConfig } from "./schema.js";
3
+ import type { INotifier } from "./types.js";
4
+ export declare function createNotifier(config: NotificationConfig, logger?: ILogger): INotifier | undefined;
@@ -0,0 +1,18 @@
1
+ import { prettifyError } from "zod";
2
+ import { SlackNotifier } from "./SlackNotifier.js";
3
+ import { NotificationConfig } from "./schema.js";
4
+ import { TelegramNotifier } from "./TelegramNotifier.js";
5
+ export function createNotifier(config, logger) {
6
+ const parsed = NotificationConfig.safeParse(config);
7
+ if (!parsed.success) {
8
+ console.log(">>>>", parsed.error);
9
+ logger?.warn(`invalid notification config: ${prettifyError(parsed.error)}`);
10
+ return undefined;
11
+ }
12
+ switch (parsed.data.type) {
13
+ case "telegram":
14
+ return new TelegramNotifier(parsed.data, logger);
15
+ case "slack":
16
+ return new SlackNotifier(parsed.data, logger);
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./createNotifier.js";
2
+ export * from "./NotificationsService.js";
3
+ export * from "./schema.js";
4
+ export * from "./types.js";
@@ -0,0 +1,4 @@
1
+ export * from "./createNotifier.js";
2
+ export * from "./NotificationsService.js";
3
+ export * from "./schema.js";
4
+ export * from "./types.js";
@@ -0,0 +1,28 @@
1
+ import { z } from "zod/v4";
2
+ import { CensoredString } from "../CensoredString.js";
3
+ export declare const TelegramConfig: z.ZodObject<{
4
+ type: z.ZodLiteral<"telegram">;
5
+ token: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
6
+ alertsChannel: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
7
+ notificationsChannel: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>>;
8
+ prefix: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type TelegramConfig = z.infer<typeof TelegramConfig>;
11
+ export declare const SlackConfig: z.ZodObject<{
12
+ type: z.ZodLiteral<"slack">;
13
+ webhookUrl: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
14
+ alertsOnly: z.ZodOptional<z.ZodPipe<z.ZodAny, z.ZodTransform<boolean, any>>>;
15
+ }, z.core.$strip>;
16
+ export type SlackConfig = z.infer<typeof SlackConfig>;
17
+ export declare const NotificationConfig: z.ZodDiscriminatedUnion<[z.ZodObject<{
18
+ type: z.ZodLiteral<"telegram">;
19
+ token: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
20
+ alertsChannel: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
21
+ notificationsChannel: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>>;
22
+ prefix: z.ZodOptional<z.ZodString>;
23
+ }, z.core.$strip>, z.ZodObject<{
24
+ type: z.ZodLiteral<"slack">;
25
+ webhookUrl: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<CensoredString<string>, CensoredString<string>>]>, z.ZodTransform<CensoredString<string>, string | CensoredString<string>>>;
26
+ alertsOnly: z.ZodOptional<z.ZodPipe<z.ZodAny, z.ZodTransform<boolean, any>>>;
27
+ }, z.core.$strip>], "type">;
28
+ export type NotificationConfig = z.infer<typeof NotificationConfig>;
@@ -0,0 +1,38 @@
1
+ import { z } from "zod/v4";
2
+ import { CensoredString } from "../CensoredString.js";
3
+ import { boolLike } from "../schema-primitives.js";
4
+ export const TelegramConfig = z.object({
5
+ type: z.literal("telegram"),
6
+ token: z
7
+ .union([z.string(), z.instanceof(CensoredString)])
8
+ .transform(CensoredString.transform),
9
+ alertsChannel: z
10
+ .union([z.string().startsWith("-"), z.instanceof(CensoredString)])
11
+ .transform(CensoredString.transform),
12
+ /**
13
+ * Optional channel to send notifications to
14
+ * If not set, notifications will not be sent
15
+ */
16
+ notificationsChannel: z
17
+ .union([z.string().startsWith("-"), z.instanceof(CensoredString)])
18
+ .transform(CensoredString.transform)
19
+ .optional(),
20
+ prefix: z.string().optional(),
21
+ });
22
+ export const SlackConfig = z.object({
23
+ type: z.literal("slack"),
24
+ /**
25
+ * Slack webhook URL for sending notifications
26
+ */
27
+ webhookUrl: z
28
+ .union([z.string(), z.instanceof(CensoredString)])
29
+ .transform(CensoredString.transform),
30
+ /**
31
+ * If true, only alerts will be sent. Informational notifications are suppressed
32
+ */
33
+ alertsOnly: boolLike().optional(),
34
+ });
35
+ export const NotificationConfig = z.discriminatedUnion("type", [
36
+ TelegramConfig,
37
+ SlackConfig,
38
+ ]);
@@ -0,0 +1,88 @@
1
+ import type { Markdown } from "@vlad-yakovlev/telegram-md";
2
+ import type { Address } from "viem";
3
+ import type { NotificationConfig } from "./schema.js";
4
+ export type NotificationSeverity = "alert" | "notification";
5
+ /**
6
+ * Notification that can be deduplicated
7
+ */
8
+ export interface IDedupableNotification {
9
+ /**
10
+ * Messages with the same dedupe key will be throttled to avoid spam
11
+ * If not provided, the message will not be throttled
12
+ */
13
+ dedupeKey?: string;
14
+ /**
15
+ * Function to calculate the wait time between deduped messages
16
+ * If not provided, the default backoff will be used
17
+ */
18
+ throttleFn?: ThrottleFn;
19
+ /**
20
+ * Plaintext notification message
21
+ */
22
+ plain: string;
23
+ /**
24
+ * Optional markdown verison of notification message
25
+ */
26
+ md?: Markdown;
27
+ }
28
+ /**
29
+ * Function to calculate the wait time between deduped messages
30
+ * @param prevWaitMs The wait time of the previous message, 0 for the first message
31
+ * @returns The wait time for the next message in milliseconds
32
+ */
33
+ export type ThrottleFn = (prevWaitMs: number) => number;
34
+ /**
35
+ * Implement this interface for your notification classes
36
+ */
37
+ export interface INotification {
38
+ /**
39
+ * Function to generate the message for a recipient. Undefined means global message (gearbox notifications)
40
+ * This allows us to put the logic to fan out message to different recipients inside the class that implements INotificationMessage,
41
+ * and not to put this logic into the service that sends the notifications (you can still put it there, though)
42
+ *
43
+ * @param recipient The recipient to generate the message for, or undefined for the default/global recipient (gearbox notifications)
44
+ * @returns The message for the recipient, or undefined for no message
45
+ */
46
+ messageFor: (recipient?: Address) => string | IDedupableNotification | undefined;
47
+ }
48
+ /**
49
+ * A notification message, dumb strings and markdowns are treated as global notifications
50
+ */
51
+ export type NotificationLike = string | IDedupableNotification | INotification;
52
+ /**
53
+ * Notification service with deduplication and fan out logic
54
+ */
55
+ export interface INotificationService {
56
+ /**
57
+ * Send an alert to Signl
58
+ * @param message
59
+ * @returns
60
+ */
61
+ signl: (message: string) => void;
62
+ /**
63
+ * Sends high priority notification
64
+ * @param message
65
+ * @returns
66
+ */
67
+ alert: (message: NotificationLike) => void;
68
+ /**
69
+ * Sends low priority notification
70
+ * @param message
71
+ * @returns
72
+ */
73
+ notify: (message: NotificationLike) => void;
74
+ /**
75
+ * Register a recipient for notifications
76
+ * @param address The address of the recipient
77
+ * @param options The options for the recipient
78
+ */
79
+ registerRecipient: (address: Address, options: NotificationConfig) => void;
80
+ }
81
+ export interface INotifier {
82
+ /**
83
+ * Sends notification
84
+ * @param message
85
+ * @returns
86
+ */
87
+ notify: (message: IDedupableNotification, severity: NotificationSeverity) => void;
88
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/cli-utils",
3
3
  "description": "Utils for creating cli apps",
4
- "version": "5.67.1",
4
+ "version": "5.68.0",
5
5
  "homepage": "https://gearbox.fi",
6
6
  "keywords": [
7
7
  "gearbox"
@@ -33,9 +33,12 @@
33
33
  "@aws-sdk/client-secrets-manager": "^3.971.0",
34
34
  "@aws-sdk/client-ssm": "^3.971.0",
35
35
  "@gearbox-protocol/sdk": ">=12.5.0",
36
+ "@vlad-yakovlev/telegram-md": "^2.1.0",
36
37
  "abitype": "^1.2.3",
37
38
  "commander": "^14.0.2",
38
39
  "lodash-es": "^4.17.22",
40
+ "lru-cache": "^11.2.4",
41
+ "p-queue": "^9.1.0",
39
42
  "viem": "^2.44.4",
40
43
  "yaml": "^2.8.2",
41
44
  "zod": "^4.3.5"