Skip to content

Commit fc3e9c7

Browse files
Merge pull request #132 from blockful/feat/multiple-handlers
feat: add multiple handlers support to TriggerProcessorService
2 parents c40d877 + 8674809 commit fc3e9c7

62 files changed

Lines changed: 2435 additions & 223 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/consumers/jest.config.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: 'ts-jest',
4+
testEnvironment: 'node',
5+
roots: ['<rootDir>/src'],
6+
testMatch: ['**/*.test.ts'],
7+
collectCoverageFrom: [
8+
'src/**/*.ts',
9+
'!src/**/*.d.ts',
10+
'!src/**/index.ts',
11+
],
12+
moduleNameMapper: {
13+
'^@/(.*)$': '<rootDir>/src/$1',
14+
},
15+
};

apps/consumers/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
"start": "node dist/index.js",
88
"dev": "nodemon --exec 'ts-node' src/index.ts",
99
"build": "tsc",
10-
"check-types": "tsc --noEmit"
10+
"check-types": "tsc --noEmit",
11+
"test": "jest"
1112
},
1213
"keywords": [],
1314
"author": "",
1415
"license": "ISC",
1516
"description": "",
1617
"devDependencies": {
18+
"@jest/globals": "^29.7.0",
19+
"@types/jest": "^29.5.11",
1720
"@types/node": "^20.17.46",
21+
"jest": "^29.7.0",
1822
"nodemon": "^3.1.0",
23+
"ts-jest": "^29.1.1",
1924
"ts-node": "^10.9.2",
2025
"typescript": "^5.8.3"
2126
},

apps/consumers/src/app.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AxiosInstance } from 'axios';
44
import { TelegramBotService } from './services/telegram-bot.service';
55
import { DAOService } from './services/dao.service';
66
import { WalletService } from './services/wallet.service';
7+
import { ExplorerService } from './services/explorer.service';
78
import { EnsResolverService } from './services/ens-resolver.service';
89
import { AnticaptureClient } from '@notification-system/anticapture-client';
910
import { SubscriptionAPIService } from './services/subscription-api.service';
@@ -19,16 +20,17 @@ export class App {
1920
telegramBotToken: string,
2021
subscriptionServerUrl: string,
2122
httpClient: AxiosInstance,
22-
rabbitmqUrl: string
23+
rabbitmqUrl: string,
24+
ensResolver: EnsResolverService
2325
) {
2426
const subscriptionApi = new SubscriptionAPIService(subscriptionServerUrl);
2527
const anticaptureClient = new AnticaptureClient(httpClient);
26-
const ensResolver = new EnsResolverService();
2728
const daoService = new DAOService(anticaptureClient, subscriptionApi);
2829
const walletService = new WalletService(subscriptionApi, ensResolver);
30+
const explorerService = new ExplorerService();
2931
const bot = new Telegraf<ContextWithSession>(telegramBotToken);
3032
bot.use(session());
31-
this.telegramBotService = new TelegramBotService(bot, daoService, walletService, ensResolver);
33+
this.telegramBotService = new TelegramBotService(bot, daoService, walletService, explorerService, ensResolver);
3234
this.rabbitmqUrl = rabbitmqUrl;
3335
}
3436

apps/consumers/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@
1313
import axios from 'axios';
1414
import { App } from './app';
1515
import { loadConfig } from './config/env';
16+
import { EnsResolverService } from './services/ens-resolver.service';
1617

1718
const config = loadConfig();
1819

20+
// Create ENS resolver
21+
const ensResolver = new EnsResolverService();
22+
1923
// Create and start the application
2024
const app = new App(
2125
config.telegramBotToken,
2226
config.subscriptionServerUrl,
2327
axios.create({ baseURL: config.anticaptureGraphqlEndpoint }),
24-
config.rabbitmqUrl
28+
config.rabbitmqUrl,
29+
ensResolver
2530
);
2631

