ethereumNodeRemote.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { EventEmitter } from 'events';
  2. import WebSocket from 'ws';
  3. import logger from './utils/logger';
  4. import Sockets from './socketManager';
  5. import Settings from './settings';
  6. import { resetRemoteNode, remoteBlockReceived } from './core/nodes/actions';
  7. import { InfuraEndpoints } from './constants';
  8. const ethereumNodeRemoteLog = logger.create('EthereumNodeRemote');
  9. // Increase defaultMaxListeners since
  10. // every subscription created in mist
  11. // adds a new listener in the remote node
  12. require('events').EventEmitter.defaultMaxListeners = 500;
  13. let instance;
  14. class EthereumNodeRemote extends EventEmitter {
  15. constructor() {
  16. super();
  17. if (!instance) {
  18. instance = this;
  19. this.lastRequestId = 0;
  20. }
  21. return instance;
  22. }
  23. async start() {
  24. if (this.starting) {
  25. ethereumNodeRemoteLog.trace('Already starting...');
  26. return this.starting;
  27. }
  28. if (this.ws && this.ws.readyState === WebSocket.OPEN) {
  29. ethereumNodeRemoteLog.error('Starting connection but already open');
  30. return;
  31. }
  32. return (this.starting = new Promise((resolve, reject) => {
  33. this.network = store.getState().nodes.network;
  34. ethereumNodeRemoteLog.trace(
  35. `Connecting to remote node on ${this.network}...`
  36. );
  37. const provider = this._getProvider(this.network);
  38. if (!provider) {
  39. const errorMessage = `No provider for network: ${this.network}`;
  40. ethereumNodeRemoteLog.error(errorMessage);
  41. reject(errorMessage);
  42. return;
  43. }
  44. this.ws = new WebSocket(provider);
  45. this.ws.once('open', () => {
  46. this.starting = false;
  47. ethereumNodeRemoteLog.trace(
  48. `Connected to remote node on ${this.network}`
  49. );
  50. this.watchBlockHeaders();
  51. resolve(true);
  52. });
  53. this.ws.on('message', data => {
  54. if (!data) {
  55. return;
  56. }
  57. ethereumNodeRemoteLog.trace(
  58. 'Message from remote WebSocket connection: ',
  59. data
  60. );
  61. });
  62. this.ws.on('close', (code, reason) => {
  63. let errorMessage = `Remote WebSocket connection closed (code: ${code})`;
  64. if (reason) {
  65. errorMessage += ` (reason: ${reason})`;
  66. }
  67. // Restart connection if didn't close on purpose
  68. if (code !== 1000) {
  69. this.start();
  70. errorMessage += '. Reopening connection...';
  71. }
  72. ethereumNodeRemoteLog.warn(errorMessage);
  73. });
  74. this.ws.on('error', error => {
  75. ethereumNodeRemoteLog.warn('Error from ws: ', error);
  76. });
  77. }));
  78. }
  79. async send(method, params = [], retry = false) {
  80. if (!Array.isArray(params)) {
  81. params = [params];
  82. }
  83. if (
  84. !this.ws ||
  85. !this.ws.readyState ||
  86. this.ws.readyState === WebSocket.CLOSED
  87. ) {
  88. ethereumNodeRemoteLog.warn(
  89. `Remote websocket connection not open, attempting to reconnect and retry ${method}...`
  90. );
  91. return new Promise(resolve => {
  92. this.start().then(() => {
  93. resolve(this.send(method, params, retry));
  94. });
  95. });
  96. }
  97. if (this.ws.readyState !== WebSocket.OPEN) {
  98. if (this.ws.readyState === WebSocket.CONNECTING) {
  99. ethereumNodeRemoteLog.error(
  100. `Can't send method ${method} because remote WebSocket is connecting`
  101. );
  102. } else if (this.ws.readyState === WebSocket.CLOSING) {
  103. ethereumNodeRemoteLog.error(
  104. `Can't send method ${method} because remote WebSocket is closing`
  105. );
  106. } else if (this.ws.readyState === WebSocket.CLOSED) {
  107. ethereumNodeRemoteLog.error(
  108. `Can't send method ${method} because remote WebSocket is closed`
  109. );
  110. }
  111. if (!retry) {
  112. ethereumNodeRemoteLog.error(`Retrying ${method} in 1.5s...`);
  113. return new Promise(resolve => {
  114. setTimeout(() => {
  115. resolve(this.send(method, params));
  116. }, 1500);
  117. });
  118. } else {
  119. return null;
  120. }
  121. }
  122. this.lastRequestId += 1;
  123. const request = {
  124. jsonrpc: '2.0',
  125. id: this.lastRequestId,
  126. method,
  127. params
  128. };
  129. this.ws.send(JSON.stringify(request), error => {
  130. if (error) {
  131. ethereumNodeRemoteLog.error(
  132. 'Error from sending request: ',
  133. error,
  134. request
  135. );
  136. } else {
  137. ethereumNodeRemoteLog.trace('Sent request to remote node: ', request);
  138. }
  139. });
  140. return request.id;
  141. }
  142. setNetwork(network) {
  143. this.stop();
  144. this.network = network;
  145. const provider = this._getProvider(network);
  146. if (!provider) {
  147. ethereumNodeRemoteLog.error('No provider');
  148. return;
  149. }
  150. this.ws = new WebSocket(provider);
  151. this.watchBlockHeaders();
  152. }
  153. _getProvider(network) {
  154. switch (network) {
  155. case 'main':
  156. return InfuraEndpoints.ethereum.websockets.Main;
  157. case 'test':
  158. // fall-through (uses Ropsten)
  159. case 'ropsten':
  160. return InfuraEndpoints.ethereum.websockets.Ropsten;
  161. case 'rinkeby':
  162. return InfuraEndpoints.ethereum.websockets.Rinkeby;
  163. case 'kovan':
  164. return InfuraEndpoints.ethereum.websockets.Kovan;
  165. default:
  166. ethereumNodeRemoteLog.error(`Unsupported network type: ${network}`);
  167. return null;
  168. }
  169. }
  170. stop() {
  171. this.unsubscribe();
  172. if (
  173. this.ws &&
  174. this.ws.readyState ===
  175. [WebSocket.OPEN, WebSocket.CONNECTING].includes(this.ws.readyState)
  176. ) {
  177. this.ws.close(
  178. 1000,
  179. 'Stopping WebSocket connection in ethereumNodeRemote.stop()'
  180. );
  181. }
  182. store.dispatch(resetRemoteNode());
  183. }
  184. async watchBlockHeaders() {
  185. // Unsubscribe before starting
  186. this.unsubscribe();
  187. const requestId = await this.send('eth_subscribe', ['newHeads']);
  188. if (!requestId) {
  189. ethereumNodeRemoteLog.error('No return request id for subscription');
  190. return;
  191. }
  192. const callback = data => {
  193. if (!data) {
  194. return;
  195. }
  196. try {
  197. data = JSON.parse(data);
  198. } catch (error) {
  199. ethereumNodeRemoteLog.trace('Error parsing data: ', data);
  200. }
  201. if (data.id === requestId && data.result) {
  202. this._syncSubscriptionId = data.result;
  203. }
  204. if (
  205. data.params &&
  206. data.params.subscription &&
  207. data.params.subscription === this._syncSubscriptionId &&
  208. data.params.result.number
  209. ) {
  210. store.dispatch(remoteBlockReceived(data.params.result));
  211. }
  212. };
  213. this.ws.on('message', callback);
  214. }
  215. unsubscribe() {
  216. if (this._syncSubscriptionId) {
  217. this.send('eth_unsubscribe', [this._syncSubscriptionId]);
  218. this._syncSubscriptionId = null;
  219. }
  220. }
  221. get connected() {
  222. return this.ws && this.ws.readyState === WebSocket.OPEN;
  223. }
  224. }
  225. module.exports = new EthereumNodeRemote();