Handshake logo
    ___   
   /\__\  
  /:/__/_ 
 /::\/\__\
 \/\::/  /
   /:/  / 
   \/__/  

Events and Sockets

To listen for and react to events in hsd, a listener must be added in the runtime script. If you are running a full node (for example) you might already be familiar with the hsd full node launch script, which instantiates a FullNode object, adds a wallet, and begins the connection and synchronization process. The script ends with an async function that actually starts these processes, and this is a good place to add event listeners. Using the table below you can discover which object needs to be listened on for each event type. Notice that because the wallet is added as a plugin, its object hierarchy path is a little… awkward :-)

// Based on https://github.com/handshake-org/hsd/blob/master/bin/node

(async () => {
  await node.ensure();
  await node.open();
  await node.connect();
  node.startSync();

  // add event listeners after everything is open, connected, and started

  // NODE
  node.on('tx', (details) => {
    console.log(' -- node tx -- \n', details)
  });
  
  node.on('block', (details) => {
    console.log(' -- node block -- \n', details)
  });

  // MEMPOOL
  node.mempool.on('confirmed', (details) => {
    console.log(' -- mempool confirmed -- \n', details)
  });

  // WALLET
  node.plugins.walletdb.wdb.on('balance', (details) => {
    console.log(' -- wallet balance -- \n', details)
  });

  node.plugins.walletdb.wdb.on('confirmed', (details) => {
    console.log(' -- wallet confirmed -- \n', details)
  });

  node.plugins.walletdb.wdb.on('address', (details) => {
    console.log(' -- wallet address -- \n', details)
  });

})().catch((err) => {
  console.error(err.stack);
  process.exit(1);
});

Events Directory

This list is comprehensive for all Handshake transaction, wallet, and blockchain activity. Events regarding errors, socket connections and peer connections have been omitted for clarity. Notice that certain methods emit the same events but with different return objects, and not all re-emitters return everything they receive.

Event Returns Origin Re-Emitters
tip ChainEntry Chain: open(), disconnect(), reconnect(), setBestChain(), reset()
connect ChainEntry, Block, CoinView Chain: setBestChain(), reconnect() chainFullNode (returns ChainEntry, Block only)
chainSPVNode (returns ChainEntry, Block only)
SPVNodeFullNodeNodeClient (emits as block connect, returns ChainEntry, Block.txs only)
disconnect ChainEntry, Headers, CoinView Chain: reorganizeSPV() chainFullNode (returns ChainEntry, Headers only)
chainSPVNode (returns ChainEntry, Headers only)
SPVNodeFullNodeNodeClient (emits as block disconnect, returns ChainEntry only)
disconnect ChainEntry, Block, CoinView Chain: disconnect() chainFullNode (returns ChainEntry, Block only)
chainSPVNode (returns ChainEntry, Block only)
SPVNodeFullNodeNodeClient (emits as block disconnect, returns ChainEntry only)
reconnect ChainEntry, Block Chain: reconnect()
reorganize tip (ChainEntry), competitor (ChainEntry) Chain: reorganize(), reorganizeSPV() chainFullNode
chainSPVNode
block Block, ChainEntry Chain: setBestChain()
CPUMiner: _start()
chainPool
chainFullNode (returns Block only)
chainSPVNode (returns Block only)
competitor Block, ChainEntry Chain: saveAlternate()
bad orphan Error, ID Chain: handleOrphans()
Mempool: handleOrphans()
resolved Block, ChainEntry Chain: handleOrphans()
checkpoint Hash, Height Chain: verifyCheckpoint()
orphan Block Chain: storeOrphan()
full Chain: maybeSync() chainPool
confirmed TX, ChainEntry Mempool: _addBlock()
confirmed TX, Details TXDB: confirm() txdbWalletDB (also returns Wallet)
txdbWallet
unconfirmed TX, Block Mempool: _removeBlock()
unconfirmed TX, Details TXDB: disconnect() txdbWalletDB (also returns Wallet)
txdbWallet
conflict TX Mempool: _removeBlock()
conflict TX, Details TXDB: disconnect() txdbWalletDB (also returns Wallet)
txdbWallet
tx TX, CoinView Mempool: addEntry() mempoolPool (returns TX only)
mempoolpoolSPVNode (returns TX only)
mempoolFullNode (returns TX only)
SPVNodeFullNodeNodeClient (returns TX only)
tx TX Pool: _handleTX()
(only if there is no mempool, i.e. SPV)
poolSPVNode
tx TX, Details TXDB: insert() txdbWalletDB (also returns Wallet)
txdbWallet
double spend MempoolEntry Mempool: removeDoubleSpends()
balance Balance TXDB: insert(), confirm(), disconnect(), erase() txdbWalletDB (also returns Wallet)
txdbWallet
address [WalletKey] Wallet: _add(), walletWalletDB (returns parent Wallet, [WalletKey])

Socket Events

Websocket connections in hsd are handled by two servers, one for Node and one for Wallet. Those servers each have child objects such as Chain, Mempool, Pool, and WalletDB, and relay events from them out the socket. To receive an event, the socket client must watch a channel (such as chain, mempool, or auth) or join a wallet (which would be user-defined like primary, hot-wallet, or multisig1). All wallets can be joined at once by joining '*'.

Listen for Socket Events with bsock

