@dignetwork/chia-block-listener 0.1.8 → 0.1.11

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/.yarnrc.yml CHANGED
@@ -1 +1 @@
1
- nodeLinker: node-modules
1
+ nodeLinker: node-modules
package/README.md CHANGED
@@ -10,6 +10,7 @@ A high-performance Chia blockchain listener for Node.js, built with Rust and NAP
10
10
  - **Event-Driven Architecture**: TypeScript-friendly event system with full type safety
11
11
  - **Transaction Analysis**: Parse CLVM puzzles and solutions from coin spends
12
12
  - **Historical Block Access**: Retrieve blocks by height or ranges
13
+ - **Connection Pool**: ChiaPeerPool provides automatic load balancing and rate limiting for historical queries
13
14
  - **Cross-platform Support**: Works on Windows, macOS, and Linux (x64 and ARM64)
14
15
  - **TypeScript Support**: Complete TypeScript definitions with IntelliSense
15
16
 
@@ -33,11 +34,11 @@ const listener = new ChiaBlockListener()
33
34
  // Listen for block events
34
35
  listener.on('blockReceived', (block) => {
35
36
  console.log(`New block received: ${block.height}`)
36
- console.log(`Header hash: ${block.header_hash}`)
37
+ console.log(`Header hash: ${block.headerHash}`)
37
38
  console.log(`Timestamp: ${new Date(block.timestamp * 1000)}`)
38
- console.log(`Coin additions: ${block.coin_additions.length}`)
39
- console.log(`Coin removals: ${block.coin_removals.length}`)
40
- console.log(`Coin spends: ${block.coin_spends.length}`)
39
+ console.log(`Coin additions: ${block.coinAdditions.length}`)
40
+ console.log(`Coin removals: ${block.coinRemovals.length}`)
41
+ console.log(`Coin spends: ${block.coinSpends.length}`)
41
42
  })
42
43
 
43
44
  // Listen for peer connection events
@@ -127,6 +128,80 @@ Retrieves a range of blocks from a connected peer.
127
128
 
128
129
  **Returns:** An array of `BlockReceivedEvent` objects
129
130
 
131
+ ### ChiaPeerPool Class
132
+
133
+ The `ChiaPeerPool` provides a managed pool of peer connections for retrieving historical blocks with automatic load balancing and rate limiting.
134
+
135
+ #### Constructor
136
+
137
+ ```javascript
138
+ const pool = new ChiaPeerPool()
139
+ ```
140
+
141
+ Creates a new peer pool instance with built-in rate limiting (500ms per peer).
142
+
143
+ #### Methods
144
+
145
+ ##### `addPeer(host, port, networkId): Promise<string>`
146
+
147
+ Adds a peer to the connection pool.
148
+
149
+ **Parameters:**
150
+ - `host` (string): The hostname or IP address of the Chia node
151
+ - `port` (number): The port number (typically 8444 for mainnet)
152
+ - `networkId` (string): The network identifier ('mainnet', 'testnet', etc.)
153
+
154
+ **Returns:** A Promise that resolves to a unique peer ID string
155
+
156
+ ##### `getBlockByHeight(height): Promise<BlockReceivedEvent>`
157
+
158
+ Retrieves a specific block by height using automatic peer selection and load balancing.
159
+
160
+ **Parameters:**
161
+ - `height` (number): The block height to retrieve
162
+
163
+ **Returns:** A Promise that resolves to a `BlockReceivedEvent` object
164
+
165
+ ##### `removePeer(peerId): Promise<boolean>`
166
+
167
+ Removes a peer from the pool.
168
+
169
+ **Parameters:**
170
+ - `peerId` (string): The peer ID to remove
171
+
172
+ **Returns:** A Promise that resolves to `true` if the peer was removed, `false` otherwise
173
+
174
+ ##### `shutdown(): Promise<void>`
175
+
176
+ Shuts down the pool and disconnects all peers.
177
+
178
+ ##### `getConnectedPeers(): Promise<string[]>`
179
+
180
+ Gets the list of currently connected peer IDs.
181
+
182
+ **Returns:** Array of peer ID strings (format: "host:port")
183
+
184
+ ##### `getPeakHeight(): Promise<number | null>`
185
+
186
+ Gets the highest blockchain peak height seen across all connected peers.
187
+
188
+ **Returns:** The highest peak height as a number, or null if no peaks have been received yet
189
+
190
+ ##### `on(event, callback): void`
191
+
192
+ Registers an event handler for pool events.
193
+
194
+ **Parameters:**
195
+ - `event` (string): The event name ('peerConnected' or 'peerDisconnected')
196
+ - `callback` (function): The event handler function
197
+
198
+ ##### `off(event, callback): void`
199
+
200
+ Removes an event handler.
201
+
202
+ **Parameters:**
203
+ - `event` (string): The event name to stop listening for
204
+
130
205
  ### Events
