Merge pull request #282 from shocknet/central-lnd

Central lnd
This commit is contained in:
Daniel Lugo 2021-01-14 15:42:45 -04:00 committed by GitHub
commit 831ca1c77d
5 changed files with 403 additions and 184 deletions

33
.vscode/snippets.code-snippets vendored Normal file
View file

@ -0,0 +1,33 @@
{
// Place your api workspace snippets here. Each snippet is defined under a
// snippet name and has a scope, prefix, body and description. Add comma
// separated ids of the languages where the snippet is applicable in the scope
// field. If scope is left empty or omitted, the snippet gets applied to all
// languages. The prefix is what is used to trigger the snippet and the body
// will be expanded and inserted. Possible variables are: $1, $2 for tab
// stops, $0 for the final cursor position, and ${1:label}, ${2:another} for
// placeholders. Placeholders with the same ids are connected. Example: "Print
// to console": {"scope": "javascript,typescript", "prefix": "log", "body":
// ["console.log('$1');", "$2"
// ],
// "description": "Log output to console"
// }
"Route Body": {
"body": [
"try {",
" return res.json({",
"",
" })",
"} catch (e) {",
" console.log(e)",
" return res.status(500).json({",
" errorMessage: e.message",
" })",
"}"
],
"description": "Route Body",
"prefix": "rbody",
"scope": "javascript"
}
}

View file

