@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/.turbo/turbo-build.log +2 -0
- package/IMPLEMENTATION.md +275 -0
- package/README.md +233 -0
- package/__tests__/test-module.js +83 -0
- package/build.sh +6 -0
- package/lib/index.d.ts +122 -0
- package/lib/index.js +333 -0
- package/package.json +28 -0
- package/src/index.ts +422 -0
- package/tsconfig.json +19 -0
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
|
+
|