131
206
 
132
207
  The `ChiaBlockListener` emits the following events:
@@ -149,6 +224,28 @@ Fired when a peer connection is lost.
149
224
 
150
225
  **Callback:** `(event: PeerDisconnectedEvent) => void`
151
226
 
227
+ ### ChiaPeerPool Events
228
+
229
+ The `ChiaPeerPool` emits the following events:
230
+
231
+ #### `peerConnected`
232
+
233
+ Fired when a peer is successfully added to the pool.
234
+
235
+ **Callback:** `(event: PeerConnectedEvent) => void`
236
+
237
+ #### `peerDisconnected`
238
+
239
+ Fired when a peer is removed from the pool or disconnects.
240
+
241
+ **Callback:** `(event: PeerDisconnectedEvent) => void`
242
+
243
+ #### `newPeakHeight`
244
+
245
+ Fired when a new highest blockchain peak is discovered.
246
+
247
+ **Callback:** `(event: NewPeakHeightEvent) => void`
248
+
152
249
  ### Event Data Types
153
250
 
154
251
  #### `BlockReceivedEvent`
@@ -190,6 +287,16 @@ interface PeerDisconnectedEvent {
190
287
  }
191
288
  ```
192
289
 
290
+ #### `NewPeakHeightEvent`
291
+
292
+ ```typescript
293
+ interface NewPeakHeightEvent {
294
+ oldPeak: number | null // Previous highest peak (null if first peak)
295
+ newPeak: number // New highest peak height
296
+ peerId: string // Peer that discovered this peak
297
+ }
298
+ ```
299
+
193
300
  #### `CoinRecord`
194
301
 
195
302
  ```typescript
@@ -211,14 +318,181 @@ interface CoinSpend {
211
318
  }
