Skip to content

Commit 9d0a6a2

Browse files
committed
feat: allow per-curator notifications
1 parent f588fb0 commit 9d0a6a2

30 files changed

+1273
-1116
lines changed

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@
2121
"pino-pretty": "^13.1.3"
2222
},
2323
"devDependencies": {
24-
"@aws-sdk/client-s3": "^3.971.0",
24+
"@aws-sdk/client-s3": "^3.972.0",
2525
"@biomejs/biome": "^2.3.11",
2626
"@commander-js/extra-typings": "^14.0.0",
2727
"@commitlint/cli": "^20.3.1",
2828
"@commitlint/config-conventional": "^20.3.1",
2929
"@gearbox-protocol/biome-config": "^1.0.17",
30-
"@gearbox-protocol/cli-utils": "^5.67.0",
30+
"@gearbox-protocol/cli-utils": "^5.68.2",
3131
"@gearbox-protocol/liquidator-contracts": "^1.36.0-experimental.67",
3232
"@gearbox-protocol/liquidator-v2-contracts": "^2.4.0",
33-
"@gearbox-protocol/sdk": "12.5.0",
33+
"@gearbox-protocol/sdk": "12.5.2",
3434
"@gearbox-protocol/types": "^1.14.8",
35-
"@types/node": "^25.0.9",
35+
"@types/node": "^25.0.10",
3636
"@uniswap/sdk-core": "^7.10.1",
3737
"@uniswap/v3-sdk": "^3.27.0",
3838
"@vlad-yakovlev/telegram-md": "^2.1.0",
@@ -46,7 +46,7 @@
4646
"lint-staged": "^16.2.7",
4747
"nanoid": "^5.1.6",
4848
"node-pty": "^1.1.0",
49-
"pino": "^10.2.0",
49+
"pino": "^10.2.1",
5050
"tsx": "4.21.0",
5151
"typescript": "^5.9.3",
5252
"viem": "^2.44.4",

src/config/common.ts

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { MAX_UINT256, WAD } from "@gearbox-protocol/sdk";
1111
import { type Hex, isHex } from "viem";
1212
import { z } from "zod/v4";
13+
import { NotificationsConfig } from "./notifications.js";
1314

1415
export const CommonSchema = z.object({
1516
...ProvidersSchema.shape,
@@ -399,55 +400,9 @@ export const CommonSchema = z.object({
399400
}),
400401

401402
/**
402-
* Telegram bot token used to send notifications
403+
* Notifications, global and per-curator
403404
*/
404-
telegramBotToken: z
405-
.string()
406-
.transform(CensoredString.transform)
407-
.optional()
408-
.register(zommandRegistry, {
409-
flags: "--telegram-bot-token <token>",
410-
description: "Telegram bot token used to send notifications",
411-
env: "TELEGRAM_BOT_TOKEN",
412-
}),
413-
/**
414-
* Telegram channel where bot will post critical notifications
415-
*/
416-
telegramAlertsChannel: z
417-
.string()
418-
.startsWith("-")
419-
.optional()
420-
.register(zommandRegistry, {
421-
flags: "--telegram-alerts-channel <channel>",
422-
description:
423-
"Telegram channel where bot will post critical notifications",
424-
env: "TELEGRAM_ALERTS_CHANNEL",
425-
}),
426-
/**
427-
* Telegram channel where bot will post non-critical notifications
428-
*/
429-
telegramNotificationsChannel: z
430-
.string()
431-
.startsWith("-")
432-
.optional()
433-
.register(zommandRegistry, {
434-
flags: "--telegram-notifications-channel <channel>",
435-
description:
436-
"Telegram channel where bot will post non-critical notifications",
437-
env: "TELEGRAM_NOTIFICATIONS_CHANNEL",
438-
}),
439-
/**
440-
* Notification cooldown in minutes
441-
*/
442-
notificationCooldown: z.coerce
443-
.number()
444-
.nonnegative()
445-
.default(4 * 60)
446-
.register(zommandRegistry, {
447-
flags: "--notification-cooldown <cooldown>",
448-
description: "Notification cooldown in minutes",
449-
env: "NOTIFICATION_COOLDOWN",
450-
}),
405+
...NotificationsConfig.shape,
451406
});
452407

453408
export type CommonSchema = z.infer<typeof CommonSchema>;

src/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export type * from "./common.js";
33
export * from "./config.js";
44
export type * from "./deleverage-liquidator.js";
55
export type * from "./full-liquidator.js";
6+
export { NotificationsConfig } from "./notifications.js";
67
export type * from "./partial-liquidator.js";
78
export { ConfigSchema } from "./schema.js";

src/config/notifications.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NotificationConfig } from "@gearbox-protocol/cli-utils";
2+
import type { Curator } from "@gearbox-protocol/sdk";
3+
import { z } from "zod/v4";
4+
5+
const extendedOptions = NotificationConfig.options.map(o =>
6+
o.extend({
7+
/**
8+
* When undefined, defaults to all curators together (aka Gearbox internal)
9+
*/
10+
curator: z.custom<Curator>(s => typeof s === "string").optional(),
11+
}),
12+
);
13+
14+
export const NotificationsConfig = z.object({
15+
notifications: z.array(
16+
z.discriminatedUnion(
17+
NotificationConfig.def.discriminator,
18+
extendedOptions as [
19+
(typeof extendedOptions)[0],
20+
...(typeof extendedOptions)[number][],
21+
],
22+
),
23+
),
24+
});

