/ docs / rest / websockets.md
websockets.md
  1  # WebSockets with `lnd`'s REST API
  2  
  3  This document describes how streaming response REST calls can be used correctly
  4  by making use of the WebSocket API.
  5  
  6  As an example, we are going to write a simple JavaScript program that subscribes
  7  to `lnd`'s
  8  [block notification RPC](https://api.lightning.community/#v2-chainnotifier-register-blocks).
  9  
 10  The WebSocket will be kept open as long as `lnd` runs and JavaScript program
 11  isn't stopped.
 12  
 13  ## Browser environment
 14  
 15  When using WebSockets in a browser, there are certain security limitations of
 16  what header fields are allowed to be sent. Therefore, the macaroon cannot just
 17  be added as a `Grpc-Metadata-Macaroon` header field as it would work with normal
 18  REST calls. The browser will just ignore that header field and not send it.
 19  
 20  Instead, we have added a workaround in `lnd`'s WebSocket proxy that allows
 21  sending the macaroon as a WebSocket "protocol":
 22  
 23  ```javascript
 24  const host = 'localhost:8080'; // The default REST port of lnd, can be overwritten with --restlisten=ip:port
 25  const macaroon = '0201036c6e6402eb01030a10625e7e60fd00f5a6f9cd53f33fc82a...'; // The hex encoded macaroon to send
 26  const initialRequest = { // The initial request to send (see API docs for each RPC).
 27      hash: "xlkMdV382uNPskw6eEjDGFMQHxHNnZZgL47aVDSwiRQ=", // Just some example to show that all `byte` fields always have to be base64 encoded in the REST API.
 28      height: 144,
 29  }
 30  
 31  // The protocol is our workaround for sending the macaroon because custom header
 32  // fields aren't allowed to be sent by the browser when opening a WebSocket.
 33  const protocolString = 'Grpc-Metadata-Macaroon+' + macaroon;
 34  
 35  // Let's now connect the web socket. Notice that all WebSocket open calls are
 36  // always GET requests. If the RPC expects a call to be POST or DELETE (see API
 37  // docs to find out), the query parameter "method" can be set to overwrite.
 38  const wsUrl = 'wss://' + host + '/v2/chainnotifier/register/blocks?method=POST';
 39  let ws = new WebSocket(wsUrl, protocolString);
 40  ws.onopen = function (event) {
 41      // After the WS connection is establishes, lnd expects the client to send the
 42      // initial message. If an RPC doesn't have any request parameters, an empty
 43      // JSON object has to be sent as a string, for example: ws.send('{}')
 44      ws.send(JSON.stringify(initialRequest));
 45  }
 46  ws.onmessage = function (event) {
 47      // We received a new message.
 48      console.log(event);
 49  
 50      // The data we're really interested in is in data and is always a string
 51      // that needs to be parsed as JSON and always contains a "result" field:
 52      console.log("Payload: ");
 53      console.log(JSON.parse(event.data).result);
 54  }
 55  ws.onerror = function (event) {
 56      // An error occurred, let's log it to the console.
 57      console.log(event);
 58  }
 59  ```
 60  
 61  ## Node.js environment
 62  
 63  With Node.js it is a bit easier to use the streaming response APIs because we
 64  can set the macaroon header field directly. This is the example from the API
 65  docs:
 66  
 67  ```javascript
 68  // --------------------------
 69  // Example with websockets:
 70  // --------------------------
 71  const WebSocket = require('ws');
 72  const fs = require('fs');
 73  const macaroon = fs.readFileSync('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon').toString('hex');
 74  let ws = new WebSocket('wss://localhost:8080/v2/chainnotifier/register/blocks?method=POST', {
 75    // Work-around for self-signed certificates.
 76    rejectUnauthorized: false,
 77    headers: {
 78      'Grpc-Metadata-Macaroon': macaroon,
 79    },
 80  });
 81  let requestBody = { 
 82    hash: "<byte>",
 83    height: "<int64>",
 84  }
 85  ws.on('open', function() {
 86      ws.send(JSON.stringify(requestBody));
 87  });
 88  ws.on('error', function(err) {
 89      console.log('Error: ' + err);
 90  });
 91  ws.on('message', function(body) {
 92      console.log(body);
 93  });
 94  // Console output (repeated for every message in the stream):
 95  //  { 
 96  //      "hash": <byte>, 
 97  //      "height": <int64>, 
 98  //  }
 99  ```
100  
101  ## Request-streaming RPCs
102  
103  Starting with `lnd v0.13.0-beta` all RPCs can be used through REST, even those
104  that are fully bidirectional (e.g. the client can also send multiple request
105  messages to the stream).
106  
107  **Example**:
108  
109  As an example we show how one can use the bidirectional channel acceptor RPC.
110  Through that RPC each incoming channel open request (another peer opening a
111  channel to our node) will be passed in for inspection. We can decide
112  programmatically whether to accept or reject the channel.
113  
114  ```javascript
115  // --------------------------
116  // Example with websockets:
117  // --------------------------
118  const WebSocket = require('ws');
119  const fs = require('fs');
120  const macaroon = fs.readFileSync('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon').toString('hex');
121  let ws = new WebSocket('wss://localhost:8080/v1/channels/acceptor?method=POST', {
122    // Work-around for self-signed certificates.
123    rejectUnauthorized: false,
124    headers: {
125      'Grpc-Metadata-Macaroon': macaroon,
126    },
127  });
128  ws.on('open', function() {
129      // We always _need_ to send an initial message to kickstart the request.
130      // This empty message will be ignored by the channel acceptor though, this
131      // is just for telling the grpc-gateway library that it can forward the
132      // request to the gRPC interface now. If this were an RPC where the client
133      // always sends the first message (for example the streaming payment RPC
134      // /v1/channels/transaction-stream), we'd simply send the first "real"
135      // message here when needed.
136      ws.send('{}');
137  });
138  ws.on('error', function(err) {
139      console.log('Error: ' + err);
140  });
141  ws.on('ping', function ping(event) {
142     console.log('Received ping from server: ' + JSON.stringify(event)); 
143  });
144  ws.on('message', function incoming(event) {
145      console.log('New channel accept message: ' + event);
146      const result = JSON.parse(event).result;
147      
148      // Accept the channel after inspecting it.
149      ws.send(JSON.stringify({accept: true, pending_chan_id: result.pending_chan_id}));
150  });
151  ```