You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

836 lines
26 KiB

  1. // Copyright (c) 2018-2020, Zpalmtree
  2. //
  3. // Please see the included LICENSE file for more information.
  4. import * as _ from 'lodash';
  5. import request = require('request-promise-native');
  6. import { EventEmitter } from 'events';
  7. import {
  8. Block as UtilsBlock,
  9. Transaction as UtilsTransaction,
  10. TransactionOutputs,
  11. TransactionInputs
  12. } from 'fedoragold-utils';
  13. import * as http from 'http';
  14. import * as https from 'https';
  15. import { assertString, assertNumber, assertBooleanOrUndefined } from './Assert';
  16. import { Config, IConfig, MergeConfig } from './Config';
  17. import { validateAddresses } from './ValidateParameters';
  18. import { LogCategory, logger, LogLevel } from './Logger';
  19. import { SUCCESS, WalletError, WalletErrorCode } from './WalletError';
  20. import {
  21. Block, TopBlock, DaemonConnection, RawCoinbaseTransaction,
  22. RawTransaction, KeyOutput, KeyInput
  23. } from './Types';
  24. export declare interface Daemon {
  25. /**
  26. * This is emitted whenever the interface fails to contact the underlying daemon.
  27. * This event will only be emitted on the first disconnection. It will not
  28. * be emitted again, until the daemon connects, and then disconnects again.
  29. *
  30. * Example:
  31. *
  32. * ```javascript
  33. * daemon.on('disconnect', (error) => {
  34. * console.log('Possibly lost connection to daemon: ' + error.toString());
  35. * });
  36. * ```
  37. *
  38. * @event This is emitted whenever the interface fails to contact the underlying daemon.
  39. */
  40. on(event: 'disconnect', callback: (error: Error) => void): this;
  41. /**
  42. * This is emitted whenever the interface previously failed to contact the
  43. * underlying daemon, and has now reconnected.
  44. * This event will only be emitted on the first connection. It will not
  45. * be emitted again, until the daemon disconnects, and then reconnects again.
  46. *
  47. * Example:
  48. *
  49. * ```javascript
  50. * daemon.on('connect', () => {
  51. * console.log('Regained connection to daemon!');
  52. * });
  53. * ```
  54. *
  55. * @event This is emitted whenever the interface previously failed to contact the underlying daemon, and has now reconnected.
  56. */
  57. on(event: 'connect', callback: () => void): this;
  58. /**
  59. * This is emitted whenever either the localDaemonBlockCount or the networkDaemonBlockCount
  60. * changes.
  61. *
  62. * Example:
  63. *
  64. * ```javascript
  65. * daemon.on('heightchange', (localDaemonBlockCount, networkDaemonBlockCount) => {
  66. * console.log(localDaemonBlockCount, networkDaemonBlockCount);
  67. * });
  68. * ```
  69. *
  70. * @event This is emitted whenever either the localDaemonBlockCount or the networkDaemonBlockCount changes
  71. */
  72. on(event: 'heightchange',
  73. callback: (localDaemonBlockCount: number, networkDaemonBlockCount: number) => void,
  74. ): this;
  75. /**
  76. * This is emitted every time we download a block from the daemon.
  77. *
  78. * This block object is an instance of the [Block fedoragold-utils class](https://utils.turtlecoin.dev/classes/block.html).
  79. * See the Utils docs for further info on using this value.
  80. *
  81. * Note that a block emitted after a previous one could potentially have a lower
  82. * height, if a blockchain fork took place.
  83. *
  84. * Example:
  85. *
  86. * ```javascript
  87. * daemon.on('rawblock', (block) => {
  88. * console.log(`Downloaded new block ${block.hash}`);
  89. * });
  90. * ```
  91. *
  92. * @event This is emitted every time we download a block from the daemon
  93. */
  94. on(event: 'rawblock', callback: (block: UtilsBlock) => void): this;
  95. /**
  96. * This is emitted every time we download a transaction from the daemon.
  97. *
  98. * This transaction object is an instance of the [Transaction fedoragold-utils class](https://utils.turtlecoin.dev/classes/transaction.html).
  99. * See the Utils docs for further info on using this value.
  100. *
  101. * Note that a transaction emitted after a previous one could potentially have a lower
  102. * height in the chain, if a blockchain fork took place.
  103. *
  104. * Example:
  105. *
  106. * ```javascript
  107. * daemon.on('rawtransaction', (block) => {
  108. * console.log(`Downloaded new transaction ${transaction.hash}`);
  109. * });
  110. * ```
  111. *
  112. * @event This is emitted every time we download a transaction from the daemon
  113. */
  114. on(event: 'rawtransaction', callback: (transaction: UtilsTransaction) => void): this;
  115. }
  116. /**
  117. * @noInheritDoc
  118. */
  119. export class Daemon extends EventEmitter {
  120. /**
  121. * Daemon/API host
  122. */
  123. private readonly host: string;
  124. /**
  125. * Daemon/API port
  126. */
  127. private readonly port: number;
  128. /**
  129. * Whether we should use https for our requests
  130. */
  131. private ssl: boolean = true;
  132. /**
  133. * Have we determined if we should be using ssl or not?
  134. */
  135. private sslDetermined: boolean = false;
  136. /**
  137. * The address node fees will go to
  138. */
  139. private feeAddress: string = '';
  140. /**
  141. * The amount of the node fee in atomic units
  142. */
  143. private feeAmount: number = 0;
  144. /**
  145. * The amount of blocks the daemon we're connected to has
  146. */
  147. private localDaemonBlockCount: number = 0;
  148. /**
  149. * The amount of blocks the network has
  150. */
  151. private networkBlockCount: number = 0;
  152. /**
  153. * The amount of peers we have, incoming+outgoing
  154. */
  155. private peerCount: number = 0;
  156. /**
  157. * The hashrate of the last known local block
  158. */
  159. private lastKnownHashrate: number = 0;
  160. /**
  161. * The number of blocks to download per /getwalletsyncdata request
  162. */
  163. private blockCount: number = 100;
  164. private config: Config = new Config();
  165. private httpAgent: http.Agent = new http.Agent({
  166. keepAlive: true,
  167. keepAliveMsecs: 20000,
  168. maxSockets: Infinity,
  169. });
  170. private httpsAgent: https.Agent = new https.Agent({
  171. keepAlive: true,
  172. keepAliveMsecs: 20000,
  173. maxSockets: Infinity,
  174. });
  175. /**
  176. * Last time the network height updated. If this goes over the configured
  177. * limit, we'll emit deadnode.
  178. */
  179. private lastUpdatedNetworkHeight: Date = new Date();
  180. /**
  181. * Last time the daemon height updated. If this goes over the configured
  182. * limit, we'll emit deadnode.
  183. */
  184. private lastUpdatedLocalHeight: Date = new Date();
  185. /**
  186. * Did our last contact with the daemon succeed. Set to true initially
  187. * so initial failure to connect will fire disconnect event.
  188. */
  189. private connected: boolean = true;
  190. private useRawBlocks: boolean = true;
  191. /**
  192. * @param host The host to access the API on. Can be an IP, or a URL, for
  193. * example, 1.1.1.1, or blockapi.turtlepay.io
  194. *
  195. * @param port The port to access the API on. Normally 30157 for a Fedoragold
  196. * daemon, 80 for a HTTP api, or 443 for a HTTPS api.
  197. *
  198. * @param ssl You can optionally specify whether this API supports
  199. * ssl/tls/https to save a couple of requests.
  200. * If you're not sure, do not specify this parameter -
  201. * we will work it out automatically.
  202. */
  203. constructor(host: string, port: number, ssl?: boolean, useRawBlocks?: boolean) {
  204. super();
  205. this.setMaxListeners(0);
  206. assertString(host, 'host');
  207. assertNumber(port, 'port');
  208. assertBooleanOrUndefined(ssl, 'ssl');
  209. assertBooleanOrUndefined(useRawBlocks, 'useRawBlocks');
  210. this.host = host;
  211. this.port = port;
  212. /* Raw IP's very rarely support SSL. This fixes the warning from
  213. https://github.com/nodejs/node/pull/23329 */
  214. if (/^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(this.host) && ssl === undefined) {
  215. ssl = false;
  216. }
  217. if (ssl !== undefined) {
  218. this.ssl = ssl;
  219. this.sslDetermined = true;
  220. }
  221. if (useRawBlocks !== undefined) {
  222. this.useRawBlocks = useRawBlocks;
  223. }
  224. }
  225. public updateConfig(config: IConfig) {
  226. this.config = MergeConfig(config);
  227. this.blockCount = this.config.blocksPerDaemonRequest;
  228. }
  229. /**
  230. * Get the amount of blocks the network has
  231. */
  232. public getNetworkBlockCount(): number {
  233. return this.networkBlockCount;
  234. }
  235. /**
  236. * Get the amount of blocks the daemon we're connected to has
  237. */
  238. public getLocalDaemonBlockCount(): number {
  239. return this.localDaemonBlockCount;
  240. }
  241. /**
  242. * Initialize the daemon and the fee info
  243. */
  244. public async init(): Promise<void> {
  245. /* Note - if one promise throws, the other will be cancelled */
  246. await Promise.all([this.updateDaemonInfo(), this.updateFeeInfo()]);
  247. if (this.networkBlockCount === 0) {
  248. this.emit('deadnode');
  249. }
  250. }
  251. /**
  252. * Update the daemon info
  253. */
  254. public async updateDaemonInfo(): Promise<void> {
  255. let info;
  256. const haveDeterminedSsl = this.sslDetermined;
  257. try {
  258. [info] = await this.makeGetRequest('/info');
  259. } catch (err) {
  260. if (err instanceof Error)
  261. logger.log(
  262. 'Failed to update daemon info: ' + err.toString(),
  263. LogLevel.INFO,
  264. [LogCategory.DAEMON],
  265. );
  266. const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000;
  267. const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000;
  268. if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval
  269. || diff2 > this.config.maxLastUpdatedLocalHeightInterval) {
  270. this.emit('deadnode');
  271. }
  272. return;
  273. }
  274. /* Possibly determined daemon type was HTTPS, got a valid response,
  275. but not valid data. Manually set to http and try again. */
  276. if (info.height === undefined && !haveDeterminedSsl) {
  277. this.sslDetermined = true;
  278. this.ssl = false;
  279. const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000;
  280. const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000;
  281. if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval
  282. || diff2 > this.config.maxLastUpdatedLocalHeightInterval) {
  283. this.emit('deadnode');
  284. }
  285. return this.updateDaemonInfo();
  286. }
  287. if (this.localDaemonBlockCount !== info.height
  288. || this.networkBlockCount !== info.networkHeight) {
  289. this.emit('heightchange', info.height, info.networkHeight);
  290. this.lastUpdatedNetworkHeight = new Date();
  291. this.lastUpdatedLocalHeight = new Date();
  292. } else {
  293. const diff1 = (new Date().getTime() - this.lastUpdatedNetworkHeight.getTime()) / 1000;
  294. const diff2 = (new Date().getTime() - this.lastUpdatedLocalHeight.getTime()) / 1000;
  295. if (diff1 > this.config.maxLastUpdatedNetworkHeightInterval
  296. || diff2 > this.config.maxLastUpdatedLocalHeightInterval) {
  297. this.emit('deadnode');
  298. }
  299. }
  300. this.localDaemonBlockCount = info.height;
  301. this.networkBlockCount = info.networkHeight;
  302. if (this.networkBlockCount > 0) {
  303. this.networkBlockCount--;
  304. }
  305. this.peerCount = info.incomingConnections + info.outgoingConnections;
  306. this.lastKnownHashrate = info.hashrate;
  307. }
  308. /**
  309. * Get the node fee and address
  310. */
  311. public nodeFee(): [string, number] {
  312. return [this.feeAddress, this.feeAmount];
  313. }
  314. /**
  315. * @param blockHashCheckpoints Hashes of the last known blocks. Later
  316. * blocks (higher block height) should be
  317. * ordered at the front of the array.
  318. *
  319. * @param startHeight Height to start taking blocks from
  320. * @param startTimestamp Block timestamp to start taking blocks from
  321. *
  322. * Gets blocks from the daemon. Blocks are returned starting from the last
  323. * known block hash (if higher than the startHeight/startTimestamp)
  324. */
  325. public async getWalletSyncData(
  326. blockHashCheckpoints: string[],
  327. startHeight: number,
  328. startTimestamp: number): Promise<[Block[], TopBlock | boolean]> {
  329. let data;
  330. const endpoint = this.useRawBlocks ? '/sync/raw' : '/sync';
  331. try {
  332. [data] = await this.makePostRequest(endpoint, {
  333. count: this.blockCount,
  334. checkpoints: blockHashCheckpoints,
  335. skipCoinbaseTransactions: !this.config.scanCoinbaseTransactions,
  336. height: startHeight,
  337. timestamp: startTimestamp,
  338. });
  339. } catch (err) {
  340. this.blockCount = Math.ceil(this.blockCount / 4);
  341. if (err instanceof Error)
  342. logger.log(
  343. `Failed to get wallet sync data: ${err.toString()}. Lowering block count to ${this.blockCount}`,
  344. LogLevel.INFO,
  345. [LogCategory.DAEMON],
  346. );
  347. return [[], false];
  348. }
  349. /* The node is not dead if we're fetching blocks. */
  350. if (data.blocks.length >= 0) {
  351. logger.log(
  352. `Fetched ${data.blocks.length} blocks from the daemon`,
  353. LogLevel.DEBUG,
  354. [LogCategory.DAEMON],
  355. );
  356. if (this.blockCount !== this.config.blocksPerDaemonRequest) {
  357. this.blockCount = Math.min(this.config.blocksPerDaemonRequest, this.blockCount * 2);
  358. logger.log(
  359. `Successfully fetched sync data, raising block count to ${this.blockCount}`,
  360. LogLevel.DEBUG,
  361. [LogCategory.DAEMON],
  362. );
  363. }
  364. this.lastUpdatedNetworkHeight = new Date();
  365. this.lastUpdatedLocalHeight = new Date();
  366. }
  367. const blocks = this.useRawBlocks
  368. ? await this.rawBlocksToBlocks(data.blocks)
  369. : data.blocks.map(Block.fromJSON);
  370. if (data.synced && data.topBlock && data.topBlock.height && data.topBlock.hash) {
  371. return [blocks, data.topBlock];
  372. }
  373. return [blocks, true];
  374. }
  375. /**
  376. * @returns Returns a mapping of transaction hashes to global indexes
  377. *
  378. * Get global indexes for the transactions in the range
  379. * [startHeight, endHeight]
  380. */
  381. public async getGlobalIndexesForRange(
  382. startHeight: number,
  383. endHeight: number): Promise<Map<string, number[]>> {
  384. try {
  385. const [data] = await this.makeGetRequest(`/indexes/${startHeight}/${endHeight}`);
  386. const indexes: Map<string, number[]> = new Map();
  387. for (const index of data) {
  388. indexes.set(index.hash, index.indexes);
  389. }
  390. return indexes;
  391. } catch (err) {
  392. if (err instanceof Error)
  393. logger.log(
  394. 'Failed to get global indexes: ' + err.toString(),
  395. LogLevel.ERROR,
  396. LogCategory.DAEMON,
  397. );
  398. return new Map();
  399. }
  400. }
  401. public async getCancelledTransactions(transactionHashes: string[]): Promise<string[]> {
  402. try {
  403. const [data] = await this.makePostRequest('/transaction/status', transactionHashes);
  404. return data.notFound || [];
  405. } catch (err) {
  406. if (err instanceof Error)
  407. logger.log(
  408. 'Failed to get transactions status: ' + err.toString(),
  409. LogLevel.ERROR,
  410. LogCategory.DAEMON,
  411. );
  412. return [];
  413. }
  414. }
  415. /**
  416. * Gets random outputs for the given amounts. requestedOuts per. Usually mixin+1.
  417. *
  418. * @returns Returns an array of amounts to global indexes and keys. There
  419. * should be requestedOuts indexes if the daemon fully fulfilled
  420. * our request.
  421. */
  422. public async getRandomOutputsByAmount(
  423. amounts: number[],
  424. requestedOuts: number): Promise<[number, [number, string][]][]> {
  425. let data;
  426. try {
  427. [data] = await this.makePostRequest('/indexes/random', {
  428. amounts: amounts,
  429. count: requestedOuts,
  430. });
  431. } catch (err) {
  432. if (err instanceof Error)
  433. logger.log(
  434. 'Failed to get random outs: ' + err.toString(),
  435. LogLevel.ERROR,
  436. [LogCategory.TRANSACTIONS, LogCategory.DAEMON],
  437. );
  438. return [];
  439. }
  440. const outputs: [number, [number, string][]][] = [];
  441. for (const output of data) {
  442. const indexes: [number, string][] = [];
  443. for (const outs of output.outputs) {
  444. indexes.push([outs.index, outs.key]);
  445. }
  446. /* Sort by output index to make it hard to determine real one */
  447. outputs.push([output.amount, _.sortBy(indexes, ([index]) => index)]);
  448. }
  449. return outputs;
  450. }
  451. public async sendTransaction(rawTransaction: string): Promise<WalletError> {
  452. try {
  453. const [ result, statusCode ] = await this.makePostRequest('/transaction', rawTransaction);
  454. if (statusCode === 202) {
  455. return SUCCESS;
  456. }
  457. if (result && result.error && result.error.code) {
  458. const code = WalletErrorCode[result.error.code] !== undefined
  459. ? result.error.code
  460. : WalletErrorCode.UNKNOWN_ERROR;
  461. return new WalletError(code, result.error.message);
  462. }
  463. return new WalletError(WalletErrorCode.UNKNOWN_ERROR);
  464. } catch (err) {
  465. if (err instanceof Error)
  466. logger.log(
  467. 'Failed to send transaction: ' + err.toString(),
  468. LogLevel.ERROR,
  469. [LogCategory.TRANSACTIONS, LogCategory.DAEMON],
  470. );
  471. let strErr = "error in sendTransaction";
  472. if (err instanceof Error) strErr = err.toString();
  473. return new WalletError(WalletErrorCode.DAEMON_ERROR, strErr);
  474. }
  475. }
  476. public getConnectionInfo(): DaemonConnection {
  477. return {
  478. host: this.host,
  479. port: this.port,
  480. ssl: this.ssl,
  481. sslDetermined: this.sslDetermined,
  482. };
  483. }
  484. public getConnectionString(): string {
  485. return this.host + ':' + this.port;
  486. }
  487. private async rawBlocksToBlocks(rawBlocks: any): Promise<Block[]> {
  488. const result: Block[] = [];
  489. for (const rawBlock of rawBlocks) {
  490. const block = await UtilsBlock.from(rawBlock.blob, this.config);
  491. this.emit('rawblock', block);
  492. this.emit('rawtransaction', block.minerTransaction);
  493. let coinbaseTransaction: RawCoinbaseTransaction | undefined;
  494. if (this.config.scanCoinbaseTransactions) {
  495. const keyOutputs: KeyOutput[] = [];
  496. for (const output of block.minerTransaction.outputs) {
  497. if (output.type === TransactionOutputs.OutputType.KEY) {
  498. const o = output as TransactionOutputs.KeyOutput;
  499. keyOutputs.push(new KeyOutput(
  500. o.key,
  501. o.amount.toJSNumber(),
  502. ));
  503. }
  504. }
  505. coinbaseTransaction = new RawCoinbaseTransaction(
  506. keyOutputs,
  507. await block.minerTransaction.hash(),
  508. block.minerTransaction.publicKey!,
  509. block.minerTransaction.unlockTime > Number.MAX_SAFE_INTEGER
  510. ? (block.minerTransaction.unlockTime as any).toJSNumber()
  511. : block.minerTransaction.unlockTime,
  512. );
  513. }
  514. const transactions: RawTransaction[] = [];
  515. for (const tx of rawBlock.transactions) {
  516. const rawTX = await UtilsTransaction.from(tx);
  517. this.emit('rawtransaction', tx);
  518. const keyOutputs: KeyOutput[] = [];
  519. const keyInputs: KeyInput[] = [];
  520. for (const output of rawTX.outputs) {
  521. if (output.type === TransactionOutputs.OutputType.KEY) {
  522. const o = output as TransactionOutputs.KeyOutput;
  523. keyOutputs.push(new KeyOutput(
  524. o.key,
  525. o.amount.toJSNumber(),
  526. ));
  527. }
  528. }
  529. for (const input of rawTX.inputs) {
  530. if (input.type === TransactionInputs.InputType.KEY) {
  531. const i = input as TransactionInputs.KeyInput;
  532. keyInputs.push(new KeyInput(
  533. i.amount.toJSNumber(),
  534. i.keyImage,
  535. ));
  536. }
  537. }
  538. transactions.push(new RawTransaction(
  539. keyOutputs,
  540. await rawTX.hash(),
  541. rawTX.publicKey!,
  542. rawTX.unlockTime > Number.MAX_SAFE_INTEGER
  543. ? (rawTX.unlockTime as any).toJSNumber()
  544. : rawTX.unlockTime,
  545. rawTX.paymentId || '',
  546. keyInputs,
  547. ));
  548. }
  549. result.push(new Block(
  550. transactions,
  551. block.height,
  552. await block.hash(),
  553. Math.floor(block.timestamp.getTime() / 1000),
  554. coinbaseTransaction,
  555. ));
  556. }
  557. return result;
  558. }
  559. /**
  560. * Update the fee address and amount
  561. */
  562. private async updateFeeInfo(): Promise<void> {
  563. let feeInfo;
  564. try {
  565. [feeInfo] = await this.makeGetRequest('/fee');
  566. } catch (err) {
  567. if (err instanceof Error)
  568. logger.log(
  569. 'Failed to update fee info: ' + err.toString(),
  570. LogLevel.INFO,
  571. [LogCategory.DAEMON],
  572. );
  573. return;
  574. }
  575. if (feeInfo.address === '') {
  576. return;
  577. }
  578. const integratedAddressesAllowed: boolean = false;
  579. const err: WalletErrorCode = (await validateAddresses(
  580. new Array(feeInfo.address), integratedAddressesAllowed, this.config,
  581. )).errorCode;
  582. if (err !== WalletErrorCode.SUCCESS) {
  583. logger.log(
  584. 'Failed to validate address from daemon fee info: ' + err.toString(),
  585. LogLevel.WARNING,
  586. [LogCategory.DAEMON],
  587. );
  588. return;
  589. }
  590. if (feeInfo.amount > 0) {
  591. this.feeAddress = feeInfo.address;
  592. this.feeAmount = feeInfo.amount;
  593. }
  594. }
  595. private async makeGetRequest(endpoint: string): Promise<[any, number]> {
  596. return this.makeRequest(endpoint, 'GET');
  597. }
  598. private async makePostRequest(endpoint: string, body: any): Promise<[any, number]> {
  599. return this.makeRequest(endpoint, 'POST', body);
  600. }
  601. /**
  602. * Makes a get request to the given endpoint
  603. */
  604. private async makeRequest(endpoint: string, method: string, body?: any): Promise<[any, number]> {
  605. const options = {
  606. body,
  607. headers: { 'User-Agent': this.config.customUserAgentString },
  608. json: true,
  609. method,
  610. timeout: this.config.requestTimeout,
  611. resolveWithFullResponse: true,
  612. };
  613. try {
  614. /* Start by trying HTTPS if we haven't determined whether it's
  615. HTTPS or HTTP yet. */
  616. const protocol = this.sslDetermined ? (this.ssl ? 'https' : 'http') : 'https';
  617. const url: string = `${protocol}://${this.host}:${this.port}${endpoint}`;
  618. logger.log(
  619. `Making request to ${url} with params ${body ? JSON.stringify(body) : '{}'}`,
  620. LogLevel.TRACE,
  621. [LogCategory.DAEMON],
  622. );
  623. const response = await request({
  624. agent: protocol === 'https' ? this.httpsAgent : this.httpAgent,
  625. ...options,
  626. ...this.config.customRequestOptions,
  627. url,
  628. });
  629. /* Cool, https works. Store for later. */
  630. if (!this.sslDetermined) {
  631. this.ssl = true;
  632. this.sslDetermined = true;
  633. }
  634. if (!this.connected) {
  635. this.emit('connect');
  636. this.connected = true;
  637. }
  638. logger.log(
  639. `Got response from ${url} with body ${JSON.stringify(response.body)}`,
  640. LogLevel.TRACE,
  641. [LogCategory.DAEMON],
  642. );
  643. return [response.body, response.statusCode];
  644. } catch (err) {
  645. /* No point trying again with SSL - we already have decided what
  646. type it is. */
  647. if (this.sslDetermined) {
  648. if (this.connected) {
  649. this.emit('disconnect', err);
  650. this.connected = false;
  651. }
  652. throw err;
  653. }
  654. try {
  655. /* Lets try HTTP now. */
  656. const url: string = `http://${this.host}:${this.port}${endpoint}`;
  657. logger.log(
  658. `Making request to ${url} with params ${body ? JSON.stringify(body) : '{}'}`,
  659. LogLevel.TRACE,
  660. [LogCategory.DAEMON],
  661. );
  662. const response = await request({
  663. agent: this.httpAgent,
  664. ...options,
  665. /* Lets try HTTP now. */
  666. url,
  667. });
  668. this.ssl = false;
  669. this.sslDetermined = true;
  670. if (!this.connected) {
  671. this.emit('connect');
  672. this.connected = true;
  673. }
  674. logger.log(
  675. `Got response from ${url} with body ${JSON.stringify(response.body)}`,
  676. LogLevel.TRACE,
  677. [LogCategory.DAEMON],
  678. );
  679. return [response.body, response.statusCode];
  680. } catch (err) {
  681. if (this.connected) {
  682. this.emit('disconnect', err);
  683. this.connected = false;
  684. }
  685. throw err;
  686. }
  687. }
  688. }
  689. }