@bsv/teranode-listener
v1.1.7
Published
An npm package to subscribe to Teranode P2P topics in a private DHT network and log messages
Downloads
509
Readme
@bsv/teranode-listener
BSV BLOCKCHAIN | A TypeScript library for subscribing to Teranode P2P topics over libp2p
A robust npm package that enables subscription to Teranode P2P topics using libp2p with private network support, DHT, and gossipsub messaging.
Table of Contents
- Overview
- Installation
- Getting Started
- API Reference
- Configuration
- Examples
- Development
- Contributing
- Support & Contacts
Overview
The @bsv/teranode-listener package provides a simple yet powerful interface for connecting to the Teranode P2P network. It handles:
- PNET compatibility: Connect with the public Teranode mainnet PNET value or a private deployment's own PSK
- DHT Integration: Distributed hash table for peer discovery
- Topic Subscription: Subscribe to specific topics and receive real-time messages
- Peer Management: Automatic peer discovery and connection management
- Message Logging: Built-in logging for network events and messages
Installation
npm install @bsv/teranode-listenerRequirements
- Node.js: Version 22.0.0 or higher
- ES Modules: This package is published as an ES module. Ensure your project supports ES modules by either:
- Adding
"type": "module"to yourpackage.json, or - Using
.mjsfile extensions for your JavaScript files
- Adding
Getting Started
Callback-Based API (Recommended)
The easiest way to use the library is with the TeranodeListener class, which provides topic-specific callbacks:
import { TeranodeListener } from '@bsv/teranode-listener'
// Define callback functions for different topics
const blockCallback = (data: Uint8Array, topic: string, from: string) => {
console.log(`New block received from ${from}:`, data)
// Process block data here
}
const subtreeCallback = (data: Uint8Array, topic: string, from: string) => {
console.log(`Subtree update from ${from}:`, data)
// Process subtree data here
}
// Create listener with topic callbacks
const listener = new TeranodeListener({
'bitcoin/mainnet-block': blockCallback,
'bitcoin/mainnet-subtree': subtreeCallback
})
// Start the listener and connect to Teranode mainnet
await listener.start()
console.log('Listener started and waiting for messages...')Decoding messages
By default, callbacks receive the raw GossipSub bytes (Uint8Array). Pass decodeMessages: true to have the listener perform bounded, canonical two-layer JSON decoding. Callbacks then receive a DecodedMessage (the unverified sender-name field plus a structurally safe payload). TypeScript payload types are compile-time aids, not schema validation or proof of publisher identity:
import { TeranodeListener, type BlockMessage, type DecodedMessage } from '@bsv/teranode-listener'
const listener = new TeranodeListener(
{
'bitcoin/mainnet-block': (msg: DecodedMessage<BlockMessage>, topic, from) => {
console.log(`Block #${msg.payload.Height} (${msg.payload.Hash}) from ${msg.sender}`)
}
},
{ decodeMessages: true }
)
await listener.start()The exported decodeMessage() / tryDecodeMessage() helpers can also be used to decode a message manually. Frames that are not valid JSON (e.g. libp2p control frames) are skipped when decodeMessages is on.
Listener construction snapshots and validates the supplied callbacks,
configuration, topic list, and address arrays. Later caller mutation cannot
silently change the network or decoding policy, and boolean controls accept
only literal booleans. Concurrent start() or stop() calls share one
lifecycle transition; a failed start cleans up its partial node and may be
retried.
Function-Based API
Alternatively, you can use the original function-based API:
import { startSubscriber } from '@bsv/teranode-listener'
// Start with selected topics (or omit the argument for all mainnet topics).
await startSubscriber({
topics: ['bitcoin/mainnet-block', 'bitcoin/mainnet-subtree']
})
console.log('Subscriber started and listening for messages...')Once started, both approaches automatically:
- Connect to the official Teranode bootstrap peer
- Use the mainnet shared key
- Listen on
127.0.0.1:9901 - Connect to known active Teranode peers
Custom Configuration
import { startSubscriber } from '@bsv/teranode-listener'
const config = {
topics: ['bitcoin/mainnet-block'], // Only subscribe to blocks
listenAddresses: ['/ip4/0.0.0.0/tcp/4000'] // Listen on a different port
}
// Start with custom topics and port
await startSubscriber(config)
console.log('Subscriber started with custom configuration...')Complete Custom Setup
import { startSubscriber } from '@bsv/teranode-listener'
const config = {
bootstrapPeers: ['/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWExample1'],
staticPeers: [
'/ip4/192.168.1.100/tcp/4003/p2p/12D3KooWStatic1',
'/ip4/192.168.1.101/tcp/4003/p2p/12D3KooWStatic2'
],
sharedKey: 'your-custom-hex-shared-key-here',
topics: ['bitcoin/testnet-block', 'bitcoin/testnet-subtree'],
listenAddresses: ['/ip4/0.0.0.0/tcp/4000'],
dhtProtocolID: '/custom-protocol'
}
await startSubscriber(config)For more detailed examples, check our Examples section.
API Reference
TeranodeListener Class (Recommended)
The primary API for subscribing to Teranode P2P topics with callback functions.
Constructor
new TeranodeListener(topicCallbacks: TopicCallbacks, config?: TeranodeListenerConfig)Parameters:
topicCallbacks- Object mapping topic names to callback functionsconfig- Optional configuration (uses Teranode mainnet defaults)
Example:
const listener = new TeranodeListener({
'bitcoin/mainnet-block': (data, topic, from) => {
console.log('Block received:', data)
},
'bitcoin/mainnet-subtree': (data, topic, from) => {
console.log('Subtree update:', data)
}
})
// Start the listener (it does not start automatically)
await listener.start()Methods
start(): Promise<void>- Start the listener and connect to Teranode peers (must be called after construction)addTopicCallback(topic: Topic, callback: MessageCallback): void- Add a new topic subscriptionremoveTopicCallback(topic: Topic): void- Remove a topic subscriptionstop(): Promise<void>- Stop the listenergetNode(): Libp2p | null- Get the underlying libp2p nodegetConnectedPeerCount(): number- Get number of connected peers
Types
// Supported Teranode P2P topics
export type Topic =
| 'bitcoin/mainnet-bestblock' // Best block message
| 'bitcoin/mainnet-block' // When miners find a block solution
| 'bitcoin/mainnet-subtree' // When a subtree is created
| 'bitcoin/mainnet-mining_on' // When mining is enabled
| 'bitcoin/mainnet-handshake' // When a peer connects to the network
| 'bitcoin/mainnet-rejected_tx' // When a transaction is rejected
type MessageCallback = (data: Uint8Array, topic: Topic, from: string) => void
type TopicCallbacks = Partial<Record<Topic, MessageCallback>>
interface TeranodeListenerConfig {
bootstrapPeers?: string[] // Bootstrap peer multiaddrs (default: Teranode mainnet bootstrap)
staticPeers?: string[] // Static peer multiaddrs (default: Known Teranode mainnet peers)
sharedKey?: string // Hex string of PSK (default: Teranode mainnet key)
dhtProtocolID?: string // DHT protocol prefix (default: '/teranode')
listenAddresses?: string[] // Listen addresses (default: ['/ip4/127.0.0.1/tcp/9901'])
usePrivateDHT?: boolean // Whether to use private DHT (default: true)
}startSubscriber(config?: SubscriberConfig): Promise<void>
Legacy function-based API for subscribing to topics.
Parameters
config- Optional configuration object for the subscriber. If not provided, uses mainnet defaults.
Returns
A Promise that resolves when the subscriber is successfully started.
SubscriberConfig
Configuration interface for the function-based API. All parameters are optional:
interface SubscriberConfig {
bootstrapPeers?: string[] // Bootstrap peer multiaddrs (default: Teranode mainnet bootstrap)
staticPeers?: string[] // Static peer multiaddrs (default: Known Teranode mainnet peers)
sharedKey?: string // Hex string of PSK (default: Teranode mainnet key)
dhtProtocolID?: string // DHT protocol prefix (default: '/teranode')
topics?: Topic[] // Topics to subscribe to (default: all Teranode topics)
listenAddresses?: string[] // Listen addresses (default: ['/ip4/127.0.0.1/tcp/9901'])
usePrivateDHT?: boolean // Whether to use private DHT (default: true)
}Configuration
Default Configuration
The package comes with production-ready defaults for Teranode mainnet:
bootstrapPeers:['/dns4/teranode-bootstrap.bsvb.tech/tcp/9901/p2p/12D3KooWESmhNAN8s6NPdGNvJH3zJ4wMKDxapXKNUe2DzkAwKYqK']staticPeers: Array of known active Teranode mainnet peers (TAAL, BSVB, etc.)sharedKey: public 32-byte Teranode mainnet PNET compatibility valuetopics: all sixbitcoin/mainnet-*topics listed in theTopictypelistenAddresses:['/ip4/127.0.0.1/tcp/9901']dhtProtocolID:/teranodeusePrivateDHT:true
Customizable Parameters
All parameters are optional and can be overridden:
bootstrapPeers: Array of multiaddr strings for initial peer discoverystaticPeers: Additional peers to maintain persistent connections withsharedKey: exactly 64 hexadecimal characters representing a 32-byte PNET valuetopics: Array of topic strings to subscribe tolistenAddresses: Network addresses to listen ondhtProtocolID: Custom DHT protocol identifierusePrivateDHT: Whether to enable the Kademlia DHT service;falsenow omits that network service entirely
Pre-Shared Key Format
The sharedKey should be provided as a hexadecimal string without the PSK headers. The library automatically formats it as:
/key/swarm/psk/1.0.0/
/base16/
<your-hex-key>The default mainnet value is published in this package so every consumer can join the same network. It is not a secret, membership credential, peer allowlist, or message signature. PNET and Noise protect compatible transport; they do not make a received event authoritative. A custom network can use a secret PSK to restrict initial membership, but applications must still verify security-critical transaction, block, and chain claims independently.
Examples
Example 1: Basic TeranodeListener Usage
import { TeranodeListener, type Topic } from '@bsv/teranode-listener'
// Simple callback-based listener
const listener = new TeranodeListener({
'bitcoin/mainnet-block': (data: Uint8Array, topic: Topic, from: string) => {
console.log(`New block from ${from}:`, data.length, 'bytes')
// Process block data
},
'bitcoin/mainnet-subtree': (data: Uint8Array, topic: Topic, from: string) => {
console.log(`Subtree update from ${from}:`, data.length, 'bytes')
// Process subtree data
}
})
await listener.start()
console.log('Listener started, waiting for messages...')Example 2: Advanced TeranodeListener with Custom Configuration
import { TeranodeListener } from '@bsv/teranode-listener'
// Create a listener with topic-specific callbacks
const listener = new TeranodeListener({
'bitcoin/mainnet-block': (data, topic, from) => {
console.log(`Received block from ${from}:`, data)
},
'bitcoin/mainnet-subtree': (data, topic, from) => {
console.log(`Received subtree from ${from}:`, data)
}
})
await listener.start()
console.log('Connected peers:', listener.getConnectedPeerCount())
// Add more topics dynamically
listener.addTopicCallback('bitcoin/mainnet-rejected_tx', (data, topic, from) => {
console.log(`Received rejection from ${from}:`, data)
})
// Monitor connection status
setInterval(() => {
console.log('Connected peers:', listener.getConnectedPeerCount())
}, 30000)Example 3: Function-Based API (Legacy)
import { startSubscriber } from '@bsv/teranode-listener'
// Connect to Teranode mainnet with all defaults
startSubscriber()
.then(() => console.log('Connected to Teranode mainnet!'))
.catch(console.error)Example 4: Custom Port and Multiple Topics (Function API)
import { startSubscriber } from '@bsv/teranode-listener'
// Use a different port and subscribe to multiple topics
const config = {
topics: ['bitcoin/mainnet-block', 'bitcoin/mainnet-subtree', 'bitcoin/mainnet-rejected_tx'],
listenAddresses: ['/ip4/0.0.0.0/tcp/4000']
}
await startSubscriber(config)
console.log('Listening on port 4000 for blocks, subtrees, and rejected transactions...')Example 5: Environment-Based Configuration
import { startSubscriber } from '@bsv/teranode-listener'
const config = {
topics: process.env.TOPICS?.split(',') || undefined, // Use defaults if not set
listenAddresses: process.env.LISTEN_ADDRESS ? [process.env.LISTEN_ADDRESS] : undefined,
sharedKey: process.env.CUSTOM_SHARED_KEY || undefined // Use default mainnet key if not set
}
// Start with environment overrides, falling back to defaults
await startSubscriber(config)
console.log('Started with environment configuration...')Example 6: Complete Custom Network
import { startSubscriber } from '@bsv/teranode-listener'
// Connect to a custom private network
const config = {
bootstrapPeers: [
'/ip4/10.0.0.1/tcp/4001/p2p/12D3KooWBootstrap1',
'/ip4/10.0.0.2/tcp/4001/p2p/12D3KooWBootstrap2'
],
staticPeers: ['/ip4/10.0.0.10/tcp/4003/p2p/12D3KooWStatic1'],
sharedKey: 'your-custom-private-network-key',
dhtProtocolID: '/custom-network',
topics: ['bitcoin/testnet-block', 'bitcoin/testnet-subtree'],
listenAddresses: ['/ip4/0.0.0.0/tcp/4000'],
usePrivateDHT: true
}
await startSubscriber(config)
console.log('Connected to custom private network...')Security boundary
- Treat every topic, envelope sender name, and payload field as untrusted.
- The callback
fromvalue is the libp2p propagation peer, which may be a relay; it is not proof that the peer authored the application payload. decodeMessagesbounds and structurally validates JSON, but it does not authenticate a publisher or validate each topic-specific schema.- The internal subtree-format implementation bounds node and byte counts, preserves exact unsigned 64-bit values, and verifies aggregate totals, conflicts, and the canonical Bitcoin Merkle root before accepting bytes. These checks establish internal consistency, not who authored a subtree.
- Verify hashes, headers, transactions, Merkle evidence, heights, and chain state through an independent trusted chain tracker before changing durable or financial state.
- Keep the default loopback listen address unless inbound P2P reachability is explicitly required and protected at the network edge.
Development
Building from Source
# Clone the repository
git clone https://github.com/bsv-blockchain/ts-stack.git
cd ts-stack
# Install dependencies
pnpm install
# Run the package contract
pnpm --filter @bsv/teranode-listener format:check
pnpm --filter @bsv/teranode-listener lint
pnpm --filter @bsv/teranode-listener typecheck
pnpm --filter @bsv/teranode-listener test:coverage
pnpm --filter @bsv/teranode-listener pack:checkTesting
This package uses Jest with ts-jest. Run the suite with:
pnpm --filter @bsv/teranode-listener testThe tests exercise message decoding, subtree construction and mutation,
listener lifecycle, subscriptions, peer reconnection, callback isolation, and
clean shutdown. Coverage thresholds enforce the package's measured baseline.
pack:check installs the exact ESM tarball and verifies its public exports.
Project Structure
ts-p2p/
├── src/
│ ├── index.ts # Main library and listener
│ ├── subtrees.ts # Subtree data structure and validation
│ └── messages.ts # Wire-format types and decoder
├── test/
│ ├── index.test.ts # Listener lifecycle and peer behavior
│ ├── messages.test.ts # Decoder test suite
│ └── subtrees.test.ts # Subtree behavior and validation
├── dist/ # Compiled JavaScript output
├── jest.config.js # Jest (ts-jest) configuration
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
└── README.md # This fileDependencies
This package relies on several key libp2p modules:
- libp2p: Core P2P networking library
- @chainsafe/libp2p-gossipsub: Gossip-based pub/sub messaging
- @libp2p/kad-dht: Kademlia DHT for peer discovery
- @libp2p/pnet: Private network support with PSK
- @chainsafe/libp2p-noise: Noise protocol for secure connections
Contributing
We welcome contributions to improve the @bsv/teranode-listener package. Whether it's bug reports, feature requests, or pull requests - all contributions are appreciated.
How to Contribute
- Fork the repository - Start by forking the project repository to your GitHub account
- Clone the repository - Clone the forked repository to your local machine
- Create a new branch - Create a new branch for your feature or bug fix
- Make your changes - Implement your changes with appropriate tests
- Build and test - Ensure the project builds and all tests pass
- Submit a pull request - Submit a pull request with a clear description
Development Guidelines
- Follow TypeScript best practices
- Maintain backward compatibility when possible
- Add tests for new features
- Update documentation as needed
- Follow the existing code style and conventions
Support & Contacts
Project Maintainers:
For questions, bug reports, or feature requests:
- Open an issue on GitHub
- Check existing documentation
License
TS Stack first-party changes are under the Open BSV License Version 6. The recovered pre-monorepo MIT notice is retained in THIRD_PARTY_NOTICES.md and LICENSES/.
Thank you for being a part of the BSV Blockchain ecosystem. Let's build the future of BSV Blockchain together!