src/services/Client.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { nextTick } from "node:process";
2-
2+
import type { INotificationService } from "@gearbox-protocol/cli-utils";
33
import { chains, formatBN, PERCENTAGE_FACTOR } from "@gearbox-protocol/sdk";
44
import type {
55
AnvilClient,
@@ -32,15 +32,13 @@ import {
3232
WaitForTransactionReceiptTimeoutError,
3333
} from "viem";
3434
import { privateKeyToAccount } from "viem/accounts";
35-
3635
import type { Config } from "../config/index.js";
3736
import { DI } from "../di.js";
3837
import { errorAbis } from "../errors/abis.js";
3938
import { TransactionRevertedError } from "../errors/TransactionRevertedError.js";
4039
import { type ILogger, Logger } from "../log/index.js";
4140
import type { StatusCode } from "../utils/index.js";
42-
import type { INotifier } from "./notifier/index.js";
43-
import { LowBalanceMessage } from "./notifier/index.js";
41+
import { LowBalanceNotification } from "./notifier/index.js";
4442

4543
const GAS_X = 5000n;
4644

@@ -50,7 +48,7 @@ export default class Client {
5048
config!: Config;
5149

5250
@DI.Inject(DI.Notifier)
53-
notifier!: INotifier;
51+
notifier!: INotificationService;
5452

5553
@DI.Inject(DI.Transport)
5654
transport!: Transport<"revolver", RevolverTransportValue>;
@@ -270,7 +268,12 @@ export default class Client {
270268
this.logger.debug(`liquidator balance is ${formatEther(balance)}`);
271269
if (balance < this.config.minBalance) {
272270
this.notifier.alert(
273-
new LowBalanceMessage(this.address, balance, this.config.minBalance),
271+
new LowBalanceNotification(
272+
this.config.network,
273+
this.address,
274+
balance,
275+
this.config.minBalance,
276+
),
274277
);
275278
}
276279
}

src/services/Scanner.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { INotificationService } from "@gearbox-protocol/cli-utils";
12
import type {
23
CreditAccountData,
34
GetCreditAccountsOptions,
@@ -20,7 +21,7 @@ import { type ILogger, Logger } from "../log/index.js";
2021
import type Client from "./Client.js";
2122
import type DeleverageService from "./DeleverageService.js";
2223
import type { ILiquidatorService } from "./liquidate/index.js";
23-
import { type INotifier, ZeroHFAccountsMessage } from "./notifier/index.js";
24+
import { ZeroHFAccountsNotification } from "./notifier/ZeroHFAccountsNotification.js";
2425

2526
const RESTAKING_CMS: Partial<Record<NetworkType, Address>> = {
2627
Mainnet:
@@ -50,7 +51,7 @@ export class Scanner {
5051
deleverage!: DeleverageService;
5152

5253
@DI.Inject(DI.Notifier)
53-
notifier!: INotifier;
54+
notifier!: INotificationService;
5455

5556
#processing: bigint | null = null;
5657
#restakingCMAddr?: Address;
@@ -59,7 +60,6 @@ export class Scanner {
5960
#maxHealthFactor = MAX_UINT256;
6061
#minHealthFactor = 0n;
6162
#unwatch?: () => void;
62-
#lastZeroHFNotification = 0;
6363
#liquidatableAccounts = 0;
6464

6565
public async launch(): Promise<void> {
@@ -265,14 +265,13 @@ export class Scanner {
265265
}
266266
}
267267
}
268-
const badTokensStr = badTokens
269-
.asArray()
270-
.map(t => this.caService.sdk.tokensMeta.get(t)?.symbol ?? t)
271-
.join(", ");
272-
this.log.warn(
273-
`found ${zeroHFAccs.length} accounts with HF=0 and ${badTokens.size} bad tokens: ${badTokensStr}`,
268+
this.notifier.alert(
269+
new ZeroHFAccountsNotification(
270+
this.caService.sdk,
271+
zeroHFAccs.length,
272+
badTokens,
273+
),
274274
);
275-
this.#notifyOnZeroHFAccounts(zeroHFAccs.length, badTokensStr);
276275
}
277276
}
278277

@@ -424,16 +423,6 @@ export class Scanner {
424423
);
425424
}
426425

427-
#notifyOnZeroHFAccounts(count: number, badTokens: string): void {
428-
const now = Date.now();
429-
if (now - this.#lastZeroHFNotification < 1000 * 60 * 5) {
430-
return;
431-
}
432-
this.#lastZeroHFNotification = now;
433-
this.log.debug("notifying on zero HF accounts");
434-
this.notifier.alert(new ZeroHFAccountsMessage(count, badTokens));
435-
}
436-
437426
public get lastUpdated(): bigint {
438427
return this.#lastUpdated;
439428
}

src/services/liquidate/AbstractLiquidator.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { INotificationService } from "@gearbox-protocol/cli-utils";
12
import type {
23
CreditAccountData,
34
GearboxSDK,
@@ -6,14 +7,13 @@ import type {
67
import { filterDustUSD } from "@gearbox-protocol/sdk";
78
import type { OptimisticResult } from "@gearbox-protocol/types/optimist";
89
import { type Address, erc20Abi } from "viem";
9-
1010
import type { CommonSchema, LiqduiatorConfig } from "../../config/index.js";
1111
import { DI } from "../../di.js";
1212
import { ErrorHandler } from "../../errors/index.js";
1313
import type { ILogger } from "../../log/index.js";
1414
import { Logger } from "../../log/index.js";
1515
import type Client from "../Client.js";
16-
import { type INotifier, StartedMessage } from "../notifier/index.js";
16+
import { ServiceStartedNotification } from "../notifier/index.js";
1717
import type { IOptimisticOutputWriter } from "../output/index.js";
1818
import type { ISwapper } from "../swap/index.js";
1919
import AccountHelper from "./AccountHelper.js";
@@ -34,7 +34,7 @@ export default abstract class AbstractLiquidator<
3434
creditAccountService!: ICreditAccountsService;
3535

3636
@DI.Inject(DI.Notifier)
37-
notifier!: INotifier;
37+
notifier!: INotificationService;
3838

3939
@DI.Inject(DI.Config)
4040
config!: LiqduiatorConfig<TConfig>;
@@ -58,7 +58,7 @@ export default abstract class AbstractLiquidator<
5858
public async launch(asFallback?: boolean): Promise<void> {
5959
this.#errorHandler = new ErrorHandler(this.config, this.logger);
6060
if (!asFallback) {
61-
this.notifier.notify(new StartedMessage());
61+
this.notifier.notify(new ServiceStartedNotification());
6262
}
6363
}
6464

src/services/liquidate/BatchLiquidator.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import type { Address, TransactionReceipt } from "viem";
2323
import { encodeFunctionData, parseEventLogs } from "viem";
2424
import type { BatchLiquidatorSchema } from "../../config/index.js";
2525
import {
26-
BatchLiquidationErrorMessage,
27-
BatchLiquidationFinishedMessage,
28-
} from "../notifier/messages.js";
26+
BatchLiquidationErrorNotification,
27+
BatchLiquidationFinishedNotification,
28+
} from "../notifier/index.js";
2929
import AbstractLiquidator from "./AbstractLiquidator.js";
3030
import type { ILiquidatorService } from "./types.js";
3131
import type {
@@ -77,13 +77,17 @@ export default class BatchLiquidator
7777
batches.length,
7878
);
7979
this.notifier.notify(
80-
new BatchLiquidationFinishedMessage(receipt, results),
80+
new BatchLiquidationFinishedNotification(this.sdk, receipt, results),
8181
);
8282
} catch (e) {
8383
const decoded = await this.errorHandler.explain(e);
8484
this.logger.error(`cant liquidate: ${decoded.shortMessage}`);
8585
this.notifier.notify(
86-
new BatchLiquidationErrorMessage(batch, decoded.shortMessage),
86+
new BatchLiquidationErrorNotification(
87+
this.sdk,
88+
batch,
89+
decoded.shortMessage,
90+
),
8791
);
8892
}
8993
}

src/services/liquidate/SingularLiquidator.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import type {
1515
import { TransactionRevertedError } from "../../errors/index.js";
1616
import { LoggerFactory } from "../../log/index.js";
1717
import {
18-
LiquidationErrorMessage,
19-
LiquidationStartMessage,
20-
LiquidationSuccessMessage,
18+
LiquidationErrorNotification,
19+
LiquidationStartNotification,
20+
LiquidationSuccessNotification,
2121
} from "../notifier/index.js";
2222
import AbstractLiquidator, {
2323
type ExecutorBalance,
@@ -103,8 +103,6 @@ export default class SingularLiquidator
103103
LoggerFactory.setLogContext({ account: ca.creditAccount });
104104
await this.#liquidateOne(ca);
105105
LoggerFactory.clearLogContext();
106-
// success or no, silence the notifier for this account for a while
107-
this.notifier.setCooldown(ca.creditAccount.toLowerCase());
108106
}
109107
}
110108

@@ -152,7 +150,7 @@ export default class SingularLiquidator
152150
let pathHuman: string[] | undefined;
153151
let skipOnFailure = false;
154152

155-
this.notifier.notify(new LiquidationStartMessage(ca));
153+
this.notifier.notify(new LiquidationStartNotification(this.sdk, ca));
156154

157155
for (const s of this.#strategies) {
158156
if (!s.isApplicable(ca)) {
@@ -171,7 +169,13 @@ export default class SingularLiquidator
171169
const receipt = await this.client.liquidate(request);
172170
if (receipt.status === "success") {
173171
this.notifier.alert(
174-
new LiquidationSuccessMessage(ca, s.name, receipt, pathHuman),
172+
new LiquidationSuccessNotification(
173+
this.sdk,
174+
ca,
175+
receipt,
176+
s.name,
177+
pathHuman,
178+
),
175179
);
176180
return;
177181
} else {
@@ -183,7 +187,8 @@ export default class SingularLiquidator
183187
`cant liquidate with ${s.name}: ${decoded.shortMessage}`,
184188
);
185189
this.notifier.alert(
186-
new LiquidationErrorMessage(
190+
new LiquidationErrorNotification(
191+
this.sdk,
187192
ca,
188193
s.name,
189194
decoded.shortMessage,

0 commit comments

Comments
 (0)