|
|
|
import dns from 'dns'
|
|
|
|
import fs from 'fs'
|
|
|
|
import axios from 'axios'
|
|
|
|
import { promisify } from 'util'
|
|
|
|
import { basename, dirname, join, normalize } from 'path'
|
|
|
|
import { platform } from 'os'
|
|
|
|
import { lookup } from 'ps-node'
|
|
|
|
import { app } from 'electron'
|
|
|
|
import isDev from 'electron-is-dev'
|
|
|
|
import grpc from 'grpc'
|
|
|
|
import isIP from 'validator/lib/isIP'
|
|
|
|
import isPort from 'validator/lib/isPort'
|
|
|
|
import get from 'lodash.get'
|
|
|
|
import { mainLog } from '../utils/log'
|
|
|
|
|
|
|
|
const fsReadFile = promisify(fs.readFile)
|
|
|
|
const dnsLookup = promisify(dns.lookup)
|
|
|
|
|
|
|
|
// ------------------------------------
|
|
|
|
// Constants
|
|
|
|
// ------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a path to prepend to any nodejs calls that are getting at files in the package,
|
|
|
|
* so that it works both from source and in an asar-packaged mac app.
|
|
|
|
* See https://github.com/electron-userland/electron-builder/issues/751
|
|
|
|
*
|
|
|
|
* windows from source: "C:\myapp\node_modules\electron\dist\resources\default_app.asar"
|
|
|
|
* mac from source: "/Users/me/dev/myapp/node_modules/electron/dist/Electron.app/Contents/Resources/default_app.asar"
|
|
|
|
* mac from a package: <appRootPathsomewhere>"/my.app/Contents/Resources/app.asar"
|
|
|
|
*
|
|
|
|
* If we are run from outside of a packaged app, our working directory is the right place to be.
|
|
|
|
* And no, we can't just set our working directory to somewhere inside the asar. The OS can't handle that.
|
|
|
|
* @return {String} Path to the lnd binary.
|
|
|
|
*/
|
|
|
|
export const appRootPath =
|
|
|
|
app.getAppPath().indexOf('default_app.asar') < 0 ? normalize(`${app.getAppPath()}/..`) : ''
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the OS specific lnd binary name.
|
|
|
|
* @return {String} 'lnd' on mac or linux, 'lnd.exe' on windows.
|
|
|
|
*/
|
|
|
|
export const binaryName = platform() === 'win32' ? 'lnd.exe' : 'lnd'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the OS specific path to the lnd binary.
|
|
|
|
* @return {String} Path to the lnd binary.
|
|
|
|
*/
|
|
|
|
export const binaryPath = isDev
|
|
|
|
? join(dirname(require.resolve('lnd-binary/package.json')), 'vendor', binaryName)
|
|
|
|
: join(appRootPath, 'bin', binaryName)
|
|
|
|
|
|
|
|
// ------------------------------------
|
|
|
|
// Helpers
|
|
|
|
// ------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to get the current block height.
|
|
|
|
* @return {Number} The current block height.
|
|
|
|
*/
|
|
|
|
export const fetchBlockHeight = () => {
|
|
|
|
const sources = [
|
|
|
|
{
|
|
|
|
baseUrl: `https://testnet-api.smartbit.com.au/v1/blockchain/blocks?limit=1`,
|
|
|
|
path: 'blocks[0].height'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
baseUrl: `https://tchain.api.btc.com/v3/block/latest`,
|
|
|
|
path: 'data.height'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
baseUrl: `https://api.blockcypher.com/v1/btc/test3`,
|
|
|
|
path: 'height'
|
|
|
|
}
|
|
|
|
]
|
|
|
|
const fetchData = (baseUrl, path) => {
|
|
|
|
mainLog.info(`Fetching current block height from ${baseUrl}`)
|
|
|
|
return axios({
|
|
|
|
method: 'get',
|
|
|
|
timeout: 5000,
|
|
|
|
url: baseUrl
|
|
|
|
})
|
|
|
|
.then(response => {
|
|
|
|
const height = Number(get(response.data, path))
|
|
|
|
mainLog.info(`Fetched block height as ${height} from: ${baseUrl}`)
|
|
|
|
return height
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
mainLog.warn(`Unable to fetch block height from ${baseUrl}: ${err.message}`)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return Promise.race(sources.map(source => fetchData(source.baseUrl, source.path)))
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to return an absolute deadline given a relative timeout in seconds.
|
|
|
|
* @param {number} timeoutSecs The number of seconds to wait before timing out
|
|
|
|
* @return {Date} A date timeoutSecs in the future
|
|
|
|
*/
|
|
|
|
export const getDeadline = timeoutSecs => {
|
|
|
|
var deadline = new Date()
|
|
|
|
deadline.setSeconds(deadline.getSeconds() + timeoutSecs)
|
|
|
|
return deadline.getTime()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to check a hostname in the format hostname:port is valid for passing to node-grpc.
|
|
|
|
* @param {string} host A hostname + optional port in the format [hostname]:[port?]
|
|
|
|
* @returns {Promise<Boolean>}
|
|
|
|
*/
|
|
|
|
export const validateHost = async host => {
|
|
|
|
var splits = host.split(':')
|
|
|
|
const lndHost = splits[0]
|
|
|
|
const lndPort = splits[1]
|
|
|
|
|
|
|
|
// If the hostname starts with a number, ensure that it is a valid IP address.
|
|
|
|
if (lndHost.match(/^\d/) && !isIP(lndHost)) {
|
|
|
|
const error = new Error(`${lndHost} is not a valid IP address or hostname`)
|
|
|
|
error.code = 'LND_GRPC_HOST_ERROR'
|
|
|
|
return Promise.reject(error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the host includes a port, ensure that it is a valid.
|
|
|
|
if (lndPort && !isPort(lndPort)) {
|
|
|
|
const error = new Error(`${lndPort} is not a valid port`)
|
|
|
|
error.code = 'LND_GRPC_HOST_ERROR'
|
|
|
|
return Promise.reject(error)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do a DNS lookup to ensure that the host is reachable.
|
|
|
|
return dnsLookup(lndHost)
|
|
|
|
.then(() => true)
|
|
|
|
.catch(e => {
|
|
|
|
const error = new Error(`${lndHost} is not accessible: ${e.message}`)
|
|
|
|
error.code = 'LND_GRPC_HOST_ERROR'
|
|
|
|
return Promise.reject(error)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates and creates the ssl channel credentials from the specified file path
|
|
|
|
* @param {String} certPath
|
|
|
|
* @returns {grpc.ChanelCredentials}
|
|
|
|
*/
|
|
|
|
export const createSslCreds = async certPath => {
|
|
|
|
let lndCert
|
|
|
|
if (certPath) {
|
|
|
|
lndCert = await fsReadFile(certPath).catch(e => {
|
|
|
|
const error = new Error(`SSL cert path could not be accessed: ${e.message}`)
|
|
|
|
error.code = 'LND_GRPC_CERT_ERROR'
|
|
|
|
throw error
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return grpc.credentials.createSsl(lndCert)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates and creates the macaroon authorization credentials from the specified file path
|
|
|
|
* @param {String} macaroonPath
|
|
|
|
* @returns {grpc.CallCredentials}
|
|
|
|
*/
|
|
|
|
export const createMacaroonCreds = async macaroonPath => {
|
|
|
|
const metadata = new grpc.Metadata()
|
|
|
|
|
|
|
|
// If it's not a filepath, then assume it is a hex encoded string.
|
|
|
|
if (macaroonPath === basename(macaroonPath)) {
|
|
|
|
metadata.add('macaroon', macaroonPath)
|
|
|
|
} else {
|
|
|
|
const macaroon = await fsReadFile(macaroonPath).catch(e => {
|
|
|
|
const error = new Error(`Macaroon path could not be accessed: ${e.message}`)
|
|
|
|
error.code = 'LND_GRPC_MACAROON_ERROR'
|
|
|
|
throw error
|
|
|
|
})
|
|
|
|
metadata.add('macaroon', macaroon.toString('hex'))
|
|
|
|
}
|
|
|
|
|
|
|
|
return grpc.credentials.createFromMetadataGenerator((params, callback) =>
|
|
|
|
callback(null, metadata)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check to see if an LND process is running.
|
|
|
|
* @return {Promise} Boolean indicating wether an existing lnd process was found on the host machine.
|
|
|
|
*/
|
|
|
|
export const isLndRunning = () => {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
mainLog.info('Looking for existing lnd process')
|
|
|
|
lookup({ command: 'lnd' }, (err, results) => {
|
|
|
|
// There was an error checking for the LND process.
|
|
|
|
if (err) {
|
|
|
|
return reject(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!results.length) {
|
|
|
|
// An LND process was found, no need to start our own.
|
|
|
|
mainLog.info('Existing lnd process not found')
|
|
|
|
return resolve(false)
|
|
|
|
}
|
|
|
|
mainLog.info('Found existing lnd process')
|
|
|
|
return resolve(true)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|