To make a socket connection to hsd, you need to run a websocket client. Luckily the bcoin and hsd developers have developed bsock, a minimal websocket-only implementation of the socket.io protocol. By default, bsock listens on localhost, and you only need to pass it a port number to connect to one of the hsd servers. The example below illustrates how to establish the socket connection, authenticate with your user-defined API key and then send and receive events! See the tables below for a complete list of calls and events available in hsd.

// bsock-example.js

const bsock = require('bsock');
const {Network, ChainEntry} = require('hsd');
const network = Network.get('regtest');
const apiKey = 'api-key';

nodeSocket = bsock.connect(network.rpcPort);
walletSocket = bsock.connect(network.walletPort);

nodeSocket.on('connect', async (e) => {
  try {
    console.log('Node - Connect event:\n', e);

    // `auth` must be called before any other actions
    console.log('Node - Attempting auth:\n', await nodeSocket.call('auth', apiKey));

    // `watch chain` subscribes us to chain events like `block`
    console.log('Node - Attempting watch chain:\n', await nodeSocket.call('watch chain'));

    // Some calls simply request information from the server like an http request
    console.log('Node - Attempting get tip:');
    const tip = await nodeSocket.call('get tip');
    console.log(ChainEntry.from(tip));

  } catch (e) {
    console.log('Node - Connection Error:\n', e);
  } 
});

// listen for new blocks
nodeSocket.bind('chain connect', (raw, txs) => {
  console.log('Node - Chain Connect Event:\n', ChainEntry.fromRaw(raw));
});

walletSocket.on('connect', async (e) => {
  try {
    console.log('Wallet - Connect event:\n', e);

    // `auth` is required before proceeding
    console.log('Wallet - Attempting auth:\n', await walletSocket.call('auth', apiKey));

    // here we join all wallets, but we could also just join `primary` or any other wallet
    console.log('Wallet - Attempting join *:\n', await walletSocket.call('join', '*'));

  } catch (e) {
    console.log('Wallet - Connection Error:\n', e);
  } 
});

// listen for new wallet transactions
walletSocket.bind('tx', (wallet, tx) => {
  console.log('Wallet - TX Event -- wallet:\n', wallet);
  console.log('Wallet - TX Event -- tx:\n', tx);
});

// listen for new address events
// (only fired when current account address receives a transaction)
walletSocket.bind('address', (wallet, json) => {
  console.log('Wallet - Address Event -- wallet:\n', wallet);
  console.log('Wallet - Address Event -- json:\n', json);
});

To see this script in action, first start hsd however you usually do:

hsd --daemon --network=regtest

Run the script (this is where the event output will be printed):

node bsock-example.js

Then, in a separate terminal window, run some commands to trigger the events!

hsd-cli rpc generatetoaddress 1  rs1qyep4stuujjkdfv483nfse53vvgtfuqr9z6v4cm

Sockets made easy: hs-client

bsock is a great low-level library for dealing with sockets, but we also have hs-client which simplifies both socket and regular http connections. A simple one-line command in the terminal can listen to all wallet events and print all the returned data:

hsw-cli listen

hs-client can also be used in a script to listen for events. If you’re already familiar with the hsd-cli API this will look very familiar. Here’s part of the script above re-written using hs-client instead of bsock:

const {NodeClient} = require('hs-client');
const {Network, ChainEntry} = require('hsd');
const network = Network.get('regtest');

const clientOptions = {
  port: network.rpcPort,
  apiKey: 'api-key'
}
const client = new NodeClient(clientOptions);

(async () => {
  // bclient handles the connection, the auth, and the channel subscriptions
  await client.open();

  // use socket connection to request data
  const tip = await client.getTip();
  console.log(tip);
})();

// listen for new blocks
client.bind('chain connect', (raw) => {
  console.log('Node - Chain Connect Event:\n', ChainEntry.fromRaw(raw));
});

Socket Events Directory

Wallet

All wallet events are emitted by a WalletDB object, which may have been triggered by its parent TXDB or Wallet. The socket emits the event along with the wallet ID, and the same “Returns” as listed above.

Event Returns
tx WalletID, TX Details
confirmed WalletID, TX Details
unconfirmed WalletID, TX Details
conflict WalletID, TX Details
balance WalletID, Balance
address WalletID, [WalletKey]

Node

Event Returns Channel Origin Original Event
chain connect ChainEntry.toRaw() chain Chain connect
block connect ChainEntry.toRaw(), Block.txs chain Chain connect
chain disconnect ChainEntry.toRaw() chain Chain disconnect
block disconnect ChainEntry.toRaw() chain Chain disconnect
tx TX.toRaw() mempool Pool tx

Server Hooks

Certain events can also be sent back to the server from the client to request new data or trigger a server action. The client action is a “call” and the server waits with a “hook”.

Wallet

Event Args Returns
auth 1. api key null
join 1. wallet id
2. wallet token
null
leave 1. wallet id null

Node

Event Args Returns
auth 1. api key null
watch chain (none) null
unwatch chain (none) null
watch mempool (none) null
unwatch mempool (none) null
set filter 1. Bloom filter (Buffer) null
get tip (none) ChainEntry.toRaw()
get entry 1. hash ChainEntry.toRaw()
get hashes 1. start (int)
2. end (int)
[hashes]
add filter 1. filter ([Buffer]) null
reset filter (none) null
estimate fee 1. blocks (int) fee rate (float)
send 1. tx (Buffer) null
rescan 1. hash null

See a mistake? Open a pull request.

https://github.com/handshake-org/handshake-org.github.io/blob/master/src/guides/events.md