212
319
  ```
213
320
 
321
+ ## ChiaPeerPool Usage
322
+
323
+ The `ChiaPeerPool` is designed for efficiently retrieving historical blocks with automatic load balancing across multiple peers.
324
+
325
+ ### Basic Usage
326
+
327
+ ```javascript
328
+ const { ChiaPeerPool, initTracing } = require('@dignetwork/chia-block-listener')
329
+
330
+ async function main() {
331
+ // Initialize tracing
332
+ initTracing()
333
+
334
+ // Create a peer pool
335
+ const pool = new ChiaPeerPool()
336
+
337
+ // Listen for pool events
338
+ pool.on('peerConnected', (event) => {
339
+ console.log(`Peer connected to pool: ${event.peerId}`)
340
+ })
341
+
342
+ pool.on('peerDisconnected', (event) => {
343
+ console.log(`Peer disconnected from pool: ${event.peerId}`)
344
+ })
345
+
346
+ pool.on('newPeakHeight', (event) => {
347
+ console.log(`New blockchain peak detected!`)
348
+ console.log(` Previous: ${event.oldPeak || 'None'}`)
349
+ console.log(` New: ${event.newPeak}`)
350
+ console.log(` Discovered by: ${event.peerId}`)
351
+ })
352
+
353
+ // Add multiple peers
354
+ await pool.addPeer('node1.chia.net', 8444, 'mainnet')
355
+ await pool.addPeer('node2.chia.net', 8444, 'mainnet')
356
+ await pool.addPeer('node3.chia.net', 8444, 'mainnet')
357
+
358
+ // Fetch blocks with automatic load balancing
359
+ const block1 = await pool.getBlockByHeight(5000000)
360
+ const block2 = await pool.getBlockByHeight(5000001)
361
+ const block3 = await pool.getBlockByHeight(5000002)
362
+
363
+ console.log(`Block ${block1.height}: ${block1.coinSpends.length} spends`)
364
+ console.log(`Block ${block2.height}: ${block2.coinSpends.length} spends`)
365
+ console.log(`Block ${block3.height}: ${block3.coinSpends.length} spends`)
366
+
367
+ // Shutdown the pool
368
+ await pool.shutdown()
369
+ }
370
+
371
+ main().catch(console.error)
372
+ ```
373
+
374
+ ### Advanced Pool Features
375
+
376
+ #### Rate Limiting
377
+
378
+ The pool automatically enforces a 500ms rate limit per peer to prevent overwhelming any single node:
379
+
380
+ ```javascript
381
+ // Rapid requests are automatically queued and distributed
382
+ const promises = []
383
+ for (let i = 5000000; i < 5000100; i++) {
384
+ promises.push(pool.getBlockByHeight(i))
385
+ }
386
+
387
+ // All requests will be processed efficiently across all peers
388
+ const blocks = await Promise.all(promises)
389
+ console.log(`Retrieved ${blocks.length} blocks`)
390
+ ```
391
+
392
+ #### Dynamic Peer Management
393
+
394
+ ```javascript
395
+ // Monitor pool health
396
+ const peers = await pool.getConnectedPeers()
397
+ console.log(`Active peers in pool: ${peers.length}`)
398
+
399
+ // Remove underperforming peers
400
+ if (slowPeer) {
401
+ await pool.removePeer(slowPeer)
402
+ console.log('Removed slow peer from pool')
403
+ }
404
+
405
+ // Add new peers dynamically
406
+ if (peers.length < 3) {
407
+ await pool.addPeer('backup-node.chia.net', 8444, 'mainnet')
408
+ }
409
+ ```
410
+
411
+ #### Error Handling
412
+
413
+ ```javascript
414
+ try {
415
+ const block = await pool.getBlockByHeight(5000000)
416
+ console.log(`Retrieved block ${block.height}`)
417
+ } catch (error) {
418
+ console.error('Failed to retrieve block:', error)
419
+
420
+ // The pool will automatically try other peers
421
+ // You can also add more peers if needed
422
+ const peers = await pool.getConnectedPeers()
423
+ if (peers.length === 0) {
424
+ console.log('No peers available, adding new ones...')
425
+ await pool.addPeer('node1.chia.net', 8444, 'mainnet')
426
+ }
427
+ }
428
+ ```
429
+
430
+ #### Peak Height Tracking
431
+
432
+ ```javascript
433
+ // Monitor blockchain sync progress
434
+ const pool = new ChiaPeerPool()
435
+
436
+ // Track peak changes
437
+ let currentPeak = null
438
+ pool.on('newPeakHeight', (event) => {
439
+ currentPeak = event.newPeak
440
+ const progress = event.oldPeak
441
+ ? `+${event.newPeak - event.oldPeak} blocks`
442
+ : 'Initial peak'
443
+ console.log(`Peak update: ${event.newPeak} (${progress})`)
444
+ })
445
+
446
+ // Add peers
447
+ await pool.addPeer('node1.chia.net', 8444, 'mainnet')
448
+ await pool.addPeer('node2.chia.net', 8444, 'mainnet')
449
+
450
+ // Check current peak
451
+ const peak = await pool.getPeakHeight()
452
+ console.log(`Current highest peak: ${peak || 'None yet'}`)
453
+
454
+ // Fetch some blocks to trigger peak updates
455
+ await pool.getBlockByHeight(5000000)
456
+ await pool.getBlockByHeight(5100000)
457
+ await pool.getBlockByHeight(5200000)
458
+
459
+ // Monitor sync status
460
+ setInterval(async () => {
461
+ const peak = await pool.getPeakHeight()
462
+ if (peak) {
463
+ const estimatedCurrent = 5200000 + Math.floor((Date.now() / 1000 - 1700000000) / 18.75)
464
+ const syncPercentage = (peak / estimatedCurrent * 100).toFixed(2)
465
+ console.log(`Sync status: ${syncPercentage}% (peak: ${peak})`)
466
+ }
467
+ }, 60000) // Check every minute
468
+ ```
469
+
470
+ ### When to Use ChiaPeerPool vs ChiaBlockListener
471
+
472
+ - **Use ChiaPeerPool when:**
473
+ - You need to fetch historical blocks
474
+ - You want automatic load balancing across multiple peers
475
+ - You're making many block requests and need rate limiting
476
+ - You don't need real-time block notifications
477
+
478
+ - **Use ChiaBlockListener when:**
479
+ - You need real-time notifications of new blocks
480
+ - You want to monitor the blockchain as it grows
481
+ - You need to track specific addresses or puzzle hashes in real-time
482
+ - You're building applications that react to blockchain events
483
+
484
+ Both classes can be used together in the same application for different purposes.
485
+
214
486
  ## TypeScript Usage
215
487
 
216
488
  ```typescript
