diff --git a/README.md b/README.md index a134e58..135ab52 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ For auctioneers that were started before multi-pool functionality a db migration | `priceSources` | (Optional) A list of assets that will have prices sourced from exchanges instead of the pool oracle. | | `profits` | (Optional) A list of auction profits to define different profit percentages used for matching auctions. | `slackWebhook` | (Optional) A slack webhook URL to post updates to (https://hooks.slack.com/services/). Leave undefined if no webhooks are required. | +| `discordWebhook` | (Optional) A Discord webhook URL to post updates to. Leave undefined if no webhooks are required. | + #### Fillers diff --git a/src/bidder_handler.ts b/src/bidder_handler.ts index 166ec34..211c95a 100644 --- a/src/bidder_handler.ts +++ b/src/bidder_handler.ts @@ -10,7 +10,7 @@ import { APP_CONFIG } from './utils/config.js'; import { AuctioneerDatabase, AuctionType } from './utils/db.js'; import { stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; export class BidderHandler { @@ -104,7 +104,7 @@ export class BidderHandler { `Fill: ${stringify(fill, 2)}\n` + `Ledgers To Fill In: ${fill.block - nextLedger}\n`; if (auctionEntry.fill_block === 0) { - await sendSlackNotification(logMessage); + await sendNotification(logMessage); } logger.info(logMessage); auctionEntry.fill_block = fill.block; diff --git a/src/bidder_submitter.ts b/src/bidder_submitter.ts index 9128778..dca2e79 100644 --- a/src/bidder_submitter.ts +++ b/src/bidder_submitter.ts @@ -6,7 +6,7 @@ import { APP_CONFIG, Filler } from './utils/config.js'; import { AuctioneerDatabase, AuctionEntry, AuctionType } from './utils/db.js'; import { serializeError, stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { SubmissionQueue } from './utils/submission_queue.js'; @@ -168,16 +168,16 @@ export class BidderSubmitter extends SubmissionQueue { `Fill Percent ${fill.percent}\n` + `Ledger Fill Delta ${result.ledger - auctionBid.auctionEntry.start_block}\n` + `Hash ${result.txHash}\n`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); return true; } else { logger.info( `Fill ledger not reached for auction bid\n` + - `Type: ${auctionBid.auctionEntry.auction_type}\n` + - `Pool: ${auctionBid.auctionEntry.pool_id}\n` + - `User: ${auctionBid.auctionEntry.user_id}\n` + - `Fill Ledger: ${fill.block} Next Ledger: ${nextLedger}` + `Type: ${auctionBid.auctionEntry.auction_type}\n` + + `Pool: ${auctionBid.auctionEntry.pool_id}\n` + + `User: ${auctionBid.auctionEntry.user_id}\n` + + `Fill Ledger: ${fill.block} Next Ledger: ${nextLedger}` ); } // allow bidder handler to re-process the auction entry @@ -190,7 +190,7 @@ export class BidderSubmitter extends SubmissionQueue { `User: ${auctionBid.auctionEntry.user_id}\n` + `Filler: ${auctionBid.filler.name}\n` + `Error: ${stringify(serializeError(e))}`; - await sendSlackNotification(` ` + logMessage); + await sendNotification(logMessage, true); logger.error(logMessage, e); return false; } @@ -257,9 +257,9 @@ export class BidderSubmitter extends SubmissionQueue { ); logger.info( `Successful unwind for filler: ${fillerUnwind.filler.name}\n` + - `Pool: ${fillerUnwind.poolId}\n` + - `Ledger: ${result.ledger}\n` + - `Hash: ${result.txHash}` + `Pool: ${fillerUnwind.poolId}\n` + + `Ledger: ${result.ledger}\n` + + `Hash: ${result.txHash}` ); this.addSubmission( { @@ -283,7 +283,7 @@ export class BidderSubmitter extends SubmissionQueue { `Filler: ${fillerUnwind.filler.name}\n` + `Backstop Token Balance: ${tokenBalanceFloat}`; logger.info(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); } } @@ -295,7 +295,7 @@ export class BidderSubmitter extends SubmissionQueue { `Pool: ${fillerUnwind.poolId}\n` + `Positions: ${stringify(filler_user.positions, 2)}`; logger.info(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); return true; } @@ -373,6 +373,6 @@ export class BidderSubmitter extends SubmissionQueue { break; } logger.error(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); } } diff --git a/src/liquidations.ts b/src/liquidations.ts index 89fc17b..35e9e06 100644 --- a/src/liquidations.ts +++ b/src/liquidations.ts @@ -5,7 +5,7 @@ import { AuctioneerDatabase, AuctionType } from './utils/db.js'; import { logger } from './utils/logger.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { WorkSubmission, WorkSubmissionType } from './work_submitter.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { stringify } from './utils/json.js'; /** @@ -324,7 +324,7 @@ export async function checkUsersForLiquidationsAndBadDebt( `User: ${user}\n` + `Error: ${e}`; logger.error(errorLog); - sendSlackNotification(errorLog); + sendNotification(errorLog); } } return submissions; diff --git a/src/pool_event_handler.ts b/src/pool_event_handler.ts index 5324837..1c8d28d 100644 --- a/src/pool_event_handler.ts +++ b/src/pool_event_handler.ts @@ -8,7 +8,7 @@ import { AuctioneerDatabase, AuctionEntry, AuctionType } from './utils/db.js'; import { stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; import { deadletterEvent, sendEvent } from './utils/messages.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { WorkSubmission } from './work_submitter.js'; const MAX_RETRIES = 2; @@ -113,7 +113,7 @@ export class PoolEventHandler { `Pool: ${poolId}\n` + `User: ${poolEvent.event.user}\n` + `Auction Data: ${stringify(poolEvent.event.auctionData, 2)}\n`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); fillerFound = true; break; @@ -125,7 +125,7 @@ export class PoolEventHandler { `Pool: ${poolId}\n` + `User: ${poolEvent.event.user}\n` + `Auction Data: ${stringify(poolEvent.event.auctionData, 2)}\n`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); } break; @@ -142,7 +142,7 @@ export class PoolEventHandler { `Liquidation Auction Deleted\n` + `Pool: ${poolId}\n` + `User: ${poolEvent.event.user}\n`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); } break; @@ -157,7 +157,7 @@ export class PoolEventHandler { `User: ${poolEvent.event.user}\n` + `Fill Percent: ${poolEvent.event.fillAmount}\n` + `Tx Hash: ${poolEvent.event.txHash}\n`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); if (poolEvent.event.fillAmount === BigInt(100)) { // auction was fully filled, remove from ongoing auctions @@ -219,7 +219,7 @@ export class PoolEventHandler { `Type: ${AuctionType[auctionType]}\n` + `Pool: ${poolId}\n` + `User: ${user}`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); } } diff --git a/src/utils/config.ts b/src/utils/config.ts index d5523b0..4e46375 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -65,6 +65,7 @@ export interface AppConfig { priceSources: PriceSource[] | undefined; profits: AuctionProfit[] | undefined; slackWebhook: string | undefined; + discordWebhook: string | undefined; highBaseFee: number | undefined; baseFee: number | undefined; } @@ -99,6 +100,7 @@ export function validateAppConfig(config: any): boolean { (config.priceSources !== undefined && !Array.isArray(config.priceSources)) || (config.profits !== undefined && !Array.isArray(config.profits)) || (config.slackWebhook !== undefined && typeof config.slackWebhook !== 'string') || + (config.discordWebhook !== undefined && typeof config.discordWebhook !== 'string') || (config.highBaseFee !== undefined && typeof config.highBaseFee !== 'number') || (config.baseFee !== undefined && typeof config.baseFee !== 'number') ) { diff --git a/src/utils/notifier.ts b/src/utils/notifier.ts new file mode 100644 index 0000000..3cfecac --- /dev/null +++ b/src/utils/notifier.ts @@ -0,0 +1,73 @@ +import { APP_CONFIG } from './config.js'; +import { logger } from './logger.js'; + +async function sendSlackNotification(message: string, tag: boolean = false): Promise { + try { + if (APP_CONFIG.slackWebhook) { + const taggedMessage = tag ? ` ${message}` : message; + const response = await fetch(APP_CONFIG.slackWebhook, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text: `*Bot Name*: ${APP_CONFIG.name}\n${taggedMessage}`, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } + } catch (e) { + logger.error(`Error sending Slack notification: ${e}`); + } +} + +async function sendDiscordNotification(message: string, tag: boolean = false): Promise { + try { + if (APP_CONFIG.discordWebhook) { + const taggedMessage = tag ? `@everyone ${message}` : message; + const response = await fetch(APP_CONFIG.discordWebhook, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: `**${APP_CONFIG.name}**\n${taggedMessage}`, + }), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + } + } catch (e) { + logger.error(`Error sending Discord notification: ${e}`); + } +} + +export async function sendNotification(message: string, tag: boolean = false): Promise { + // If no webhooks are configured, log to console as fallback + if (!APP_CONFIG.slackWebhook && !APP_CONFIG.discordWebhook) { + console.log( + `Bot Name: ${APP_CONFIG.name}\nTimestamp: ${new Date().toISOString()}\n${message}` + ); + return; + } + + // Send to both platforms in parallel if configured + const notifications = []; + + if (APP_CONFIG.slackWebhook) { + notifications.push(sendSlackNotification(message, tag)); + } + + if (APP_CONFIG.discordWebhook) { + notifications.push(sendDiscordNotification(message, tag)); + } + + try { + await Promise.all(notifications); + } catch (error) { + logger.error(`Error sending notifications: ${error}`); + } +} \ No newline at end of file diff --git a/src/utils/slack_notifier.ts b/src/utils/slack_notifier.ts deleted file mode 100644 index 6920633..0000000 --- a/src/utils/slack_notifier.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { APP_CONFIG } from './config.js'; -import { logger } from './logger.js'; - -export async function sendSlackNotification(message: string): Promise { - try { - if (APP_CONFIG.slackWebhook) { - const response = await fetch(APP_CONFIG.slackWebhook, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - text: `*Bot Name*: ${APP_CONFIG.name}\n${message}`, - }), - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - } else { - console.log( - `Bot Name: ${APP_CONFIG.name}\nTimestamp: ${new Date().toISOString()}\n${message}` - ); - } - } catch (e) { - logger.error(`Error sending slack notification: ${e}`); - } -} diff --git a/src/work_handler.ts b/src/work_handler.ts index 0ce4997..bc39a58 100644 --- a/src/work_handler.ts +++ b/src/work_handler.ts @@ -8,7 +8,7 @@ import { AuctioneerDatabase, AuctionType } from './utils/db.js'; import { logger } from './utils/logger.js'; import { deadletterEvent } from './utils/messages.js'; import { setPrices } from './utils/prices.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { WorkSubmissionType, WorkSubmitter } from './work_submitter.js'; import { canFillerBid, checkFillerSupport, getFillerAvailableBalances } from './filler.js'; @@ -154,7 +154,7 @@ export class WorkHandler { `Pool: ${poolId}\n` + `User: ${user.user_id}`; logger.error(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); } const { estimate: poolUserEstimate, user: poolUser } = diff --git a/src/work_submitter.ts b/src/work_submitter.ts index 235808e..2d19d49 100644 --- a/src/work_submitter.ts +++ b/src/work_submitter.ts @@ -3,7 +3,7 @@ import { APP_CONFIG, Filler } from './utils/config.js'; import { AuctionType } from './utils/db.js'; import { serializeError, stringify } from './utils/json.js'; import { logger } from './utils/logger.js'; -import { sendSlackNotification } from './utils/slack_notifier.js'; +import { sendNotification } from './utils/notifier.js'; import { SorobanHelper } from './utils/soroban_helper.js'; import { SubmissionQueue } from './utils/submission_queue.js'; import { Address, Contract, nativeToScVal } from '@stellar/stellar-sdk'; @@ -90,7 +90,7 @@ export class WorkSubmitter extends SubmissionQueue { `Lot: ${stringify(auction.lot)}\n`; logger.info(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); return true; } catch (e: any) { const logMessage = @@ -103,7 +103,7 @@ export class WorkSubmitter extends SubmissionQueue { `Lot: ${stringify(auction.lot)}\n` + `Error: ${stringify(serializeError(e))}\n`; logger.error(logMessage); - await sendSlackNotification(`\n` + logMessage); + await sendNotification(logMessage, true); // if pool throws a "LIQ_TOO_SMALL" or "LIQ_TOO_LARGE" error, adjust the fill percentage // by 1 percentage point before retrying. @@ -133,7 +133,7 @@ export class WorkSubmitter extends SubmissionQueue { `Successfully transferred bad debt to backstop\n` + `Pool: ${badDebtTransfer.poolId}\n` + `User: ${badDebtTransfer.user}`; - await sendSlackNotification(logMessage); + await sendNotification(logMessage); logger.info(logMessage); return true; } catch (e: any) { @@ -143,7 +143,7 @@ export class WorkSubmitter extends SubmissionQueue { `User: ${badDebtTransfer.user}` + `Error: ${stringify(serializeError(e))}\n`; logger.error(logMessage); - await sendSlackNotification(` ` + logMessage); + await sendNotification(logMessage, true); return false; } } @@ -167,6 +167,6 @@ export class WorkSubmitter extends SubmissionQueue { break; } logger.error(logMessage); - await sendSlackNotification(logMessage); + await sendNotification(logMessage); } } diff --git a/test/bidder_handler.test.ts b/test/bidder_handler.test.ts index d912d28..5ea33ee 100644 --- a/test/bidder_handler.test.ts +++ b/test/bidder_handler.test.ts @@ -12,7 +12,7 @@ import { AppEvent, EventType, LedgerEvent } from '../src/events'; import { APP_CONFIG, AppConfig } from '../src/utils/config'; import { AuctioneerDatabase, AuctionEntry, AuctionType } from '../src/utils/db'; import { logger } from '../src/utils/logger'; -import { sendSlackNotification } from '../src/utils/slack_notifier'; +import { sendNotification } from '../src/utils/notifier'; import { SorobanHelper } from '../src/utils/soroban_helper'; import { inMemoryAuctioneerDb } from './helpers/mocks'; @@ -84,7 +84,7 @@ jest.mock('../src/liquidations'); jest.mock('../src/utils/soroban_helper'); jest.mock('../src/bidder_submitter'); jest.mock('../src/auction'); -jest.mock('../src/utils/slack_notifier'); +jest.mock('../src/utils/notifier'); describe('BidderHandler', () => { let bidderHandler: BidderHandler; @@ -95,8 +95,8 @@ describe('BidderHandler', () => { let mockedCalcAuctionFill = calculateAuctionFill as jest.MockedFunction< typeof calculateAuctionFill >; - let mockedSendSlackNotif = sendSlackNotification as jest.MockedFunction< - typeof sendSlackNotification + let mockedSendSlackNotif = sendNotification as jest.MockedFunction< + typeof sendNotification >; beforeEach(() => { diff --git a/test/bidder_submitter.test.ts b/test/bidder_submitter.test.ts index 33511f5..ab1f230 100644 --- a/test/bidder_submitter.test.ts +++ b/test/bidder_submitter.test.ts @@ -19,7 +19,7 @@ import { getFillerAvailableBalances, managePositions } from '../src/filler'; import { APP_CONFIG, Filler } from '../src/utils/config'; import { AuctioneerDatabase, AuctionEntry, AuctionType, FilledAuctionEntry } from '../src/utils/db'; import { logger } from '../src/utils/logger'; -import { sendSlackNotification } from '../src/utils/slack_notifier'; +import { sendNotification } from '../src/utils/notifier'; import { SorobanHelper } from '../src/utils/soroban_helper'; import { inMemoryAuctioneerDb, mockPool, mockPoolOracle } from './helpers/mocks'; import { stringify } from '../src/utils/json'; @@ -28,7 +28,7 @@ import { stringify } from '../src/utils/json'; jest.mock('../src/utils/db'); jest.mock('../src/utils/soroban_helper'); jest.mock('../src/auction'); -jest.mock('../src/utils/slack_notifier'); +jest.mock('../src/utils/notifier'); jest.mock('../src/filler'); jest.mock('../src/utils/soroban_helper'); jest.mock('@stellar/stellar-sdk', () => { @@ -79,8 +79,8 @@ describe('BidderSubmitter', () => { }; mockedSorobanHelperConstructor.mockReturnValue(mockedSorobanHelper); - const mockedSendSlackNotif = sendSlackNotification as jest.MockedFunction< - typeof sendSlackNotification + const mockedSendSlackNotif = sendNotification as jest.MockedFunction< + typeof sendNotification >; const mockedCalcAuctionFill = calculateAuctionFill as jest.MockedFunction< typeof calculateAuctionFill diff --git a/test/pool_event_handler.test.ts b/test/pool_event_handler.test.ts index 014e470..3249dc2 100644 --- a/test/pool_event_handler.test.ts +++ b/test/pool_event_handler.test.ts @@ -20,7 +20,7 @@ import { inMemoryAuctioneerDb, mockPool } from './helpers/mocks.js'; jest.mock('../src/user.js'); jest.mock('../src/utils/soroban_helper.js'); -jest.mock('../src/utils/slack_notifier.js'); +jest.mock('../src/utils/notifier'); jest.mock('../src/utils/messages'); jest.mock('../src/utils/logger.js', () => ({ logger: { diff --git a/test/work_submitter.test.ts b/test/work_submitter.test.ts index 84643b6..f9d8899 100644 --- a/test/work_submitter.test.ts +++ b/test/work_submitter.test.ts @@ -3,7 +3,7 @@ import { Keypair } from '@stellar/stellar-sdk'; import { AppConfig } from '../src/utils/config'; import { AuctionType } from '../src/utils/db'; import { logger } from '../src/utils/logger'; -import { sendSlackNotification } from '../src/utils/slack_notifier'; +import { sendNotification } from '../src/utils/notifier'; import { SorobanHelper } from '../src/utils/soroban_helper'; import { WorkSubmission, WorkSubmissionType, WorkSubmitter } from '../src/work_submitter'; import { mockPool } from './helpers/mocks'; @@ -12,7 +12,7 @@ import { serializeError, stringify } from '../src/utils/json'; // Mock dependencies jest.mock('../src/utils/db'); jest.mock('../src/utils/soroban_helper'); -jest.mock('../src/utils/slack_notifier'); +jest.mock('../src/utils/notifier'); jest.mock('../src/utils/logger'); jest.mock('../src/utils/logger.js', () => ({ logger: { @@ -33,8 +33,8 @@ describe('WorkSubmitter', () => { let mockedSorobanHelper = new SorobanHelper() as jest.Mocked; let mockedSorobanHelperConstructor = SorobanHelper as jest.MockedClass; - const mockedSendSlackNotif = sendSlackNotification as jest.MockedFunction< - typeof sendSlackNotification + const mockedSendSlackNotif = sendNotification as jest.MockedFunction< + typeof sendNotification >; beforeEach(() => { @@ -256,7 +256,7 @@ describe('WorkSubmitter', () => { expect(result).toBe(true); expect(mockedSorobanHelper.submitTransaction).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalled(); - expect(sendSlackNotification).toHaveBeenCalled(); + expect(sendNotification).toHaveBeenCalled(); }); it('should submit a bad debt auction successfully', async () => { @@ -276,7 +276,7 @@ describe('WorkSubmitter', () => { expect(result).toBe(true); expect(mockedSorobanHelper.submitTransaction).toHaveBeenCalled(); expect(logger.info).toHaveBeenCalled(); - expect(sendSlackNotification).toHaveBeenCalled(); + expect(sendNotification).toHaveBeenCalled(); }); it('should not submit if auction already exists', async () => {