2732
(async () => {

apps/consumers/src/interfaces/notification.interface.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44
*/
55

66
export interface NotificationPayload {
7-
userId?: string;
8-
channelUserId: number;
7+
userId: string;
8+
channel: string;
9+
channelUserId: string | number;
910
message: string;
1011
metadata?: {
1112
addresses?: Record<string, string>; // key: placeholder name, value: ethereum address
13+
transaction?: {
14+
hash: string;
15+
chainId: number;
16+
};
17+
[key: string]: any;
1218
};
1319
}
1420

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { ExplorerService } from './explorer.service';
2+
3+
describe('ExplorerService', () => {
4+
let explorerService: ExplorerService;
5+
6+
beforeEach(() => {
7+
explorerService = new ExplorerService();
8+
});
9+
10+
describe('getTransactionLink', () => {
11+
it('should return Etherscan link for Ethereum mainnet (chainId: 1)', () => {
12+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
13+
const result = explorerService.getTransactionLink(1, hash);
14+
expect(result).toBe('https://etherscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
15+
});
16+
17+
it('should return Arbiscan link for Arbitrum (chainId: 42161)', () => {
18+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
19+
const result = explorerService.getTransactionLink(42161, hash);
20+
expect(result).toBe('https://arbiscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
21+
});
22+
23+
it('should return Optimistic Etherscan link for Optimism (chainId: 10)', () => {
24+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
25+
const result = explorerService.getTransactionLink(10, hash);
26+
expect(result).toBe('https://optimistic.etherscan.io/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
27+
});
28+
29+
it('should return empty string for unknown chain', () => {
30+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
31+
const result = explorerService.getTransactionLink(999999, hash);
32+
expect(result).toBe('');
33+
});
34+
35+
it('should return empty string for invalid hash format', () => {
36+
const hash = 'invalid-hash';
37+
const result = explorerService.getTransactionLink(1, hash);
38+
expect(result).toBe('');
39+
});
40+
41+
it('should return empty string for empty hash', () => {
42+
const result = explorerService.getTransactionLink(1, '');
43+
expect(result).toBe('');
44+
});
45+
46+
it('should return empty string for 0x-only hash', () => {
47+
const result = explorerService.getTransactionLink(1, '0x');
48+
expect(result).toBe('');
49+
});
50+
51+
it('should handle undefined chainId gracefully', () => {
52+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
53+
const result = explorerService.getTransactionLink(undefined as any, hash);
54+
expect(result).toBe('');
55+
});
56+
57+
it('should handle null chainId gracefully', () => {
58+
const hash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
59+
const result = explorerService.getTransactionLink(null as any, hash);
60+
expect(result).toBe('');
61+
});
62+
});
63+
64+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Explorer Service
3+
* Handles blockchain explorer URL generation for transaction links using Viem.
4+
*/
5+
6+
import * as chains from 'viem/chains';
7+
import { isHash, extractChain } from 'viem';
8+
import type { Chain } from 'viem';
9+
10+
export class ExplorerService {
11+
12+
/**
13+
* Generate transaction URL for a given chain and transaction hash
14+
* @param chainId The EIP-155 chain ID
15+
* @param transactionHash The transaction hash
16+
* @returns Clean transaction URL or empty string if hash is invalid
17+
*/
18+
public getTransactionLink(chainId: number, transactionHash: string): string {
19+
// Validate hash using viem's isHash utility
20+
if (!isHash(transactionHash)) {
21+
return '';
22+
}
23+
24+
const chain = extractChain({
25+
chains: Object.values(chains),
26+
id: chainId as any
27+
});
28+
29+
if (chain?.blockExplorers?.default?.url) {
30+
// Ensure transaction hash is properly formatted with 0x prefix
31+
const formattedHash = transactionHash.startsWith('0x')
32+
? transactionHash
33+
: `0x${transactionHash}`;
34+
35+
return `${chain.blockExplorers.default.url}/tx/${formattedHash}`;
36+
}
37+
38+
// Fallback when explorer is not available or chain not found
39+
return '';
40+
}
41+
42+
}

apps/consumers/src/services/rabbitmq-notification-consumer.service.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
import { RabbitMQConnection, RabbitMQConsumer, RabbitMQMessage } from '@notification-system/rabbitmq-client';
22
import { TelegramBotService } from './telegram-bot.service';
3-
4-
/**
5-
* Interface for the notification payload received from RabbitMQ
6-
*/
7-
interface NotificationPayload {
8-
userId: string;
9-
channelUserId: number;
10-
message: string;
11-
metadata?: {
12-
addresses?: Record<string, string>;
13-
};
14-
}
3+
import { NotificationPayload } from '../interfaces/notification.interface';
154

165
/**
176
* Service to consume notification messages from RabbitMQ and process them through TelegramBotService
@@ -55,12 +44,7 @@ export class RabbitMQNotificationConsumerService {
5544
return;
5645
}
5746
try {
58-
await this.telegramBotService.sendNotification({
59-
userId: message.payload.userId,
60-
channelUserId: message.payload.channelUserId,
61-
message: message.payload.message,
62-
metadata: message.payload.metadata,
63-
});
47+
await this.telegramBotService.sendNotification(message.payload);
6448
} catch (error: any) {
6549
if (error?.response?.description === 'Bad Request: chat not found') {
6650
console.log('⚠️ Unable to send message to user:', message.payload.userId);

apps/consumers/src/services/telegram-bot.service.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Telegraf, Markup } from 'telegraf';
88
import { WELCOME_MESSAGE, HELP_MESSAGE, DAOS_BUTTON_TEXT, LEARN_MORE_BUTTON_TEXT, MY_WALLETS_BUTTON_TEXT } from '../messages';
99
import { DAOService } from '../services/dao.service';
1010
import { WalletService } from '../services/wallet.service';
11+
import { ExplorerService } from '../services/explorer.service';
1112
import { EnsResolverService } from '../services/ens-resolver.service';
1213
import { ContextWithSession } from '../interfaces/bot.interface';
1314
import { NotificationPayload } from '../interfaces/notification.interface';
@@ -16,17 +17,20 @@ export class TelegramBotService {
1617
private bot: Telegraf<ContextWithSession>;
1718
private daoService: DAOService;
1819
private walletService: WalletService;
20+
private explorerService: ExplorerService;
1921
private ensResolver: EnsResolverService;
2022

2123
constructor(
2224
bot: Telegraf<ContextWithSession>,
2325
daoService: DAOService,
2426
walletService: WalletService,
27+
explorerService: ExplorerService,
2528
ensResolver: EnsResolverService
2629
) {
2730
this.bot = bot;
2831
this.daoService = daoService;
2932
this.walletService = walletService;
33+
this.explorerService = explorerService;
3034
this.ensResolver = ensResolver;
3135
this.setupCommands();
3236
}
@@ -143,6 +147,14 @@ export class TelegramBotService {
143147
public async sendNotification(payload: NotificationPayload): Promise<string> {
144148
let processedMessage = payload.message;
145149

150+
// Process transaction link if transaction metadata is provided
151+
if (payload.metadata?.transaction) {
152+
const { hash, chainId } = payload.metadata.transaction;
153+
const txUrl = this.explorerService.getTransactionLink(chainId, hash);
154+
const markdownLink = `[Transaction details](${txUrl})`;
155+
processedMessage = processedMessage.replace('{{txLink}}', markdownLink);
156+
}
157+
146158
// Process ENS names if addresses are provided in metadata
147159
if (payload.metadata?.addresses) {
148160
for (const [placeholder, address] of Object.entries(payload.metadata.addresses)) {
@@ -153,7 +165,8 @@ export class TelegramBotService {
153165

154166
const sentMessage = await this.bot.telegram.sendMessage(
155167
payload.channelUserId,
156-
processedMessage
168+
processedMessage,
169+
{ parse_mode: 'Markdown' }
157170
);
158171
return `${sentMessage.message_id}`;
159172
}

apps/consumers/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
"resolveJsonModule": true
1818
},
1919
"include": ["src/**/*"],
20-
"exclude": ["node_modules", "dist"]
20+
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/*.spec.ts"]
2121
}

0 commit comments

Comments
 (0)