217
489
  import {
218
490
  ChiaBlockListener,
491
+ ChiaPeerPool,
219
492
  BlockReceivedEvent,
220
493
  PeerConnectedEvent,
221
494
  PeerDisconnectedEvent,
495
+ NewPeakHeightEvent,
222
496
  CoinRecord,
223
497
  CoinSpend,
224
498
  initTracing,
@@ -279,6 +553,29 @@ async function getHistoricalBlocks() {
279
553
  // Get event type constants
280
554
  const eventTypes = getEventTypes()
281
555
  console.log('Available events:', eventTypes)
556
+
557
+ // TypeScript support for ChiaPeerPool
558
+ const pool = new ChiaPeerPool()
559
+
560
+ // Type-safe event handling
561
+ pool.on('peerConnected', (event: PeerConnectedEvent) => {
562
+ console.log(`Pool peer connected: ${event.peerId}`)
563
+ })
564
+
565
+ pool.on('newPeakHeight', (event: NewPeakHeightEvent) => {
566
+ console.log(`New peak: ${event.oldPeak} → ${event.newPeak}`)
567
+ })
568
+
569
+ // Async/await with proper typing
570
+ async function fetchHistoricalData() {
571
+ const block: BlockReceivedEvent = await pool.getBlockByHeight(5000000)
572
+ const peers: string[] = await pool.getConnectedPeers()
573
+ const peak: number | null = await pool.getPeakHeight()
574
+
575
+ console.log(`Block ${block.height} has ${block.coinSpends.length} spends`)
576
+ console.log(`Pool has ${peers.length} active peers`)
577
+ console.log(`Current peak: ${peak || 'No peak yet'}`)
578
+ }
282
579
  ```
283
580
 
284
581
  ## Advanced Usage
@@ -1,9 +1,6 @@
1
1
  use crate::{
2
2
  error::{GeneratorParserError, Result},
3
- types::{
4
- BlockHeightInfo, CoinInfo, CoinSpendInfo, GeneratorAnalysis, GeneratorBlockInfo,
5
- ParsedBlock, ParsedGenerator,
6
- },
3
+ types::{BlockHeightInfo, CoinInfo, CoinSpendInfo, GeneratorBlockInfo, ParsedBlock},
7
4
  };
8
5
  use chia_bls::Signature;
9
6
  use chia_consensus::{
@@ -14,7 +11,7 @@ use chia_consensus::{
14
11
  run_block_generator::{run_block_generator2, setup_generator_args},
15
12
  validation_error::{atom, first, next, rest, ErrorCode},
16
13
  };
17
- use chia_protocol::{Bytes32, FullBlock};
14
+ use chia_protocol::FullBlock;
18
15
  use chia_traits::streamable::Streamable;
19
16
  use clvm_utils::tree_hash;
20
17
  use clvmr::{
@@ -63,7 +60,7 @@ impl BlockParser {
63
60
  .map(|g| g.len() as u32);
64
61
 
65
62
  // Extract generator info
66
- let generator_info = block
63
+ let _generator_info = block
67
64
  .transactions_generator
68
65
  .as_ref()
69
66
  .map(|gen| GeneratorBlockInfo {
@@ -101,7 +98,6 @@ impl BlockParser {
101
98
  coin_creations,
102
99
  has_transactions_generator,
103
100
  generator_size,
104
- generator_info,
105
101
  })
106
102
  }
107
103
 
@@ -306,9 +302,23 @@ impl BlockParser {
306
302
  ) -> Option<CoinSpendInfo> {
307
303
  // Extract parent coin info
308
304
  let parent_bytes = self.extract_parent_coin_info(allocator, coin_spend)?;
309
- let mut parent_arr = [0u8; 32];
310
- parent_arr.copy_from_slice(&parent_bytes);
311
- let parent_coin_info = Bytes32::new(parent_arr);
305
+ info!("🔍 DEBUG: parent_bytes length = {}", parent_bytes.len());
306
+
307
+ if parent_bytes.len() != 32 {
308
+ info!(
309
+ "❌ ERROR: parent_bytes wrong length: {} bytes (expected 32)",
310
+ parent_bytes.len()
311
+ );
312
+ return None;
313
+ }
314
+
315
+ // parent_bytes is already Vec<u8> with 32 bytes, just hex encode it directly
316
+ let parent_hex = hex::encode(&parent_bytes);
317
+ info!(
318
+ "🔍 DEBUG: parent_coin_info hex = {} (length: {})",
319
+ parent_hex,
320
+ parent_hex.len()
321
+ );
312
322
 
313
323
  // Extract puzzle, amount, and solution
314
324
  let rest1 = rest(allocator, coin_spend).ok()?;
@@ -324,14 +334,31 @@ impl BlockParser {
324
334
 
325
335
  // Calculate puzzle hash
326
336
  let puzzle_hash_vec = tree_hash(allocator, puzzle);
327
- let mut puzzle_hash_arr = [0u8; 32];
328
- puzzle_hash_arr.copy_from_slice(&puzzle_hash_vec);
329
- let puzzle_hash = Bytes32::new(puzzle_hash_arr);
337
+ info!(
338
+ "🔍 DEBUG: tree_hash returned {} bytes",
339
+ puzzle_hash_vec.len()
340
+ );
341
+
342
+ if puzzle_hash_vec.len() != 32 {
343
+ info!(
344
+ "❌ ERROR: tree_hash returned wrong length: {} bytes (expected 32)",
345
+ puzzle_hash_vec.len()
346
+ );
347
+ return None;
348
+ }
349
+
350
+ // tree_hash returns Vec<u8> with 32 bytes, just hex encode it directly
351
+ let puzzle_hash_hex = hex::encode(&puzzle_hash_vec);
352
+ info!(
353
+ "🔍 DEBUG: puzzle_hash hex = {} (length: {})",
354
+ puzzle_hash_hex,
355
+ puzzle_hash_hex.len()
356
+ );
330
357
 
331
358
  // Create coin info
332
359
  let coin_info = CoinInfo {
333
- parent_coin_info: hex::encode(&parent_coin_info),
334
- puzzle_hash: hex::encode(&puzzle_hash),
360
+ parent_coin_info: parent_hex,
361
+ puzzle_hash: puzzle_hash_hex,
335
362
  amount,
336
363
  };
337
364
 
@@ -342,15 +369,15 @@ impl BlockParser {
342
369
  // Get created coins from conditions
343
370
  let created_coins = self.extract_created_coins(spend_index, spend_bundle_conditions);
344
371
 
345
- Some(CoinSpendInfo {
346
- coin: coin_info,
347
- puzzle_reveal,
348
- solution: solution_bytes,
349
- real_data: true,
350
- parsing_method: "clvm_execution".to_string(),
351
- offset: 0,
372
+ Some(CoinSpendInfo::new(
373
+ coin_info,
374
+ hex::encode(puzzle_reveal),
375
+ hex::encode(solution_bytes),
376
+ true,
377
+ "From transaction generator".to_string(),
378
+ 0,
352
379
  created_coins,
353
- })
380
+ ))
354
381
  }
355
382
 
356
383
  /// Extract parent coin info from a coin spend node
@@ -433,42 +460,62 @@ impl BlockParser {
433
460
  })
434
461
  }
435
462
 
463
+ /*
436
464
  /// Parse generator from hex string
437
465
  pub fn parse_generator_from_hex(&self, generator_hex: &str) -> Result<ParsedGenerator> {
438
466
  let generator_bytes =
439
- hex::decode(generator_hex).map_err(|e| GeneratorParserError::HexDecodingError(e))?;
467
+ hex::decode(generator_hex).map_err(|e| ParseError::HexDecodingError(e))?;
440
468
  self.parse_generator_from_bytes(&generator_bytes)
441
469
  }
442
470
 
443
471
  /// Parse generator from bytes
444
472
  pub fn parse_generator_from_bytes(&self, generator_bytes: &[u8]) -> Result<ParsedGenerator> {
445
- let analysis = self.analyze_generator(generator_bytes)?;
446
-
473
+ // Create a dummy GeneratorBlockInfo for now
447
474
  Ok(ParsedGenerator {
448
- block_info: GeneratorBlockInfo {
449
- prev_header_hash: Bytes32::default(),
450
- transactions_generator: Some(generator_bytes.to_vec()),
451
- transactions_generator_ref_list: Vec::new(),
452
- },
475
+ block_info: GeneratorBlockInfo::new(
476
+ [0u8; 32].into(),
477
+ Some(generator_bytes.to_vec()),
478
+ vec![],
479
+ ),
453
480
  generator_hex: Some(hex::encode(generator_bytes)),
454
- analysis,
481
+ analysis: self.analyze_generator(generator_bytes)?,
455
482
  })
456
483
  }
457
484
 
458
485
  /// Analyze generator bytecode
459
486
  pub fn analyze_generator(&self, generator_bytes: &[u8]) -> Result<GeneratorAnalysis> {
460
487
  let size_bytes = generator_bytes.len();
461
- let is_empty = size_bytes == 0;
462
-
463
- // Check for CLVM patterns
464
- let contains_clvm_patterns = generator_bytes
465
- .windows(2)
466
- .any(|w| matches!(w, [0xff, _] | [_, 0xff]));
467
-
468
- // Check for coin patterns (CREATE_COIN opcode)
469
- let contains_coin_patterns = generator_bytes.windows(1).any(|w| w[0] == 0x33);
488
+ let is_empty = generator_bytes.is_empty();
489
+
490
+ // Check for common CLVM patterns
491
+ let contains_clvm_patterns = generator_bytes.windows(2).any(|w| {
492
+ w == [0x01, 0x00] || // pair
493
+ w == [0x02, 0x00] || // cons
494
+ w == [0x03, 0x00] || // first
495
+ w == [0x04, 0x00] // rest
496
+ });
497
+
498
+ // Check for coin patterns (32-byte sequences)
499
+ let contains_coin_patterns = generator_bytes.len() >= 32;
500
+
501
+ // Calculate simple entropy
502
+ let mut byte_counts = [0u64; 256];
503
+ for &byte in generator_bytes {
504
+ byte_counts[byte as usize] += 1;
505
+ }
470
506
 
471
- let entropy = self.calculate_entropy(generator_bytes);
507
+ let total = generator_bytes.len() as f64;
508
+ let entropy = if total > 0.0 {
509
+ byte_counts.iter()
510
+ .filter(|&&count| count > 0)
511
+ .map(|&count| {
512
+ let p = count as f64 / total;
513
+ -p * p.log2()
514
+ })
515
+ .sum()
516
+ } else {
517
+ 0.0
518
+ };
472
519
 
473
520
  Ok(GeneratorAnalysis {
474
521
  size_bytes,
@@ -478,8 +525,10 @@ impl BlockParser {
478
525
  entropy,
479
526
  })
480
527
  }
528
+ */
481
529
 
482
530
  /// Calculate Shannon entropy of data
531
+ #[allow(dead_code)]
483
532
  fn calculate_entropy(&self, data: &[u8]) -> f64 {
484
533
  if data.is_empty() {
485
534
  return 0.0;