@ -30,11 +30,7 @@ const {
const GunActions = require('../services/gunDB/contact-api/actions') const GunActions = require('../services/gunDB/contact-api/actions')
const GunGetters = require('../services/gunDB/contact-api/getters') const GunGetters = require('../services/gunDB/contact-api/getters')
const GunKey = require('../services/gunDB/contact-api/key') const GunKey = require('../services/gunDB/contact-api/key')
const { const LV2 = require('../utils/lightningServices/v2')
sendPaymentV2Keysend,
sendPaymentV2Invoice,
listPayments
} = require('../utils/lightningServices/v2')
const GunWriteRPC = require('../services/gunDB/rpc') const GunWriteRPC = require('../services/gunDB/rpc')
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
@ -1024,30 +1020,15 @@ module.exports = async (
) )
}) })
// get lnd chan info // get lnd chan info
app.post('/api/lnd/getchaninfo', (req, res) => { app.post('/api/lnd/getchaninfo', async (req, res) => {
const { lightning } = LightningServices.services try {
return res.json(await LV2.getChanInfo(req.body.chan_id))
lightning.getChanInfo( } catch (e) {
{ chan_id: req.body.chan_id }, console.log(e)
async (err, response) => { return res.status(500).json({
if (err) { errorMessage: e.message
logger.debug('GetChanInfo Error:', err) })
const health = await checkHealth() }
if (health.LNDStatus.success) {
res.status(400)
res.json({
field: 'getChanInfo',
errorMessage: sanitizeLNDError(err.message)
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('GetChanInfo:', response)
res.json(response)
}
)
}) })
app.get('/api/lnd/getnetworkinfo', (req, res) => { app.get('/api/lnd/getnetworkinfo', (req, res) => {
@ -1072,47 +1053,30 @@ module.exports = async (
}) })
// get lnd node active channels list // get lnd node active channels list
app.get('/api/lnd/listpeers', (req, res) => { app.get('/api/lnd/listpeers', async (req, res) => {
const { lightning } = LightningServices.services try {
lightning.listPeers({}, async (err, response) => { return res.json({
if (err) { peers: await LV2.listPeers(req.body.latestError)
logger.debug('ListPeers Error:', err) })
const health = await checkHealth() } catch (e) {
if (health.LNDStatus.success) { console.log(e)
res.status(400).json({ return res.status(500).json({
field: 'listPeers', errorMessage: e.message
errorMessage: sanitizeLNDError(err.message) })
}) }
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('ListPeers:', response)
res.json(response)
})
}) })
// newaddress // newaddress
app.post('/api/lnd/newaddress', (req, res) => { app.post('/api/lnd/newaddress', async (req, res) => {
const { lightning } = LightningServices.services try {
lightning.newAddress({ type: req.body.type }, async (err, response) => { return res.json({
if (err) { address: await LV2.newAddress(req.body.type)
logger.debug('NewAddress Error:', err) })
const health = await checkHealth() } catch (e) {
if (health.LNDStatus.success) { return res.status(500).json({
res.status(400).json({ errorMessage: e.message
field: 'newAddress', })
errorMessage: sanitizeLNDError(err.message) }
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('NewAddress:', response)
res.json(response)
})
}) })
// connect peer to lnd node // connect peer to lnd node
@ -1157,47 +1121,28 @@ module.exports = async (
}) })
// get lnd node opened channels list // get lnd node opened channels list
app.get('/api/lnd/listchannels', (req, res) => { app.get('/api/lnd/listchannels', async (_, res) => {
const { lightning } = LightningServices.services try {
lightning.listChannels({}, async (err, response) => { return res.json({
if (err) { channels: await LV2.listChannels()
logger.debug('ListChannels Error:', err) })
const health = await checkHealth() } catch (e) {
if (health.LNDStatus.success) { console.log(e)
res.status(400).json({ return res.status(500).json({
field: 'listChannels', errorMessage: e.message
errorMessage: sanitizeLNDError(err.message) })
}) }
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('ListChannels:', response)
res.json(response)
})
}) })
// get lnd node pending channels list app.get('/api/lnd/pendingchannels', async (req, res) => {
app.get('/api/lnd/pendingchannels', (req, res) => { try {
const { lightning } = LightningServices.services return res.json(await LV2.pendingChannels())
lightning.pendingChannels({}, async (err, response) => { } catch (e) {
if (err) { console.log(e)
logger.debug('PendingChannels Error:', err) return res.status(500).json({
const health = await checkHealth() errorMessage: e.message
if (health.LNDStatus.success) { })
res.status(400).json({ }
field: 'pendingChannels',
errorMessage: sanitizeLNDError(err.message)
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('PendingChannels:', response)
res.json(response)
})
}) })
app.get('/api/lnd/unifiedTrx', (req, res) => { app.get('/api/lnd/unifiedTrx', (req, res) => {
@ -1375,7 +1320,7 @@ module.exports = async (
} }
return res.status(200).json( return res.status(200).json(
await listPayments({ await LV2.listPayments({
include_incomplete, include_incomplete,
index_offset, index_offset,
max_payments, max_payments,
@ -1662,7 +1607,7 @@ module.exports = async (
}) })
} }
const payment = await sendPaymentV2Keysend({ const payment = await LV2.sendPaymentV2Keysend({
amt, amt,
dest, dest,
feeLimit, feeLimit,
@ -1675,7 +1620,7 @@ module.exports = async (
} }
const { payreq } = req.body const { payreq } = req.body
const payment = await sendPaymentV2Invoice({ const payment = await LV2.sendPaymentV2Invoice({
feeLimit, feeLimit,
payment_request: payreq, payment_request: payreq,
amt: req.body.amt, amt: req.body.amt,
@ -1764,64 +1709,32 @@ module.exports = async (
}) })
// addinvoice // addinvoice
app.post('/api/lnd/addinvoice', (req, res) => { app.post('/api/lnd/addinvoice', async (req, res) => {
const { lightning } = LightningServices.services const { expiry, value, memo } = req.body
const invoiceRequest = { memo: req.body.memo, private: true } const addInvoiceRes = await LV2.addInvoice(value, memo, true, expiry)
if (req.body.value) {
invoiceRequest.value = req.body.value if (value) {
} const channelsList = await LV2.listChannels({ active_only: true })
if (req.body.expiry) { let remoteBalance = Big(0)
invoiceRequest.expiry = req.body.expiry channelsList.forEach(element => {
} const remB = Big(element.remote_balance)
lightning.addInvoice(invoiceRequest, async (err, newInvoice) => { if (remB.gt(remoteBalance)) {
if (err) { remoteBalance = remB
logger.debug('AddInvoice Error:', err)
const health = await checkHealth()
if (health.LNDStatus.success) {
res.status(400).json({
field: 'addInvoice',
errorMessage: sanitizeLNDError(err.message)
})
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
} }
return err })
}
logger.debug('AddInvoice:', newInvoice) addInvoiceRes.liquidityCheck = remoteBalance > value
if (req.body.value) { //newInvoice.remoteBalance = remoteBalance
logger.debug('AddInvoice liquidity check:') }
lightning.listChannels({ active_only: true }, async (err, response) => {
if (err) { try {
logger.debug('ListChannels Error:', err) return res.json(addInvoiceRes)
const health = await checkHealth() } catch (e) {
if (health.LNDStatus.success) { console.log(e)
res.status(400).json({ return res.status(500).json({
field: 'listChannels', errorMessage: e.message
errorMessage: sanitizeLNDError(err.message) })
}) }
} else {
res.status(500)
res.json({ errorMessage: 'LND is down' })
}
}
logger.debug('ListChannels:', response)
const channelsList = response.channels
let remoteBalance = Big(0)
channelsList.forEach(element => {
const remB = Big(element.remote_balance)
if (remB.gt(remoteBalance)) {
remoteBalance = remB
}
})
newInvoice.liquidityCheck = remoteBalance > req.body.value
//newInvoice.remoteBalance = remoteBalance
res.json(newInvoice)
})
} else {
res.json(newInvoice)
}
})
}) })
// signmessage // signmessage
@ -1959,23 +1872,25 @@ module.exports = async (
) )
}) })
app.post('/api/lnd/listunspent', (req, res) => { const listunspent = async (req, res) => {
const { lightning } = LightningServices.services try {
const { minConfirmations = 3, maxConfirmations = 6 } = req.body return res.status(200).json({
lightning.listUnspent( utxos: await LV2.listUnspent(
{ req.body.minConfirmations,
min_confs: minConfirmations, req.body.maxConfirmations
max_confs: maxConfirmations )
}, })
(err, unspent) => { } catch (e) {
if (err) { return res.status(500).json({
return handleError(res, err) errorMessage: e.message
} })
logger.debug('ListUnspent:', unspent) }
res.json(unspent) }
}
) app.get('/api/lnd/listunspent', listunspent)
})
// TODO: should be GET
app.post('/api/lnd/listunspent', listunspent)
app.get('/api/lnd/transactions', (req, res) => { app.get('/api/lnd/transactions', (req, res) => {
const { lightning } = LightningServices.services const { lightning } = LightningServices.services

View file

@ -74,6 +74,13 @@ class LightningServices {
} }
} }
/**
* @returns {import('./types').Services}
*/
getServices() {
return this.services
}
get servicesData() { get servicesData() {
return this.lnServicesData return this.lnServicesData
} }

View file

@ -1,6 +1,7 @@
/** /**
* @format * @format
*/ */
import * as Common from 'shock-common'
export interface PaymentV2 { export interface PaymentV2 {
payment_hash: string payment_hash: string
@ -106,3 +107,90 @@ export interface SendPaymentInvoiceParams {
payment_request: string payment_request: string
timeoutSeconds?: number timeoutSeconds?: number
} }
type StreamListener = (data: any) => void
/**
* Caution: Not all methods return an stream.
*/
interface LightningStream {
on(ev: 'data' | 'end' | 'error' | 'status', listener: StreamListener): void
}
type LightningCB = (err: Error, data: Record<string, any>) => void
type LightningMethod = (
args: Record<string, any>,
cb?: LightningCB
) => LightningStream
/**
* Makes it easier for code calling services.
*/
export interface Services {
lightning: Record<string, LightningMethod>
walletUnlocker: Record<string, LightningMethod>
router: Record<string, LightningMethod>
}
export interface ListChannelsReq {
active_only: boolean
inactive_only: boolean
public_only: boolean
private_only: boolean
/**
* Filters the response for channels with a target peer's pubkey. If peer is
* empty, all channels will be returned.
*/
peer: Common.Bytes
}
/**
* https://api.lightning.community/#pendingchannels
*/
export interface PendingChannelsRes {
/**
* The balance in satoshis encumbered in pending channels.
*/
total_limbo_balance: string
/**
* Channels pending opening.
*/
pending_open_channels: Common.PendingOpenChannel[]
/**
* Channels pending force closing.
*/
pending_force_closing_channels: Common.ForceClosedChannel[]
/**
* Channels waiting for closing tx to confirm.
*/
waiting_close_channels: Common.WaitingCloseChannel[]
}
/**
* https://github.com/lightningnetwork/lnd/blob/daf7c8a85420fc67fffa18fa5f7d08c2040946e4/lnrpc/rpc.proto#L2948
*/
export interface AddInvoiceRes {
/**
*
*/
r_hash: Common.Bytes
/**
* A bare-bones invoice for a payment within the Lightning Network. With the
* details of the invoice, the sender has all the data necessary to send a
* payment to the recipient.
*/
payment_request: string
/**
* The "add" index of this invoice. Each newly created invoice will increment
* this index making it monotonically increasing. Callers to the
* SubscribeInvoices call can use this to instantly get notified of all added
* invoices with an add_index greater than this one.
*/
add_index: string
/**
* The payment address of the generated invoice. This value should be used in
* all payments for this invoice as we require it for end to end security.
*/
payment_addr: Common.Bytes
}

View file

@ -402,9 +402,185 @@ const decodePayReq = payReq =>
) )
}) })
/**
* @param {0|1} type
* @returns {Promise<string>}
*/
const newAddress = (type = 0) => {
const { lightning } = lightningServices.getServices()
return Common.Utils.makePromise((res, rej) => {
lightning.newAddress({ type }, (err, response) => {
if (err) {
rej(new Error(err.message))
} else {
res(response.address)
}
})
})
}
/**
* @param {number} minConfs
* @param {number} maxConfs
* @returns {Promise<Common.Utxo[]>}
*/
const listUnspent = (minConfs = 3, maxConfs = 6) =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listUnspent(
{
min_confs: minConfs,
max_confs: maxConfs
},
(err, unspent) => {
if (err) {
rej(new Error(err.message))
} else {
res(unspent.utxos)
}
}
)
})
/**
* @typedef {import('./types').ListChannelsReq} ListChannelsReq
*/
/**
* @param {ListChannelsReq} req
* @returns {Promise<Common.Channel[]>}
*/
const listChannels = req =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listChannels(req, (err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
res(resp.channels)
}
})
})
/**
* https://api.lightning.community/#getchaninfo
* @param {string} chanID
* @returns {Promise<Common.ChannelEdge>}
*/
const getChanInfo = chanID =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.getChanInfo(
{
chan_id: chanID
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {Common.ChannelEdge} */ (resp))
}
}
)
})
/**
* https://api.lightning.community/#listpeers
* @param {boolean=} latestError If true, only the last error that our peer sent
* us will be returned with the peer's information, rather than the full set of
* historic errors we have stored.
* @returns {Promise<Common.Peer[]>}
*/
const listPeers = latestError =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.listPeers(
{
latest_error: latestError
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
res(resp.peers)
}
}
)
})
/**
* @typedef {import('./types').PendingChannelsRes} PendingChannelsRes
*/
/**
* @returns {Promise<PendingChannelsRes>}
*/
const pendingChannels = () =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.pendingChannels({}, (err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {PendingChannelsRes} */ (resp))
}
})
})
/**
* @typedef {import('./types').AddInvoiceRes} AddInvoiceRes
*/
/**
* https://api.lightning.community/#addinvoice
* @param {number} value
* @param {string=} memo
* @param {boolean=} confidential Alias for `private`.
* @param {number=} expiry
* @returns {Promise<AddInvoiceRes>}
*/
const addInvoice = (value, memo = '', confidential = true, expiry = 180) =>
Common.makePromise((res, rej) => {
const { lightning } = lightningServices.getServices()
lightning.addInvoice(
{
value,
memo,
private: confidential,
expiry
},
(err, resp) => {
if (err) {
rej(new Error(err.message))
} else {
// Needs cast because typescript refuses to assign Record<string, any>
// to an actual object :shrugs
res(/** @type {AddInvoiceRes} */ (resp))
}
}
)
})
module.exports = { module.exports = {
sendPaymentV2Keysend, sendPaymentV2Keysend,
sendPaymentV2Invoice, sendPaymentV2Invoice,
listPayments, listPayments,
decodePayReq decodePayReq,
newAddress,
listUnspent,
listChannels,
getChanInfo,
listPeers,
pendingChannels,
addInvoice
} }