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.
209 lines
6.1 KiB
209 lines
6.1 KiB
// @flow
|
|
|
|
import grpc from 'grpc'
|
|
import { loadSync } from '@grpc/proto-loader'
|
|
import { BrowserWindow } from 'electron'
|
|
import StateMachine from 'javascript-state-machine'
|
|
import LndConfig from './config'
|
|
import { getDeadline, validateHost, createSslCreds, createMacaroonCreds, waitForFile } from './util'
|
|
import methods from './methods'
|
|
import { mainLog } from '../utils/log'
|
|
import subscribeToTransactions from './subscribe/transactions'
|
|
import subscribeToInvoices from './subscribe/invoices'
|
|
import subscribeToChannelGraph from './subscribe/channelgraph'
|
|
import { getInfo } from './methods/networkController'
|
|
|
|
// Type definition for subscriptions property.
|
|
type LightningSubscriptionsType = {
|
|
channelGraph: any,
|
|
invoices: any,
|
|
transactions: any
|
|
}
|
|
|
|
/**
|
|
* Creates an LND grpc client lightning service.
|
|
* @returns {Lightning}
|
|
*/
|
|
class Lightning {
|
|
mainWindow: BrowserWindow
|
|
service: any
|
|
lndConfig: LndConfig
|
|
subscriptions: LightningSubscriptionsType
|
|
fsm: StateMachine
|
|
|
|
constructor(lndConfig: LndConfig) {
|
|
this.fsm = new StateMachine({
|
|
init: 'ready',
|
|
transitions: [
|
|
{ name: 'connect', from: 'ready', to: 'connected' },
|
|
{ name: 'disconnect', from: 'connected', to: 'ready' },
|
|
{ name: 'terminate', from: 'connected', to: 'ready' }
|
|
],
|
|
methods: {
|
|
onBeforeConnect: this.onBeforeConnect.bind(this),
|
|
onBeforeDisconnect: this.onBeforeDisconnect.bind(this),
|
|
onBeforeTerminate: this.onBeforeTerminate.bind(this)
|
|
}
|
|
})
|
|
|
|
this.mainWindow = null
|
|
this.service = null
|
|
this.lndConfig = lndConfig
|
|
this.subscriptions = {
|
|
channelGraph: null,
|
|
invoices: null,
|
|
transactions: null
|
|
}
|
|
}
|
|
|
|
// ------------------------------------
|
|
// FSM Proxies
|
|
// ------------------------------------
|
|
|
|
connect(...args: any[]) {
|
|
return this.fsm.connect(args)
|
|
}
|
|
disconnect(...args: any[]) {
|
|
return this.fsm.disconnect(args)
|
|
}
|
|
terminate(...args: any[]) {
|
|
return this.fsm.terminate(args)
|
|
}
|
|
is(...args: any[]) {
|
|
return this.fsm.is(args)
|
|
}
|
|
can(...args: any[]) {
|
|
return this.fsm.can(args)
|
|
}
|
|
|
|
// ------------------------------------
|
|
// FSM Callbacks
|
|
// ------------------------------------
|
|
|
|
/**
|
|
* Connect to the gRPC interface and verify it is functional.
|
|
* @return {Promise<rpc.lnrpc.Lightning>}
|
|
*/
|
|
async onBeforeConnect() {
|
|
mainLog.info('Connecting to Lightning gRPC service')
|
|
const { rpcProtoPath, host, cert, macaroon, type } = this.lndConfig
|
|
|
|
// Verify that the host is valid before creating a gRPC client that is connected to it.
|
|
return (
|
|
validateHost(host)
|
|
// If we are trying to connect to the internal lnd, wait upto 20 seconds for the macaroon to be generated.
|
|
.then(() => (type === 'local' ? waitForFile(macaroon, 20000) : Promise.resolve()))
|
|
// Attempt to connect using the supplied credentials.
|
|
.then(async () => {
|
|
// Load the gRPC proto file.
|
|
// The following options object closely approximates the existing behavior of grpc.load.
|
|
// See https://github.com/grpc/grpc-node/blob/master/packages/grpc-protobufjs/README.md
|
|
const options = {
|
|
keepCase: true,
|
|
longs: Number,
|
|
enums: String,
|
|
defaults: true,
|
|
oneofs: true
|
|
}
|
|
const packageDefinition = loadSync(rpcProtoPath, options)
|
|
|
|
// Load gRPC package definition as a gRPC object hierarchy.
|
|
const rpc = grpc.loadPackageDefinition(packageDefinition)
|
|
|
|
// Create ssl and macaroon credentials to use with the gRPC client.
|
|
const [sslCreds, macaroonCreds] = await Promise.all([
|
|
createSslCreds(cert),
|
|
createMacaroonCreds(macaroon)
|
|
])
|
|
const credentials = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds)
|
|
|
|
// Create a new gRPC client instance.
|
|
this.service = new rpc.lnrpc.Lightning(host, credentials)
|
|
|
|
// Wait upto 20 seconds for the gRPC connection to be established.
|
|
return new Promise((resolve, reject) => {
|
|
this.service.waitForReady(getDeadline(20), err => {
|
|
if (err) {
|
|
return reject(err)
|
|
}
|
|
return resolve()
|
|
})
|
|
})
|
|
})
|
|
// Once connected, make a call to getInfo to verify that we can make successful calls.
|
|
.then(() => getInfo(this.service))
|
|
.catch(err => {
|
|
if (this.service) {
|
|
this.service.close()
|
|
}
|
|
throw err
|
|
})
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Discomnnect the gRPC service.
|
|
*/
|
|
onBeforeDisconnect() {
|
|
mainLog.info('Disconnecting from Lightning gRPC service')
|
|
this.unsubscribe()
|
|
if (this.service) {
|
|
this.service.close()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gracefully shutdown the gRPC service.
|
|
*/
|
|
async onBeforeTerminate() {
|
|
mainLog.info('Shutting down Lightning daemon')
|
|
this.unsubscribe()
|
|
return new Promise((resolve, reject) => {
|
|
this.service.stopDaemon({}, (err, data) => {
|
|
if (err) {
|
|
return reject(err)
|
|
}
|
|
resolve(data)
|
|
})
|
|
})
|
|
}
|
|
|
|
// ------------------------------------
|
|
// Helpers
|
|
// ------------------------------------
|
|
|
|
/**
|
|
* Hook up lnd restful methods.
|
|
*/
|
|
registerMethods(event: Event, msg: string, data: any) {
|
|
return methods(this.service, mainLog, event, msg, data)
|
|
}
|
|
|
|
/**
|
|
* Subscribe to all bi-directional streams.
|
|
*/
|
|
subscribe(mainWindow: BrowserWindow) {
|
|
mainLog.info('Subscribing to Lightning gRPC streams')
|
|
this.mainWindow = mainWindow
|
|
|
|
this.subscriptions.channelGraph = subscribeToChannelGraph.call(this)
|
|
this.subscriptions.invoices = subscribeToInvoices.call(this)
|
|
this.subscriptions.transactions = subscribeToTransactions.call(this)
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from all bi-directional streams.
|
|
*/
|
|
unsubscribe() {
|
|
mainLog.info('Unsubscribing from Lightning gRPC streams')
|
|
this.mainWindow = null
|
|
Object.keys(this.subscriptions).forEach(subscription => {
|
|
if (this.subscriptions[subscription]) {
|
|
this.subscriptions[subscription].cancel()
|
|
this.subscriptions[subscription] = null
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
export default Lightning
|
|
|