@pioneer-platform/pioneer-subscriptions 1.0.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/lib/index.d.ts ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Pioneer Subscriptions Module
3
+ *
4
+ * Session-based blockchain address subscription manager for real-time payment notifications.
5
+ *
6
+ * Features:
7
+ * - Subscribe to blockchain addresses via blockbook websockets
8
+ * - Session-based: subscriptions tied to client connection lifecycle
9
+ * - Automatic cleanup on disconnect
10
+ * - Multi-chain support
11
+ * - Event callbacks for payment notifications
12
+ *
13
+ * Usage:
14
+ * ```ts
15
+ * const manager = new SubscriptionManager();
16
+ * await manager.init();
17
+ *
18
+ * // Register callback for payment notifications
19
+ * manager.onPayment((data) => {
20
+ * console.log('Payment received:', data);
21
+ * // Push to client via websocket
22
+ * });
23
+ *
24
+ * // Subscribe addresses for a session
25
+ * await manager.subscribe({
26
+ * sessionId: 'socket-123',
27
+ * username: 'user1',
28
+ * coin: 'BTC',
29
+ * addresses: ['bc1q...', '3...']
30
+ * });
31
+ *
32
+ * // On disconnect
33
+ * await manager.unsubscribe('socket-123');
34
+ * ```
35
+ */
36
+ export interface SubscriptionRequest {
37
+ sessionId: string;
38
+ username: string;
39
+ coin: string;
40
+ addresses: string[];
41
+ }
42
+ export interface PaymentNotification {
43
+ sessionId: string;
44
+ username: string;
45
+ coin: string;
46
+ address: string;
47
+ txid: string;
48
+ amount: string;
49
+ confirmations: number;
50
+ timestamp: number;
51
+ }
52
+ export interface SessionSubscription {
53
+ sessionId: string;
54
+ username: string;
55
+ coin: string;
56
+ addresses: string[];
57
+ subscribedAt: number;
58
+ }
59
+ export interface SubscriptionStats {
60
+ totalSessions: number;
61
+ totalAddresses: number;
62
+ sessionsByUsername: Record<string, number>;
63
+ addressesByCoin: Record<string, number>;
64
+ sessions: SessionSubscription[];
65
+ }
66
+ type PaymentCallback = (notification: PaymentNotification) => void;
67
+ export declare class SubscriptionManager {
68
+ private blockbookSockets;
69
+ private sessions;
70
+ private addressToSessions;
71
+ private paymentCallbacks;
72
+ private isInitialized;
73
+ constructor();
74
+ /**
75
+ * Initialize the subscription manager and blockbook connections
76
+ */
77
+ init(): Promise<void>;
78
+ /**
79
+ * Register a callback to be called when payment notifications are received
80
+ */
81
+ onPayment(callback: PaymentCallback): void;
82
+ /**
83
+ * Remove a payment callback
84
+ */
85
+ offPayment(callback: PaymentCallback): void;
86
+ /**
87
+ * Setup listeners for blockbook websocket notifications
88
+ */
89
+ private setupBlockbookListeners;
90
+ /**
91
+ * Handle notification from blockbook about address activity
92
+ */
93
+ private handleBlockbookNotification;
94
+ /**
95
+ * Subscribe a session to monitor specific addresses
96
+ */
97
+ subscribe(request: SubscriptionRequest): Promise<{
98
+ success: boolean;
99
+ message: string;
100
+ }>;
101
+ /**
102
+ * Unsubscribe a session from all its addresses
103
+ */
104
+ unsubscribe(sessionId: string): Promise<void>;
105
+ /**
106
+ * Get subscription statistics
107
+ */
108
+ getStats(): SubscriptionStats;
109
+ /**
110
+ * Get available coins for subscription
111
+ */
112
+ getAvailableCoins(): string[];
113
+ /**
114
+ * Check if initialized
115
+ */
116
+ isReady(): boolean;
117
+ /**
118
+ * Cleanup and shutdown
119
+ */
120
+ shutdown(): Promise<void>;
121
+ }
122
+ export default SubscriptionManager;
package/lib/index.js ADDED
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ /**
3
+ * Pioneer Subscriptions Module
4
+ *
5
+ * Session-based blockchain address subscription manager for real-time payment notifications.
6
+ *
7
+ * Features:
8
+ * - Subscribe to blockchain addresses via blockbook websockets
9
+ * - Session-based: subscriptions tied to client connection lifecycle
10
+ * - Automatic cleanup on disconnect
11
+ * - Multi-chain support
12
+ * - Event callbacks for payment notifications
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const manager = new SubscriptionManager();
17
+ * await manager.init();
18
+ *
19
+ * // Register callback for payment notifications
20
+ * manager.onPayment((data) => {
21
+ * console.log('Payment received:', data);
22
+ * // Push to client via websocket
23
+ * });
24
+ *
25
+ * // Subscribe addresses for a session
26
+ * await manager.subscribe({
27
+ * sessionId: 'socket-123',
28
+ * username: 'user1',
29
+ * coin: 'BTC',
30
+ * addresses: ['bc1q...', '3...']
31
+ * });
32
+ *
33
+ * // On disconnect
34
+ * await manager.unsubscribe('socket-123');
35
+ * ```
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.SubscriptionManager = void 0;
39
+ const log = require('@pioneer-platform/loggerdog')();
40
+ const blockbook = require('@pioneer-platform/blockbook');
41
+ const TAG = ' | PioneerSubscriptions | ';
42
+ class SubscriptionManager {
43
+ constructor() {
44
+ this.blockbookSockets = {};
45
+ this.sessions = new Map();
46
+ this.addressToSessions = new Map();
47
+ this.paymentCallbacks = new Set();
48
+ this.isInitialized = false;
49
+ }
50
+ /**
51
+ * Initialize the subscription manager and blockbook connections
52
+ */
53
+ async init() {
54
+ const tag = TAG + ' | init | ';
55
+ try {
56
+ log.info(tag, 'Initializing Subscription Manager');
57
+ // Initialize blockbook module
58
+ await blockbook.init();
59
+ // Get blockbook websocket clients
60
+ this.blockbookSockets = blockbook.getBlockbookSockets();
61
+ const availableCoins = Object.keys(this.blockbookSockets);
62
+ log.info(tag, `Blockbook sockets available for: ${availableCoins.join(', ')}`);
63
+ // Setup listeners for each coin's blockbook socket
64
+ this.setupBlockbookListeners();
65
+ this.isInitialized = true;
66
+ log.info(tag, '✅ Subscription Manager initialized successfully');
67
+ return;
68
+ }
69
+ catch (error) {
70
+ log.error(tag, 'Failed to initialize Subscription Manager:', error);
71
+ throw error;
72
+ }
73
+ }
74
+ /**
75
+ * Register a callback to be called when payment notifications are received
76
+ */
77
+ onPayment(callback) {
78
+ this.paymentCallbacks.add(callback);
79
+ }
80
+ /**
81
+ * Remove a payment callback
82
+ */
83
+ offPayment(callback) {
84
+ this.paymentCallbacks.delete(callback);
85
+ }
86
+ /**
87
+ * Setup listeners for blockbook websocket notifications
88
+ */
89
+ setupBlockbookListeners() {
90
+ const tag = TAG + ' | setupBlockbookListeners | ';
91
+ for (const [coin, socket] of Object.entries(this.blockbookSockets)) {
92
+ if (!socket)
93
+ continue;
94
+ try {
95
+ log.info(tag, `Setting up listener for ${coin}`);
96
+ // Listen for address transaction notifications
97
+ socket.on('notification', (data) => {
98
+ this.handleBlockbookNotification(coin, data);
99
+ });
100
+ socket.on('error', (error) => {
101
+ log.error(tag, `Blockbook socket error for ${coin}:`, error);
102
+ });
103
+ socket.on('connect', () => {
104
+ log.info(tag, `Blockbook socket connected for ${coin}`);
105
+ });
106
+ socket.on('disconnect', () => {
107
+ log.warn(tag, `Blockbook socket disconnected for ${coin}`);
108
+ });
109
+ }
110
+ catch (error) {
111
+ log.error(tag, `Failed to setup listener for ${coin}:`, error);
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Handle notification from blockbook about address activity
117
+ */
118
+ handleBlockbookNotification(coin, data) {
119
+ const tag = TAG + ' | handleBlockbookNotification | ';
120
+ try {
121
+ log.debug(tag, `Received notification for ${coin}:`, data);
122
+ // Extract address from notification
123
+ // Blockbook notification format: { address, tx: {...} }
124
+ const address = data.address;
125
+ if (!address) {
126
+ log.warn(tag, 'Notification missing address:', data);
127
+ return;
128
+ }
129
+ // Find all sessions subscribed to this address
130
+ const addressKey = `${coin}:${address}`;
131
+ const sessionIds = this.addressToSessions.get(addressKey);
132
+ if (!sessionIds || sessionIds.size === 0) {
133
+ log.debug(tag, `No subscribers for ${addressKey}`);
134
+ return;
135
+ }
136
+ log.info(tag, `💰 Payment detected on ${addressKey}, notifying ${sessionIds.size} session(s)`);
137
+ // Notify each subscribed session
138
+ for (const sessionId of sessionIds) {
139
+ const session = this.sessions.get(sessionId);
140
+ if (!session)
141
+ continue;
142
+ const notification = {
143
+ sessionId,
144
+ username: session.username,
145
+ coin,
146
+ address,
147
+ txid: data.tx?.txid || data.txid || 'unknown',
148
+ amount: data.tx?.value || data.value || '0',
149
+ confirmations: data.tx?.confirmations || 0,
150
+ timestamp: Date.now()
151
+ };
152
+ // Call all registered callbacks
153
+ this.paymentCallbacks.forEach(callback => {
154
+ try {
155
+ callback(notification);
156
+ }
157
+ catch (error) {
158
+ log.error(tag, 'Error in payment callback:', error);
159
+ }
160
+ });
161
+ }
162
+ }
163
+ catch (error) {
164
+ log.error(tag, 'Error handling blockbook notification:', error);
165
+ }
166
+ }
167
+ /**
168
+ * Subscribe a session to monitor specific addresses
169
+ */
170
+ async subscribe(request) {
171
+ const tag = TAG + ' | subscribe | ';
172
+ try {
173
+ if (!this.isInitialized) {
174
+ throw new Error('Subscription Manager not initialized');
175
+ }
176
+ const { sessionId, username, coin, addresses } = request;
177
+ const coinUpper = coin.toUpperCase();
178
+ const blockbookSocket = this.blockbookSockets[coinUpper];
179
+ if (!blockbookSocket) {
180
+ return {
181
+ success: false,
182
+ message: `Blockbook not available for ${coinUpper}`
183
+ };
184
+ }
185
+ log.info(tag, `Subscribing session ${sessionId} (${username}) to ${addresses.length} addresses on ${coinUpper}`);
186
+ // Subscribe to blockbook for each address
187
+ const subscribedAddresses = [];
188
+ for (const address of addresses) {
189
+ try {
190
+ // Subscribe via blockbook-client
191
+ await blockbookSocket.subscribeAddresses([address]);
192
+ // Track subscription
193
+ const addressKey = `${coinUpper}:${address}`;
194
+ if (!this.addressToSessions.has(addressKey)) {
195
+ this.addressToSessions.set(addressKey, new Set());
196
+ }
197
+ this.addressToSessions.get(addressKey).add(sessionId);
198
+ subscribedAddresses.push(address);
199
+ log.debug(tag, `✅ Subscribed to ${addressKey}`);
200
+ }
201
+ catch (error) {
202
+ log.error(tag, `Failed to subscribe to ${address}:`, error);
203
+ }
204
+ }
205
+ // Store or merge session info
206
+ const existing = this.sessions.get(sessionId);
207
+ if (existing && existing.coin === coinUpper) {
208
+ // Merge addresses
209
+ existing.addresses = Array.from(new Set([...existing.addresses, ...subscribedAddresses]));
210
+ }
211
+ else {
212
+ // New session subscription
213
+ this.sessions.set(sessionId, {
214
+ sessionId,
215
+ username,
216
+ coin: coinUpper,
217
+ addresses: subscribedAddresses,
218
+ subscribedAt: Date.now()
219
+ });
220
+ }
221
+ log.info(tag, `✅ Successfully subscribed session ${sessionId} to ${subscribedAddresses.length} addresses on ${coinUpper}`);
222
+ return {
223
+ success: true,
224
+ message: `Subscribed to ${subscribedAddresses.length} addresses on ${coinUpper}`
225
+ };
226
+ }
227
+ catch (error) {
228
+ log.error(tag, 'Error subscribing:', error);
229
+ return {
230
+ success: false,
231
+ message: error.message || 'Unknown error'
232
+ };
233
+ }
234
+ }
235
+ /**
236
+ * Unsubscribe a session from all its addresses
237
+ */
238
+ async unsubscribe(sessionId) {
239
+ const tag = TAG + ' | unsubscribe | ';
240
+ try {
241
+ const session = this.sessions.get(sessionId);
242
+ if (!session) {
243
+ log.debug(tag, `No subscription found for session ${sessionId}`);
244
+ return;
245
+ }
246
+ log.info(tag, `Unsubscribing session ${sessionId} from ${session.addresses.length} addresses on ${session.coin}`);
247
+ const blockbookSocket = this.blockbookSockets[session.coin];
248
+ // Remove from address mappings and unsubscribe if no more sessions
249
+ for (const address of session.addresses) {
250
+ const addressKey = `${session.coin}:${address}`;
251
+ const sessions = this.addressToSessions.get(addressKey);
252
+ if (sessions) {
253
+ sessions.delete(sessionId);
254
+ // If no more sessions for this address, unsubscribe from blockbook
255
+ if (sessions.size === 0) {
256
+ try {
257
+ if (blockbookSocket) {
258
+ await blockbookSocket.unsubscribeAddresses([address]);
259
+ }
260
+ this.addressToSessions.delete(addressKey);
261
+ log.debug(tag, `Unsubscribed from ${addressKey} (no more sessions)`);
262
+ }
263
+ catch (error) {
264
+ log.error(tag, `Failed to unsubscribe from ${addressKey}:`, error);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ // Remove session
270
+ this.sessions.delete(sessionId);
271
+ log.info(tag, `✅ Successfully cleaned up session ${sessionId}`);
272
+ }
273
+ catch (error) {
274
+ log.error(tag, 'Error unsubscribing:', error);
275
+ }
276
+ }
277
+ /**
278
+ * Get subscription statistics
279
+ */
280
+ getStats() {
281
+ const sessionsByUsername = {};
282
+ const addressesByCoin = {};
283
+ const sessions = [];
284
+ for (const session of this.sessions.values()) {
285
+ sessionsByUsername[session.username] = (sessionsByUsername[session.username] || 0) + 1;
286
+ addressesByCoin[session.coin] = (addressesByCoin[session.coin] || 0) + session.addresses.length;
287
+ sessions.push(session);
288
+ }
289
+ return {
290
+ totalSessions: this.sessions.size,
291
+ totalAddresses: this.addressToSessions.size,
292
+ sessionsByUsername,
293
+ addressesByCoin,
294
+ sessions
295
+ };
296
+ }
297
+ /**
298
+ * Get available coins for subscription
299
+ */
300
+ getAvailableCoins() {
301
+ return Object.keys(this.blockbookSockets);
302
+ }
303
+ /**
304
+ * Check if initialized
305
+ */
306
+ isReady() {
307
+ return this.isInitialized;
308
+ }
309
+ /**
310
+ * Cleanup and shutdown
311
+ */
312
+ async shutdown() {
313
+ const tag = TAG + ' | shutdown | ';
314
+ try {
315
+ log.info(tag, 'Shutting down Subscription Manager');
316
+ // Unsubscribe all sessions
317
+ const sessionIds = Array.from(this.sessions.keys());
318
+ for (const sessionId of sessionIds) {
319
+ await this.unsubscribe(sessionId);
320
+ }
321
+ // Clear callbacks
322
+ this.paymentCallbacks.clear();
323
+ this.isInitialized = false;
324
+ log.info(tag, '✅ Subscription Manager shut down successfully');
325
+ }
326
+ catch (error) {
327
+ log.error(tag, 'Error during shutdown:', error);
328
+ }
329
+ }
330
+ }
331
+ exports.SubscriptionManager = SubscriptionManager;
332
+ // Export singleton instance
333
+ exports.default = SubscriptionManager;
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@pioneer-platform/pioneer-subscriptions",
3
+ "version": "1.0.0",
4
+ "description": "Session-based blockchain address subscription manager for real-time payment notifications",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
+ "scripts": {
8
+ "test": "pnpm run build && node __tests__/test-module.js",
9
+ "build": "tsc -p .",
10
+ "prepublish": "tsc -p .",
11
+ "build:live": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "@pioneer-platform/loggerdog": "^8.11.0",
15
+ "@pioneer-platform/blockbook": "^8.12.0"
16
+ },
17
+ "keywords": [
18
+ "blockchain",
19
+ "subscriptions",
20
+ "websocket",
21
+ "payments",
22
+ "notifications",
23
+ "pioneer"
24
+ ],
25
+ "author": "highlander",
26
+ "license": "MIT"
27
+ }
28
+