├── .nvmrc ├── .node-version ├── .npmrc ├── src ├── grpc-utils │ ├── index.js │ ├── deadline.js │ └── deadline.spec.js ├── constants │ ├── invoice-states.js │ ├── index.js │ ├── channel-rounding.js │ └── engine-statuses.js ├── utils │ ├── big.js │ ├── delay.js │ ├── loggable-pubkey.js │ ├── index.js │ ├── sha256.js │ ├── loggable-pubkey.spec.js │ ├── sha256.spec.js │ ├── index.spec.js │ ├── delay.spec.js │ ├── load-proto.js │ ├── load-proto.spec.js │ ├── network-address-formatter.spec.js │ └── network-address-formatter.js ├── engine-actions │ ├── cancel-swap.js │ ├── get-public-key.js │ ├── settle-swap.js │ ├── get-uncommitted-balance.js │ ├── get-confirmed-balance.js │ ├── get-unconfirmed-balance.js │ ├── connect-user.js │ ├── unlock-wallet.js │ ├── get-invoices.js │ ├── create-invoice.js │ ├── get-peers.js │ ├── get-invoice-value.js │ ├── num-channels-for-address.js │ ├── get-invoice.js │ ├── get-total-channel-balance.js │ ├── change-wallet-password.js │ ├── withdraw-funds.js │ ├── get-total-pending-channel-balance.js │ ├── pay-invoice.js │ ├── create-new-address.js │ ├── is-invoice-paid.js │ ├── get-total-balance-for-address.js │ ├── get-public-key.spec.js │ ├── get-pending-channel-capacities.js │ ├── create-new-address.spec.js │ ├── get-confirmed-balance.spec.js │ ├── get-uncommitted-balance.spec.js │ ├── get-unconfirmed-balance.spec.js │ ├── create-swap-hash.js │ ├── settle-swap.spec.js │ ├── get-settled-swap-preimage.js │ ├── get-invoice-value.spec.js │ ├── get-payment-channel-network-address.js │ ├── get-total-reserved-channel-balance.js │ ├── unlock-wallet.spec.js │ ├── create-swap-hash.spec.js │ ├── withdraw-funds.spec.js │ ├── create-invoice.spec.js │ ├── create-wallet.js │ ├── get-uncommitted-pending-balance.js │ ├── get-total-channel-balance.spec.js │ ├── pay-invoice.spec.js │ ├── connect-user.spec.js │ ├── get-total-pending-channel-balance.spec.js │ ├── get-channels-for-remote-address.js │ ├── get-peers.spec.js │ ├── get-max-channel-for-address.js │ ├── create-refund-invoice.js │ ├── num-channels-for-address.spec.js │ ├── get-pending-channel-capacities.spec.js │ ├── get-invoices.spec.js │ ├── get-open-channel-capacities.js │ ├── get-open-channel-capacities.spec.js │ ├── get-payment-channel-network-address.spec.js │ ├── is-invoice-paid.spec.js │ ├── change-wallet-password.spec.js │ ├── get-max-channel.js │ ├── is-balance-sufficient.js │ ├── get-total-reserved-channel-balance.spec.js │ ├── create-wallet.spec.js │ ├── get-uncommitted-pending-balance.spec.js │ ├── recover-wallet.js │ ├── create-refund-invoice.spec.js │ ├── initiate-swap.js │ ├── prepare-swap.spec.js │ ├── get-total-balance-for-address.spec.js │ ├── initiate-swap.spec.js │ ├── get-max-channel-for-address.spec.js │ ├── get-settled-swap-preimage.spec.js │ ├── prepare-swap.js │ ├── wait-for-swap-commitment.js │ ├── get-channels-for-remote-address.spec.js │ ├── get-channels.js │ ├── get-max-channel.spec.js │ ├── wait-for-swap-commitment.spec.js │ ├── close-channels.js │ ├── index.js │ └── get-status.js ├── lnd-actions │ ├── send-to-route.js │ ├── subscribe-single-invoice.js │ ├── list-peers.js │ ├── get-info.js │ ├── list-channels.js │ ├── cancel-invoice.js │ ├── list-payments.js │ ├── wallet-balance.js │ ├── gen-seed.js │ ├── list-closed-channels.js │ ├── settle-invoice.js │ ├── list-invoices.js │ ├── get-transactions.js │ ├── lookup-invoice.js │ ├── send-coins.js │ ├── close-channel.js │ ├── unlock-wallet.js │ ├── new-address.js │ ├── decode-payment-request.js │ ├── describe-graph.js │ ├── update-channel-policy.js │ ├── change-password.js │ ├── list-pending-channels.js │ ├── add-hold-invoice.js │ ├── lookup-payment-status.js │ ├── add-invoice.js │ ├── open-channel.js │ ├── query-routes.js │ ├── track-payment.js │ ├── connect-peer.js │ ├── init-wallet.js │ ├── send-payment.js │ └── index.js ├── lnd-setup │ ├── generate-wallet-unlocker-client.js │ ├── generate-wallet-unlocker-client.spec.js │ ├── index.js │ └── generate-lightning-client.js └── config.json ├── docker ├── README.md ├── lnd │ ├── conf │ │ ├── lnd-bitcoind.conf │ │ └── lnd-litecoind.conf │ ├── start-lnd-btc.sh │ ├── start-lnd-ltc.sh │ └── Dockerfile ├── bitcoind │ └── Dockerfile └── litecoind │ └── Dockerfile ├── .jsdoc.json ├── test └── test-helper.js ├── scripts ├── check-unused.js └── build.sh ├── .eslintrc ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── proto └── invoicesrpc └── invoices.proto /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.11 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 8.11 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /src/grpc-utils/index.js: -------------------------------------------------------------------------------- 1 | const deadline = require('./deadline') 2 | 3 | module.exports = { deadline } 4 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## LND ENGINE DOCKER FILES 2 | 3 | This directory contains all files needed to setup the LND Engine with a SparkSwap Broker 4 | -------------------------------------------------------------------------------- /src/constants/invoice-states.js: -------------------------------------------------------------------------------- 1 | 2 | // lightning invoice states from rpc.proto 3 | // note: proto-loader is configured to convert enums to strings 4 | module.exports = { 5 | OPEN: 'OPEN', 6 | SETTLED: 'SETTLED', 7 | CANCELED: 'CANCELED', 8 | ACCEPTED: 'ACCEPTED' 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/big.js: -------------------------------------------------------------------------------- 1 | const Big = require('big.js') 2 | 3 | /** 4 | * We want to use "bankers' rounding" 5 | * @see {@link https://en.wikipedia.org/wiki/Rounding#Round_half_to_even} 6 | * @see {@link http://mikemcl.github.io/big.js/#rm} 7 | */ 8 | Big.RM = 2 9 | 10 | module.exports = Big 11 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | const ENGINE_STATUSES = require('./engine-statuses') 2 | const CHANNEL_ROUNDING = require('./channel-rounding') 3 | const INVOICE_STATES = require('./invoice-states') 4 | 5 | module.exports = { 6 | ENGINE_STATUSES, 7 | CHANNEL_ROUNDING, 8 | INVOICE_STATES 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/delay.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevents code execution for a designated amount of milliseconds 3 | * 4 | * @param {number} ms - milleseconds of delay 5 | * @returns {Promise} 6 | */ 7 | function delay (ms) { 8 | return new Promise((resolve, reject) => { 9 | void reject 10 | setTimeout(resolve, ms) 11 | }) 12 | } 13 | 14 | module.exports = delay 15 | -------------------------------------------------------------------------------- /src/engine-actions/cancel-swap.js: -------------------------------------------------------------------------------- 1 | const { cancelInvoice } = require('../lnd-actions') 2 | 3 | /** 4 | * Cancels the invoice for a swap 5 | * 6 | * @param {string} swapHash - hash of the swap 7 | * @returns {Promise} 8 | */ 9 | async function cancelSwap (swapHash) { 10 | return cancelInvoice(swapHash, { client: this.client }) 11 | } 12 | 13 | module.exports = cancelSwap 14 | -------------------------------------------------------------------------------- /src/utils/loggable-pubkey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Resizes an LND pubkey to log 3 | * 4 | * @param {string} pubKey - LND identity pubkey 5 | * @returns {?string} string snippet of public key (length 15) 6 | */ 7 | function loggablePubKey (pubKey) { 8 | if (!pubKey) { 9 | return null 10 | } 11 | return `${pubKey.slice(0, 15)}...` 12 | } 13 | 14 | module.exports = loggablePubKey 15 | -------------------------------------------------------------------------------- /src/engine-actions/get-public-key.js: -------------------------------------------------------------------------------- 1 | const { getInfo } = require('../lnd-actions') 2 | 3 | /** 4 | * Returns the lnd instance's public key 5 | * 6 | * @returns {Promise} identityPubkey 7 | */ 8 | async function getPublicKey () { 9 | const { identityPubkey } = await getInfo({ client: this.client }) 10 | return identityPubkey 11 | } 12 | 13 | module.exports = getPublicKey 14 | -------------------------------------------------------------------------------- /src/engine-actions/settle-swap.js: -------------------------------------------------------------------------------- 1 | const { settleInvoice } = require('../lnd-actions') 2 | 3 | /** 4 | * Settles the invoice for a swap 5 | * 6 | * @param {string} preimage - base64 preimage of the swap hash 7 | * @returns {Promise} 8 | */ 9 | async function settleSwap (preimage) { 10 | return settleInvoice(preimage, { client: this.client }) 11 | } 12 | 13 | module.exports = settleSwap 14 | -------------------------------------------------------------------------------- /src/engine-actions/get-uncommitted-balance.js: -------------------------------------------------------------------------------- 1 | const { walletBalance } = require('../lnd-actions') 2 | 3 | /** 4 | * Total balance of unspent funds 5 | * @returns {Promise} total 6 | */ 7 | async function getUncommittedBalance () { 8 | const { confirmedBalance } = await walletBalance({ client: this.client }) 9 | return confirmedBalance 10 | } 11 | 12 | module.exports = getUncommittedBalance 13 | -------------------------------------------------------------------------------- /src/engine-actions/get-confirmed-balance.js: -------------------------------------------------------------------------------- 1 | const { walletBalance } = require('../lnd-actions') 2 | 3 | /** 4 | * Balance of confirmed unspent funds 5 | * 6 | * @returns {Promise} total 7 | */ 8 | async function getConfirmedBalance () { 9 | const { confirmedBalance } = await walletBalance({ client: this.client }) 10 | return confirmedBalance 11 | } 12 | 13 | module.exports = getConfirmedBalance 14 | -------------------------------------------------------------------------------- /src/engine-actions/get-unconfirmed-balance.js: -------------------------------------------------------------------------------- 1 | const { walletBalance } = require('../lnd-actions') 2 | 3 | /** 4 | * Balance of unconfirmed unspent funds 5 | * @returns {Promise} total 6 | */ 7 | async function getUnconfirmedBalance () { 8 | const { unconfirmedBalance } = await walletBalance({ client: this.client }) 9 | return unconfirmedBalance 10 | } 11 | 12 | module.exports = getUnconfirmedBalance 13 | -------------------------------------------------------------------------------- /src/engine-actions/connect-user.js: -------------------------------------------------------------------------------- 1 | const { connectPeer } = require('../lnd-actions') 2 | const { networkAddressFormatter } = require('../utils') 3 | 4 | function connectUser (paymentChannelNetworkAddress) { 5 | const { publicKey, host } = networkAddressFormatter.parse(paymentChannelNetworkAddress) 6 | return connectPeer(publicKey, host, { client: this.client, logger: this.logger }) 7 | } 8 | 9 | module.exports = connectUser 10 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const Big = require('./big') 2 | const networkAddressFormatter = require('./network-address-formatter') 3 | const sha256 = require('./sha256') 4 | const loadProto = require('./load-proto') 5 | const delay = require('./delay') 6 | const loggablePubKey = require('./loggable-pubkey') 7 | 8 | module.exports = { 9 | Big, 10 | networkAddressFormatter, 11 | sha256, 12 | loadProto, 13 | delay, 14 | loggablePubKey 15 | } 16 | -------------------------------------------------------------------------------- /src/engine-actions/unlock-wallet.js: -------------------------------------------------------------------------------- 1 | const { 2 | unlockWallet: lndUnlockWallet 3 | } = require('../lnd-actions') 4 | 5 | /** 6 | * Unlocks an engine's wallet 7 | * 8 | * @param {string} password - wallet password 9 | * @returns {Promise} 10 | */ 11 | function unlockWallet (password) { 12 | const walletPassword = Buffer.from(password, 'utf8') 13 | return lndUnlockWallet(walletPassword, { client: this.walletUnlocker }) 14 | } 15 | 16 | module.exports = unlockWallet 17 | -------------------------------------------------------------------------------- /src/utils/sha256.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require('crypto') 2 | 3 | /** 4 | * Generate a hash from a preimage 5 | * @param {string} preimage - Base64 string of the preimage data 6 | * @returns {string} Base64 string of the derived hash 7 | */ 8 | function hash (preimage) { 9 | const sha256 = createHash('sha256') 10 | const preimageBuf = Buffer.from(preimage, 'base64') 11 | return sha256.update(preimageBuf).digest('base64') 12 | } 13 | 14 | module.exports = { 15 | hash 16 | } 17 | -------------------------------------------------------------------------------- /src/engine-actions/get-invoices.js: -------------------------------------------------------------------------------- 1 | const { listInvoices } = require('../lnd-actions') 2 | 3 | /** 4 | * Returns a list of all invoices on the engine instance 5 | * 6 | * @param {object} [args={}] 7 | * @param {boolean} [args.pendingOnly=false] - if we return ONLY pending invoices 8 | * @returns {Promise} 9 | */ 10 | async function getInvoices ({ pendingOnly = false } = {}) { 11 | return listInvoices(pendingOnly, { client: this.client }) 12 | } 13 | 14 | module.exports = getInvoices 15 | -------------------------------------------------------------------------------- /src/engine-actions/create-invoice.js: -------------------------------------------------------------------------------- 1 | const { addInvoice } = require('../lnd-actions') 2 | 3 | /** 4 | * Creates an invoice 5 | * 6 | * @param {string} memo 7 | * @param {string} expiry - in seconds 8 | * @param {string} value 9 | * @returns {Promise} paymentRequest hash of invoice from lnd 10 | */ 11 | async function createInvoice (memo, expiry, value) { 12 | const { paymentRequest } = await addInvoice({ memo, expiry, value }, { client: this.client }) 13 | return paymentRequest 14 | } 15 | 16 | module.exports = createInvoice 17 | -------------------------------------------------------------------------------- /src/lnd-actions/send-to-route.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 2 | /** @typedef {import('grpc').ClientReadableStream} ClientReadableStream */ 3 | 4 | /** 5 | * Sends to a specified route 6 | * 7 | * @see https://github.com/lightningnetwork/lnd/blob/master/lnrpc/rpc.proto#L422 8 | * @param {object} opts 9 | * @param {LndClient} opts.client 10 | * @returns {ClientReadableStream} 11 | */ 12 | function sendToRoute ({ client }) { 13 | return client.sendToRoute({}) 14 | } 15 | 16 | module.exports = sendToRoute 17 | -------------------------------------------------------------------------------- /src/engine-actions/get-peers.js: -------------------------------------------------------------------------------- 1 | const { listPeers } = require('../lnd-actions') 2 | 3 | /** 4 | * Gets all currently active peers connected to a specific engine 5 | * 6 | * @returns {Promise>} list of peers with pubkey, address and inbound flag 7 | */ 8 | async function getPeers () { 9 | const { peers = [] } = await listPeers({ client: this.client }) 10 | 11 | return peers.map(peer => { 12 | return { 13 | pubKey: peer.pubKey, 14 | address: peer.address 15 | } 16 | }) 17 | } 18 | module.exports = getPeers 19 | -------------------------------------------------------------------------------- /src/engine-actions/get-invoice-value.js: -------------------------------------------------------------------------------- 1 | const { decodePaymentRequest } = require('../lnd-actions') 2 | 3 | /** 4 | * Returns an object with decoded paymentHash and numSatoshis of a payment request 5 | * 6 | * @param {string} paymentRequestString - request string to be decoded 7 | * @returns {Promise} 8 | */ 9 | async function getInvoiceValue (paymentRequestString) { 10 | const { numSatoshis } = await decodePaymentRequest(paymentRequestString, { client: this.client }) 11 | return numSatoshis 12 | } 13 | 14 | module.exports = getInvoiceValue 15 | -------------------------------------------------------------------------------- /src/utils/loggable-pubkey.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('test/test-helper') 2 | 3 | const loggablePubKey = require('./loggable-pubkey') 4 | 5 | describe('loggablePubKey', () => { 6 | let pubkey = '0360369b401f06426e779ab89e84f74c20d2f55900d3cf0933ad93d18f146b29d1' 7 | 8 | it('returns null if no public key is provided', () => { 9 | expect(loggablePubKey()).to.be.null() 10 | }) 11 | 12 | it('returns the 15 characters selected', () => { 13 | const res = loggablePubKey(pubkey) 14 | expect(res).to.include('0360369b401f064') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/lnd-actions/subscribe-single-invoice.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 2 | /** @typedef {import('grpc').ClientReadableStream} ClientReadableStream */ 3 | 4 | /** 5 | * Subscribe to status updates for a single invoice 6 | * 7 | * @param {string} rHash 8 | * @param {object} opts 9 | * @param {LndClient} opts.client 10 | * @returns {ClientReadableStream} 11 | */ 12 | function subscribeSingleInvoice (rHash, { client }) { 13 | return client.invoices.subscribeSingleInvoice({ rHash }) 14 | } 15 | 16 | module.exports = subscribeSingleInvoice 17 | -------------------------------------------------------------------------------- /src/engine-actions/num-channels-for-address.js: -------------------------------------------------------------------------------- 1 | const getChannelsForRemoteAddress = require('./get-channels-for-remote-address') 2 | 3 | /** 4 | * Returns a number of channels that have the remotePubkey 5 | * @param {string} address - Payment channel network address 6 | * @returns {Promise} number of active and pending channels 7 | */ 8 | async function numChannelsForAddress (address) { 9 | const channelsForAddress = await getChannelsForRemoteAddress.call(this, address) 10 | 11 | return channelsForAddress.length 12 | } 13 | 14 | module.exports = numChannelsForAddress 15 | -------------------------------------------------------------------------------- /src/engine-actions/get-invoice.js: -------------------------------------------------------------------------------- 1 | const { decodePaymentRequest } = require('../lnd-actions') 2 | 3 | /** 4 | * Returns destination and satoshis from a decoded invoice 5 | * 6 | * @param {string} paymentRequestString - request string to be decoded 7 | * @returns {Promise} 8 | */ 9 | async function getInvoice (paymentRequestString) { 10 | const { 11 | destination, 12 | numSatoshis 13 | } = await decodePaymentRequest(paymentRequestString, { client: this.client }) 14 | 15 | return { 16 | destination, 17 | numSatoshis 18 | } 19 | } 20 | 21 | module.exports = getInvoice 22 | -------------------------------------------------------------------------------- /src/utils/sha256.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('test/test-helper') 2 | 3 | const { hash } = require('./sha256') 4 | 5 | describe('sha256', () => { 6 | describe('hash', () => { 7 | const cases = [ 8 | { 9 | name: '#1', 10 | input: 'IzxHRP9Pk4gd/uUPAuzEqQ8J84SVThSx9X8HaDxZpqo=', 11 | output: 'CM9VlMahIx5kjlXHQ7lA9ponFrXg4ZC+QACukB26jzM=' 12 | } 13 | ] 14 | 15 | cases.forEach(({ name, input, output }) => { 16 | it(`creates a hash for ${name}`, () => { 17 | expect(hash(input)).to.be.eql(output) 18 | }) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/constants/channel-rounding.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * List of rounding behaviors when opening multiple channels and encountering a channel that 4 | * would be uneconomically small. 5 | * 6 | * @constant 7 | * @type {object} 8 | * @default 9 | */ 10 | const CHANNEL_ROUNDING = Object.freeze({ 11 | ERROR: 'ERROR', // throw an error if we encounter a channel that is too small 12 | DOWN: 'DOWN', // drop the extra amount if we enounter a channel that is too small 13 | UP: 'UP' // round up to the minimum channel size if we encounter a channel that is too small 14 | }) 15 | 16 | module.exports = CHANNEL_ROUNDING 17 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["src", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true 17 | }, 18 | "opts": { 19 | "destination": "./docs/", 20 | "encoding": "utf8", 21 | "private": true, 22 | "recurse": true, 23 | "template": "./node_modules/minami" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-channel-balance.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get local balance of all channels for a specific daemon 6 | * 7 | * @returns {Promise} totalBalance (int64) 8 | */ 9 | async function getTotalChannelBalance () { 10 | const { channels = [] } = await listChannels({ client: this.client }) 11 | 12 | const totalLocalBalance = channels.reduce((acc, c) => { 13 | return acc.plus(c.localBalance) 14 | }, Big(0)) 15 | 16 | return totalLocalBalance.toString() 17 | } 18 | 19 | module.exports = getTotalChannelBalance 20 | -------------------------------------------------------------------------------- /src/lnd-actions/list-peers.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of peers connected to the specified lnd instance 7 | * 8 | * @param {object} opts 9 | * @param {LndClient} opts.client 10 | * @returns {Promise<{peers: Array}>} 11 | */ 12 | function listPeers ({ client }) { 13 | return new Promise((resolve, reject) => { 14 | client.listPeers({}, { deadline: deadline() }, (err, res) => { 15 | if (err) return reject(err) 16 | return resolve(res) 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = listPeers 22 | -------------------------------------------------------------------------------- /src/lnd-actions/get-info.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Queries LND for its public key 7 | * 8 | * @see http://api.lightning.community/#getInfo 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function getInfo ({ client }) { 14 | return new Promise((resolve, reject) => { 15 | client.getInfo({}, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = getInfo 23 | -------------------------------------------------------------------------------- /src/lnd-actions/list-channels.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of open channels 7 | * @see https://api.lightning.community/#listchannels 8 | * @param {object} opts 9 | * @param {LndClient} opts.client 10 | * @returns {Promise} 11 | */ 12 | function listChannels ({ client }) { 13 | return new Promise((resolve, reject) => { 14 | client.listChannels({}, { deadline: deadline() }, (err, res) => { 15 | if (err) return reject(err) 16 | return resolve(res) 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = listChannels 22 | -------------------------------------------------------------------------------- /src/utils/index.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('test/test-helper') 2 | 3 | const { 4 | Big, 5 | networkAddressFormatter, 6 | sha256 7 | } = require('./index') 8 | 9 | describe('utils index', () => { 10 | it('defines Big', () => { 11 | expect(Big).to.not.be.null() 12 | expect(Big).to.not.be.undefined() 13 | }) 14 | 15 | it('defines networkAddressFormatter', () => { 16 | expect(networkAddressFormatter).to.not.be.null() 17 | expect(networkAddressFormatter).to.not.be.undefined() 18 | }) 19 | 20 | it('defines sha256', () => { 21 | expect(sha256).to.not.be.null() 22 | expect(sha256).to.not.be.undefined() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/engine-actions/change-wallet-password.js: -------------------------------------------------------------------------------- 1 | const { 2 | changePassword: lndChangePassword 3 | } = require('../lnd-actions') 4 | 5 | /** 6 | * Changes an engine's wallet password 7 | * 8 | * @param {string} currentPass - current wallet password 9 | * @param {string} newPass - new wallet password 10 | * @returns {Promise} 11 | */ 12 | function changeWalletPassword (currentPass, newPass) { 13 | const currentPassword = Buffer.from(currentPass, 'utf8') 14 | const newPassword = Buffer.from(newPass, 'utf8') 15 | 16 | return lndChangePassword(currentPassword, newPassword, { client: this.walletUnlocker }) 17 | } 18 | 19 | module.exports = changeWalletPassword 20 | -------------------------------------------------------------------------------- /src/lnd-actions/cancel-invoice.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Cancel a hold invoice 7 | * 8 | * @param {string} paymentHash 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function cancelInvoice (paymentHash, { client }) { 14 | return new Promise((resolve, reject) => { 15 | client.invoices.cancelInvoice({ paymentHash }, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = cancelInvoice 23 | -------------------------------------------------------------------------------- /src/lnd-actions/list-payments.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of completed payments 7 | * 8 | * @see https://api.lightning.community/#listpayments 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function listPayments ({ client }) { 14 | return new Promise((resolve, reject) => { 15 | client.listPayments({}, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = listPayments 23 | -------------------------------------------------------------------------------- /src/lnd-actions/wallet-balance.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Gets the specified lnd instance's wallet balance 7 | * 8 | * @see http://api.lightning.community/#walletBalance 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function walletBalance ({ client }) { 14 | return new Promise((resolve, reject) => { 15 | client.walletBalance({}, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = walletBalance 23 | -------------------------------------------------------------------------------- /src/engine-actions/withdraw-funds.js: -------------------------------------------------------------------------------- 1 | const { 2 | sendCoins 3 | } = require('../lnd-actions') 4 | 5 | /** 6 | * Given an address and amount, it withdraws funds from the lnd wallet to the given address 7 | * 8 | * @param {string} addr - wallet address to send the coins to 9 | * @param {number} amount - amount of coin to send to wallet address 10 | * @returns {Promise} txid transaction for the withdrawal 11 | */ 12 | 13 | async function withdrawFunds (addr, amount) { 14 | const { txid } = await sendCoins(addr, amount, { client: this.client }) 15 | this.logger.debug('Funds withdrawn successfully', { txid }) 16 | 17 | return txid 18 | } 19 | 20 | module.exports = withdrawFunds 21 | -------------------------------------------------------------------------------- /src/lnd-actions/gen-seed.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndWalletUnlockerClient} WalletUnlocker */ 4 | 5 | /** 6 | * Generates a mnuemonic seed used to recover a user's wallet 7 | * 8 | * @see http://api.lightning.community/#genSeed 9 | * @param {object} opts 10 | * @param {WalletUnlocker} opts.client 11 | * @returns {Promise} 12 | */ 13 | function genSeed ({ client }) { 14 | return new Promise((resolve, reject) => { 15 | client.genSeed({}, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = genSeed 23 | -------------------------------------------------------------------------------- /src/grpc-utils/deadline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grpc Deadline 3 | * @module broker-daemon/utils/grpc-deadline 4 | */ 5 | 6 | /** 7 | * @constant 8 | * @type {number} 9 | * @default 10 | */ 11 | const DEFAULT_TIMEOUT_IN_SECONDS = 5 12 | 13 | /** 14 | * gRPC uses the term `deadline` which is a timeout feature that is an absolute 15 | * point in time, instead of a duration. 16 | * 17 | * @param {number} [timeoutInSeconds=DEFAULT_TIMEOUT_IN_SECONDS] 18 | * @returns {number} deadline in seconds 19 | */ 20 | function grpcDeadline (timeoutInSeconds = DEFAULT_TIMEOUT_IN_SECONDS) { 21 | return new Date().setSeconds(new Date().getSeconds() + timeoutInSeconds) 22 | } 23 | 24 | module.exports = grpcDeadline 25 | -------------------------------------------------------------------------------- /src/lnd-actions/list-closed-channels.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of closed channels 7 | * @see https://api.lightning.community/#closedchannels 8 | * @param {object} opts 9 | * @param {LndClient} opts.client 10 | * @returns {Promise<{channels: Array}>} 11 | */ 12 | function listClosedChannels ({ client }) { 13 | return new Promise((resolve, reject) => { 14 | client.closedChannels({}, { deadline: deadline() }, (err, res) => { 15 | if (err) return reject(err) 16 | return resolve(res) 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = listClosedChannels 22 | -------------------------------------------------------------------------------- /src/lnd-actions/settle-invoice.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Settle a hold invoice 7 | * 8 | * @param {string} preimage - base64 preimage of the invoice hash 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function settleInvoice (preimage, { client }) { 14 | return new Promise((resolve, reject) => { 15 | client.invoices.settleInvoice({ preimage }, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = settleInvoice 23 | -------------------------------------------------------------------------------- /src/lnd-actions/list-invoices.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of invoices 7 | * 8 | * @param {boolean} pendingOnly - if true, returns only pending invoices 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise} 12 | */ 13 | function listInvoices (pendingOnly, { client }) { 14 | return new Promise((resolve, reject) => { 15 | client.listInvoices({ pendingOnly }, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = listInvoices 23 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-pending-channel-balance.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listPendingChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get local balance of all pending channels for a specific daemon 6 | * 7 | * @returns {Promise} totalBalance (int64) 8 | */ 9 | async function getTotalPendingChannelBalance () { 10 | const { pendingOpenChannels = [] } = await listPendingChannels({ client: this.client }) 11 | 12 | const totalPendingLocalBalance = pendingOpenChannels.reduce((acc, c) => { 13 | return acc.plus(c.channel.localBalance) 14 | }, Big(0)) 15 | 16 | return totalPendingLocalBalance.toString() 17 | } 18 | 19 | module.exports = getTotalPendingChannelBalance 20 | -------------------------------------------------------------------------------- /src/lnd-actions/get-transactions.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a list of all on-chain transactions for the engine 7 | * 8 | * @see http://api.lightning.community/#getTransactions 9 | * @param {object} opts 10 | * @param {LndClient} opts.client 11 | * @returns {Promise<{transactions: Array}>} response 12 | */ 13 | function getTransactions ({ client }) { 14 | return new Promise((resolve, reject) => { 15 | client.getTransactions({}, { deadline: deadline() }, (err, res) => { 16 | if (err) return reject(err) 17 | return resolve(res) 18 | }) 19 | }) 20 | } 21 | 22 | module.exports = getTransactions 23 | -------------------------------------------------------------------------------- /src/lnd-actions/lookup-invoice.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Check's an invoice status 7 | * 8 | * @see http://api.lightning.community/#lookupInvoice 9 | * @param {object} paymentHashObj 10 | * @param {object} opts 11 | * @param {LndClient} opts.client 12 | * @returns {Promise} 13 | */ 14 | function lookupInvoice (paymentHashObj, { client }) { 15 | return new Promise((resolve, reject) => { 16 | client.lookupInvoice(paymentHashObj, { deadline: deadline() }, (err, res) => { 17 | if (err) return reject(err) 18 | return resolve(res) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = lookupInvoice 24 | -------------------------------------------------------------------------------- /src/lnd-actions/send-coins.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Sends coins from lnd wallet to address 7 | * 8 | * @see http://api.lightning.community/#sendCoins 9 | * @param {string} addr 10 | * @param {number} amount 11 | * @param {object} opts 12 | * @param {LndClient} opts.client 13 | * @returns {object} response 14 | */ 15 | function sendCoins (addr, amount, { client }) { 16 | return new Promise((resolve, reject) => { 17 | client.sendCoins({ addr, amount }, { deadline: deadline() }, (err, res) => { 18 | if (err) return reject(err) 19 | return resolve(res) 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = sendCoins 25 | -------------------------------------------------------------------------------- /docker/lnd/conf/lnd-bitcoind.conf: -------------------------------------------------------------------------------- 1 | [Application Options] 2 | 3 | datadir=/data 4 | maxlogfiles=3 5 | maxlogfilesize=50 6 | tlscertpath=/shared/lnd-engine-tls-btc.cert 7 | tlskeypath=/shared/lnd-engine-tls-btc.key 8 | adminmacaroonpath=/shared/lnd-engine-admin-btc.macaroon 9 | readonlymacaroonpath=/shared/lnd-engine-readonly-btc.macaroon 10 | invoicemacaroonpath=/shared/lnd-engine-invoice-btc.macaroon 11 | listen=0.0.0.0:9735 12 | rpclisten=0.0.0.0:10009 13 | debuglevel=info 14 | maxpendingchannels=2 15 | nobootstrap=1 16 | tlsextradomain=lnd_btc 17 | minchansize=250000 18 | backupfilepath=/backup/lnd_btc.backup 19 | 20 | [autopilot] 21 | autopilot.active=0 22 | 23 | [Bitcoin] 24 | 25 | bitcoin.active=1 26 | bitcoin.node=bitcoind 27 | bitcoin.defaultchanconfs=3 28 | -------------------------------------------------------------------------------- /src/engine-actions/pay-invoice.js: -------------------------------------------------------------------------------- 1 | const { 2 | sendPayment 3 | } = require('../lnd-actions') 4 | 5 | /** 6 | * Given a payment request, it pays the invoices and returns a refund invoice 7 | * 8 | * @param {string} paymentRequest 9 | * @returns {Promise} paymentPreimage 10 | */ 11 | 12 | async function payInvoice (paymentRequest) { 13 | const { paymentError, paymentPreimage } = await sendPayment({ paymentRequest }, { client: this.client }) 14 | 15 | if (paymentError) { 16 | this.logger.error('Failed to pay invoice', { paymentRequest }) 17 | throw new Error(paymentError) 18 | } 19 | 20 | this.logger.debug('Payment successfully made', { paymentRequest }) 21 | 22 | return paymentPreimage 23 | } 24 | 25 | module.exports = payInvoice 26 | -------------------------------------------------------------------------------- /docker/lnd/conf/lnd-litecoind.conf: -------------------------------------------------------------------------------- 1 | [Application Options] 2 | 3 | datadir=/data 4 | maxlogfiles=3 5 | maxlogfilesize=50 6 | tlscertpath=/shared/lnd-engine-tls-ltc.cert 7 | tlskeypath=/shared/lnd-engine-tls-ltc.key 8 | adminmacaroonpath=/shared/lnd-engine-admin-ltc.macaroon 9 | readonlymacaroonpath=/shared/lnd-engine-readonly-ltc.macaroon 10 | invoicemacaroonpath=/shared/lnd-engine-invoice-ltc.macaroon 11 | listen=0.0.0.0:9735 12 | rpclisten=0.0.0.0:10009 13 | debuglevel=info 14 | maxpendingchannels=8 15 | nobootstrap=1 16 | tlsextradomain=lnd_ltc 17 | minchansize=15000000 18 | backupfilepath=/backup/lnd_ltc.backup 19 | 20 | [autopilot] 21 | autopilot.active=0 22 | 23 | [Litecoin] 24 | 25 | litecoin.active=1 26 | litecoin.node=litecoind 27 | litecoin.defaultchanconfs=3 28 | -------------------------------------------------------------------------------- /src/lnd-actions/close-channel.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 2 | /** @typedef {import('grpc').ClientReadableStream} ClientReadableStream */ 3 | 4 | /** 5 | * Close a channel w/ LND 6 | * @see http://api.lightning.community/#closechannel 7 | * 8 | * @param {object} channelPoint - { fundingTxidStr, outputIndex } to identify the channel to be closed 9 | * @param {boolean} force - true if we want to force close the channel, false if not (defaults to false) 10 | * @param {object} opts 11 | * @param {LndClient} opts.client 12 | * @returns {ClientReadableStream} 13 | */ 14 | function closeChannel (channelPoint, force, { client }) { 15 | return client.closeChannel({ channelPoint, force }) 16 | } 17 | 18 | module.exports = closeChannel 19 | -------------------------------------------------------------------------------- /src/lnd-actions/unlock-wallet.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndWalletUnlockerClient} WalletUnlocker */ 4 | 5 | /** 6 | * Unlock an LND wallet 7 | * 8 | * @see http://api.lightning.community/#unlockWallet 9 | * @param {Buffer} walletPassword - Buffer or base64 string 10 | * @param {object} opts 11 | * @param {WalletUnlocker} opts.client 12 | * @returns {Promise} 13 | */ 14 | function unlockWallet (walletPassword, { client }) { 15 | return new Promise((resolve, reject) => { 16 | client.unlockWallet({ walletPassword }, { deadline: deadline() }, (err, res) => { 17 | if (err) return reject(err) 18 | return resolve(res) 19 | }) 20 | }) 21 | } 22 | 23 | module.exports = unlockWallet 24 | -------------------------------------------------------------------------------- /src/lnd-actions/new-address.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Returns a new wallet address from a lnd instance 7 | * 8 | * @see http://api.lightning.community/#newAddress 9 | * @param {number} type - Wallet Address Type 10 | * @param {object} opts 11 | * @param {LndClient} opts.client 12 | * @returns {Promise} address 13 | */ 14 | function newAddress (type, { client }) { 15 | return new Promise((resolve, reject) => { 16 | client.newAddress({ type }, { deadline: deadline() }, (err, res) => { 17 | if (err) return reject(err) 18 | const { address } = res 19 | return resolve(address) 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = newAddress 25 | -------------------------------------------------------------------------------- /src/engine-actions/create-new-address.js: -------------------------------------------------------------------------------- 1 | const { newAddress } = require('../lnd-actions') 2 | 3 | /** 4 | * Nested Segregated Witness address type. This address is referred to as 5 | * nested-pay-to-witness-key-hash (np2wkh). 6 | * 7 | * This value is taken from grpc enums located in lnd's rpc.proto 8 | * 9 | * @see https://github.com/lightningnetwork/lnd/blob/master/lnrpc/rpc.proto 10 | * @constant 11 | * @type {number} 12 | * @default 13 | */ 14 | const NESTED_WITNESS_ADDRESS_TYPE = 1 15 | 16 | /** 17 | * Creates a new wallet address 18 | * 19 | * @returns {Promise} address 20 | */ 21 | async function createNewAddress () { 22 | const address = await newAddress(NESTED_WITNESS_ADDRESS_TYPE, { client: this.client }) 23 | return address 24 | } 25 | 26 | module.exports = createNewAddress 27 | -------------------------------------------------------------------------------- /src/lnd-actions/decode-payment-request.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Given an lnd payment request hash, try to decode the hash w/ a specified 7 | * lnd node 8 | * 9 | * @see https://api.lightning.community/#decodepayreq 10 | * @param {string} paymentRequest 11 | * @param {object} opts 12 | * @param {LndClient} opts.client 13 | * @returns {Promise} 14 | */ 15 | function decodePaymentRequest (paymentRequest, { client }) { 16 | return new Promise((resolve, reject) => { 17 | client.decodePayReq({ payReq: paymentRequest }, { deadline: deadline() }, (err, res) => { 18 | if (err) return reject(err) 19 | return resolve(res) 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = decodePaymentRequest 25 | -------------------------------------------------------------------------------- /src/lnd-actions/describe-graph.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * @typedef {object} ChannelGraph 7 | * @property {Array} nodes List of Lightning Nodes in the graph 8 | * @property {Array} edges List of Channel Edges connecting Lightning Nodes in the graph 9 | */ 10 | 11 | /** 12 | * Returns a description of the graph state 13 | * 14 | * @param {object} opts 15 | * @param {LndClient} opts.client 16 | * @returns {Promise} 17 | */ 18 | function describeGraph ({ client }) { 19 | return new Promise((resolve, reject) => { 20 | client.describeGraph({}, { deadline: deadline() }, (err, res) => { 21 | if (err) return reject(err) 22 | return resolve(res) 23 | }) 24 | }) 25 | } 26 | 27 | module.exports = describeGraph 28 | -------------------------------------------------------------------------------- /src/engine-actions/is-invoice-paid.js: -------------------------------------------------------------------------------- 1 | const { 2 | decodePaymentRequest, 3 | lookupInvoice 4 | } = require('../lnd-actions') 5 | const { 6 | INVOICE_STATES 7 | } = require('../constants') 8 | 9 | /** 10 | * Looks up whether or not an invoice has been paid 11 | * 12 | * @see {lnd-actions#lookupinvoice} 13 | * @see {lnd-actions#decodePaymentRequest} 14 | * @param {string} paymentRequest 15 | * @returns {Promise} true if the invoice is settled, false if not 16 | */ 17 | async function isInvoicePaid (paymentRequest) { 18 | const { paymentHash } = await decodePaymentRequest(paymentRequest, { client: this.client }) 19 | const rHash = Buffer.from(paymentHash, 'hex').toString('base64') 20 | const { state } = await lookupInvoice({ rHash }, { client: this.client }) 21 | 22 | return state === INVOICE_STATES.SETTLED 23 | } 24 | 25 | module.exports = isInvoicePaid 26 | -------------------------------------------------------------------------------- /test/test-helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * LND-Engine test helper 3 | * 4 | * NOTE: This file is specifically loaded before all tests so that we 5 | * can globally require some files. 6 | */ 7 | const sinon = require('sinon') 8 | const chai = require('chai') 9 | const sinonChai = require('sinon-chai') 10 | const dirtyChai = require('dirty-chai') 11 | const rewire = require('rewire') 12 | const timekeeper = require('timekeeper') 13 | const chaiAsPromised = require('chai-as-promised') 14 | const delay = require('timeout-as-promise') 15 | 16 | const { expect } = chai 17 | 18 | chai.use(sinonChai) 19 | chai.use(dirtyChai) 20 | chai.use(chaiAsPromised) 21 | 22 | let sandbox = sinon.createSandbox() 23 | 24 | afterEach(function () { 25 | sandbox.restore() 26 | }) 27 | 28 | module.exports = { 29 | chai, 30 | sinon: sandbox, 31 | rewire, 32 | expect, 33 | timekeeper, 34 | delay 35 | } 36 | -------------------------------------------------------------------------------- /src/lnd-actions/update-channel-policy.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 2 | 3 | /** 4 | * @constant 5 | * @type {number} 6 | * @default 7 | */ 8 | const DEFAULT_TIMELOCK_DELTA = 9 9 | 10 | /** 11 | * 12 | * @param {object} chanPoint 13 | * @param {string} feeRate - number up to 6 decimal places 14 | * @param {number} [timeLockDelta=DEFAULT_TIMELOCK_DELTA] 15 | * @param {object} opts 16 | * @param {LndClient} opts.client 17 | * @returns {Promise} res 18 | */ 19 | async function updateChannelPolicy (chanPoint, feeRate, timeLockDelta = DEFAULT_TIMELOCK_DELTA, { client }) { 20 | return new Promise((resolve, reject) => 21 | client.updateChannelPolicy({ chanPoint, feeRate, timeLockDelta }, (err, res) => { 22 | if (err) return reject(err) 23 | return resolve(res) 24 | }) 25 | ) 26 | } 27 | 28 | module.exports = updateChannelPolicy 29 | -------------------------------------------------------------------------------- /src/lnd-actions/change-password.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndWalletUnlockerClient} WalletUnlocker */ 4 | 5 | /** 6 | * Change the wallet password of an lnd wallet 7 | * 8 | * @see http://api.lightning.community/#changePassword 9 | * @param {Buffer} currentPassword - Buffer or base64 string 10 | * @param {Buffer} newPassword - Buffer or base64 string 11 | * @param {object} opts 12 | * @param {WalletUnlocker} opts.client 13 | * @returns {Promise} 14 | */ 15 | function changePassword (currentPassword, newPassword, { client }) { 16 | return new Promise((resolve, reject) => { 17 | client.changePassword({ currentPassword, newPassword }, { deadline: deadline() }, (err, res) => { 18 | if (err) return reject(err) 19 | return resolve(res) 20 | }) 21 | }) 22 | } 23 | 24 | module.exports = changePassword 25 | -------------------------------------------------------------------------------- /scripts/check-unused.js: -------------------------------------------------------------------------------- 1 | const madge = require('madge') 2 | require('colors') 3 | 4 | /** 5 | * Regexp string to exclude spec files from orphans 6 | * @type {string} 7 | * @constant 8 | */ 9 | const TEST_FILE_REGEXP = '.spec.js$' 10 | 11 | /** 12 | * directory to check for orphans 13 | * @type {string} 14 | * @constant 15 | */ 16 | const DIRECTORY = 'src/' 17 | 18 | /** 19 | * Entrypoint into the app which will always be orphaned 20 | * @type {string} 21 | * @constant 22 | */ 23 | const ENTRYPOINT = 'index.js' 24 | 25 | const config = { 26 | excludeRegExp: [ TEST_FILE_REGEXP ] 27 | } 28 | 29 | ;(async function () { 30 | const tree = await madge(DIRECTORY, config) 31 | const orphans = tree.orphans() 32 | if (orphans.length === 1 && orphans[0] === ENTRYPOINT) { 33 | return 34 | } else { 35 | console.error(`There are modules with no dependencies: ${orphans}`.red) 36 | process.exit(1) 37 | } 38 | })() 39 | -------------------------------------------------------------------------------- /src/lnd-actions/list-pending-channels.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** @typedef {object} PendingChannels 6 | * @property {Array} pendingOpenChannels 7 | * @property {Array} pendingClosingChannels 8 | * @property {Array} pendingForceClosingChannels 9 | * @property {Array} waitingCloseChannels 10 | */ 11 | 12 | /** 13 | * Returns a list of pending channels 14 | * @see https://api.lightning.community/#pendingchannels 15 | * @param {object} opts 16 | * @param {LndClient} opts.client 17 | * @returns {Promise} 18 | */ 19 | function listPendingChannels ({ client }) { 20 | return new Promise((resolve, reject) => { 21 | client.pendingChannels({}, { deadline: deadline() }, (err, res) => { 22 | if (err) return reject(err) 23 | return resolve(res) 24 | }) 25 | }) 26 | } 27 | 28 | module.exports = listPendingChannels 29 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-balance-for-address.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const getChannelsForRemoteAddress = require('./get-channels-for-remote-address') 3 | 4 | /** 5 | * Get local balance of all channels for a specific address 6 | * @param {string} address 7 | * @param {object} [options={}] 8 | * @param {boolean} [options.outbound=true] - outbound is true if checking outbound channels, false if inbound 9 | * @returns {Promise} totalBalance (int64) 10 | */ 11 | async function getTotalBalanceForAddress (address, { outbound = true } = {}) { 12 | const channelsForAddress = await getChannelsForRemoteAddress.call(this, address) 13 | 14 | const balanceType = outbound ? 'localBalance' : 'remoteBalance' 15 | const totalBalance = channelsForAddress.reduce((acc, c) => { 16 | return acc.plus(c[balanceType]) 17 | }, Big(0)) 18 | 19 | return totalBalance.toString() 20 | } 21 | 22 | module.exports = getTotalBalanceForAddress 23 | -------------------------------------------------------------------------------- /src/lnd-actions/add-hold-invoice.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Creates a hold invoice on lnd 7 | * 8 | * @param {object} params 9 | * @param {string} params.memo 10 | * @param {string} params.expiry - invoice expiry in seconds 11 | * @param {string} params.cltvExpiry - cltv delta of the final hop in blocks 12 | * @param {string} params.value 13 | * @param {string} params.hash - hash of the preimage 14 | * @param {object} opts 15 | * @param {LndClient} opts.client 16 | * @returns {Promise} lightning invoice 17 | */ 18 | function addHoldInvoice (params, { client }) { 19 | return new Promise((resolve, reject) => { 20 | client.invoices.addHoldInvoice(params, { deadline: deadline() }, (err, res) => { 21 | if (err) return reject(err) 22 | return resolve(res) 23 | }) 24 | }) 25 | } 26 | 27 | module.exports = addHoldInvoice 28 | -------------------------------------------------------------------------------- /src/utils/delay.spec.js: -------------------------------------------------------------------------------- 1 | const { sinon, rewire, expect } = require('test/test-helper') 2 | const path = require('path') 3 | const delay = rewire(path.resolve(__dirname, 'delay')) 4 | 5 | describe('delay', () => { 6 | let delayTime 7 | let setTimeout 8 | 9 | beforeEach(() => { 10 | setTimeout = sinon.stub().callsArg(0) 11 | delayTime = 100 12 | delay.__set__('setTimeout', setTimeout) 13 | }) 14 | 15 | it('calls setTimeout with the callFunction and delay time', async () => { 16 | await delay(delayTime) 17 | expect(setTimeout).to.have.been.calledWith(sinon.match.func, delayTime) 18 | }) 19 | 20 | it('calls the callFunction after setTimeout resolves', async () => { 21 | let isResolved = false 22 | 23 | delay(delayTime).then(() => { 24 | isResolved = true 25 | }) 26 | 27 | expect(isResolved).to.be.false() 28 | setTimeout.args[0][0]() 29 | setImmediate(() => expect(isResolved).to.be.true()) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "extends": "standard", 6 | "plugins": [ 7 | "jsdoc" 8 | ], 9 | "rules": { 10 | "jsdoc/check-examples": 1, 11 | "jsdoc/check-param-names": 1, 12 | "jsdoc/check-tag-names": 1, 13 | "jsdoc/check-types": 1, 14 | "jsdoc/newline-after-description": 0, 15 | "jsdoc/no-undefined-types": 0, 16 | "jsdoc/require-description": 0, 17 | "jsdoc/require-description-complete-sentence": 0, 18 | "jsdoc/require-example": 0, 19 | "jsdoc/require-hyphen-before-param-description": 1, 20 | "jsdoc/require-param": 1, 21 | "jsdoc/require-param-description": 0, 22 | "jsdoc/require-param-name": 1, 23 | "jsdoc/require-param-type": 1, 24 | "jsdoc/require-returns": 1, 25 | "jsdoc/require-returns-check": 1, 26 | "jsdoc/require-returns-description": 0, 27 | "jsdoc/require-returns-type": 1, 28 | "jsdoc/valid-types": 1 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/engine-actions/get-public-key.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getPublicKey = rewire(path.resolve(__dirname, 'get-public-key')) 5 | 6 | describe('getPublicKey', () => { 7 | let getInfoResponse 8 | let getInfoStub 9 | let clientStub 10 | let res 11 | 12 | beforeEach(() => { 13 | getInfoResponse = { identityPubkey: '1234' } 14 | getInfoStub = sinon.stub().returns(getInfoResponse) 15 | clientStub = sinon.stub() 16 | 17 | getPublicKey.__set__('getInfo', getInfoStub) 18 | getPublicKey.__set__('client', clientStub) 19 | }) 20 | 21 | beforeEach(async () => { 22 | res = await getPublicKey() 23 | }) 24 | 25 | it('gets info on a specified lnd instance', () => { 26 | expect(getInfoStub).to.have.been.calledWith(sinon.match({ client: clientStub })) 27 | }) 28 | 29 | it('returns the identity_publickey', () => { 30 | expect(res).to.be.eql(getInfoResponse.identityPubkey) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/constants/engine-statuses.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * List of statuses for an lnd-engine. Each status represents a step in an engine's 4 | * lifecycle 5 | * 6 | * @constant 7 | * @type {object} 8 | * @default 9 | */ 10 | const ENGINE_STATUSES = Object.freeze({ 11 | UNKNOWN: 'UNKNOWN', // Default state of the engine 12 | UNAVAILABLE: 'UNAVAILABLE', // LightningRpc (lnrpc) is unavailable, WalletUnlocker rpc is unavailable 13 | NEEDS_WALLET: 'NEEDS_WALLET', // Wallet does not exist, LightningRpc is UNIMPLEMENTED, genSeeds does not throw 14 | LOCKED: 'LOCKED', // LightningRpc (lnrpc) is UNIMPLEMENTED, genSeeds throws "wallet already exists" 15 | UNLOCKED: 'UNLOCKED', // LightningRpc (lnrpc) getInfo does not throw an error 16 | NOT_SYNCED: 'NOT_SYNCED', // LightningRpc (lnrpc) getInfo synced_to_chain boolean is false 17 | OLD_VERSION: 'OLD_VERSION', // LND version is older than the specified minVersion 18 | VALIDATED: 'VALIDATED' // LightningRpc (lnrpc) getInfo matches the engines configuration 19 | }) 20 | 21 | module.exports = ENGINE_STATUSES 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Kinesis Inc DBA Sparkswap 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/engine-actions/get-pending-channel-capacities.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listPendingChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get local balance of all channels for a specific daemon 6 | * 7 | * @returns {Promise<{localBalance: string, remoteBalance: string}>} totalBalance (int64) 8 | */ 9 | async function getPendingChannelCapacities () { 10 | const { pendingOpenChannels = [] } = await listPendingChannels({ client: this.client }) 11 | 12 | if (pendingOpenChannels.length === 0) { 13 | this.logger.debug('getPendingChannelCapacities: No channels exist') 14 | } 15 | 16 | const totalLocalBalance = pendingOpenChannels.reduce((acc, c) => { 17 | return acc.plus(c.channel.localBalance) 18 | }, Big(0)) 19 | 20 | const totalRemoteBalance = pendingOpenChannels.reduce((acc, c) => { 21 | return acc.plus(c.channel.remoteBalance) 22 | }, Big(0)) 23 | 24 | return { localBalance: totalLocalBalance.toString(), remoteBalance: totalRemoteBalance.toString() } 25 | } 26 | 27 | module.exports = getPendingChannelCapacities 28 | -------------------------------------------------------------------------------- /src/engine-actions/create-new-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const createNewAddress = rewire(path.resolve(__dirname, 'create-new-address')) 5 | 6 | describe('createNewAddress', () => { 7 | let newAddressStub 8 | let clientStub 9 | let addressResponse 10 | let res 11 | let nestedWitnessType 12 | 13 | beforeEach(() => { 14 | nestedWitnessType = 1 15 | addressResponse = '1234' 16 | newAddressStub = sinon.stub().returns(addressResponse) 17 | clientStub = sinon.stub() 18 | 19 | createNewAddress.__set__('newAddress', newAddressStub) 20 | createNewAddress.__set__('client', clientStub) 21 | }) 22 | 23 | beforeEach(async () => { 24 | res = await createNewAddress() 25 | }) 26 | 27 | it('creates a new address through lnd', () => { 28 | expect(newAddressStub).to.have.been.calledWith(nestedWitnessType, { client: clientStub }) 29 | }) 30 | 31 | it('returns a wallet address', () => { 32 | expect(res).to.be.eql(addressResponse) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/grpc-utils/deadline.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { rewire, expect, timekeeper } = require('test/test-helper') 3 | 4 | const deadline = rewire(path.resolve(__dirname, 'deadline')) 5 | 6 | describe('grpc-deadline', () => { 7 | let timeoutInSeconds 8 | 9 | beforeEach(() => { 10 | timeoutInSeconds = deadline.__get__('DEFAULT_TIMEOUT_IN_SECONDS') 11 | timekeeper.freeze(new Date()) 12 | }) 13 | 14 | afterEach(() => { 15 | timekeeper.reset() 16 | }) 17 | 18 | it('returns a time in the future based on default timeout', () => { 19 | const expectedDeadline = (new Date().getSeconds() + timeoutInSeconds) 20 | const expectedDate = new Date().setSeconds(expectedDeadline) 21 | expect(deadline()).to.eql(expectedDate) 22 | }) 23 | 24 | it('returns a time in the future based on a given timeout value', () => { 25 | const customTimeout = 500 26 | const expectedDeadline = (new Date().getSeconds() + customTimeout) 27 | const expectedDate = new Date().setSeconds(expectedDeadline) 28 | expect(deadline(customTimeout)).to.eql(expectedDate) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/engine-actions/get-confirmed-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getConfirmedBalance = rewire(path.resolve(__dirname, 'get-confirmed-balance')) 5 | 6 | describe('getConfirmedBalance', () => { 7 | let walletBalanceStub 8 | let clientStub 9 | let balanceResponse 10 | let confirmedBalance 11 | let res 12 | 13 | beforeEach(() => { 14 | confirmedBalance = '1234' 15 | balanceResponse = { confirmedBalance } 16 | walletBalanceStub = sinon.stub().returns(balanceResponse) 17 | clientStub = sinon.stub() 18 | 19 | getConfirmedBalance.__set__('walletBalance', walletBalanceStub) 20 | getConfirmedBalance.__set__('client', clientStub) 21 | }) 22 | 23 | beforeEach(async () => { 24 | res = await getConfirmedBalance() 25 | }) 26 | 27 | it('gets an wallet balance', () => { 28 | expect(walletBalanceStub).to.have.been.calledWith(sinon.match({ client: clientStub })) 29 | }) 30 | 31 | it('returns a confirmedBalance', () => { 32 | expect(res).to.be.eql(confirmedBalance) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/engine-actions/get-uncommitted-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getUncommittedBalance = rewire(path.resolve(__dirname, 'get-uncommitted-balance')) 5 | 6 | describe('getUncommittedBalance', () => { 7 | let walletBalanceStub 8 | let clientStub 9 | let balanceResponse 10 | let confirmedBalance 11 | let res 12 | 13 | beforeEach(() => { 14 | confirmedBalance = '1234' 15 | balanceResponse = { confirmedBalance } 16 | walletBalanceStub = sinon.stub().returns(balanceResponse) 17 | clientStub = sinon.stub() 18 | 19 | getUncommittedBalance.__set__('walletBalance', walletBalanceStub) 20 | getUncommittedBalance.__set__('client', clientStub) 21 | }) 22 | 23 | beforeEach(async () => { 24 | res = await getUncommittedBalance() 25 | }) 26 | 27 | it('gets a wallet balance', () => { 28 | expect(walletBalanceStub).to.have.been.calledWith(sinon.match({ client: clientStub })) 29 | }) 30 | 31 | it('returns the totalBalance', () => { 32 | expect(res).to.be.eql(confirmedBalance) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/lnd-actions/lookup-payment-status.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Payment Status corresponding to the output of 7 | * LND's LookupPaymentStatus RPC 8 | * @constant 9 | * @type {object} 10 | * @default 11 | */ 12 | const PAYMENT_STATUSES = Object.freeze({ 13 | GROUNDED: 'GROUNDED', 14 | IN_FLIGHT: 'IN_FLIGHT', 15 | COMPLETED: 'COMPLETED' 16 | }) 17 | 18 | /** 19 | * Checks a payment's status 20 | * 21 | * @param {string} paymentHash - Base64 encoded payment hash for the desired payment 22 | * @param {object} opts 23 | * @param {LndClient} opts.client 24 | * @returns {Promise} response 25 | */ 26 | function lookupPaymentStatus (paymentHash, { client }) { 27 | return new Promise((resolve, reject) => { 28 | client.lookupPaymentStatus({ rHash: paymentHash }, { deadline: deadline() }, (err, res) => { 29 | if (err) return reject(err) 30 | return resolve(res) 31 | }) 32 | }) 33 | } 34 | 35 | lookupPaymentStatus.STATUSES = PAYMENT_STATUSES 36 | 37 | module.exports = lookupPaymentStatus 38 | -------------------------------------------------------------------------------- /src/engine-actions/get-unconfirmed-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getUnconfirmedBalance = rewire(path.resolve(__dirname, 'get-unconfirmed-balance')) 5 | 6 | describe('getUnconfirmedBalance', () => { 7 | let walletBalanceStub 8 | let clientStub 9 | let balanceResponse 10 | let unconfirmedBalance 11 | let res 12 | 13 | beforeEach(() => { 14 | unconfirmedBalance = '1234' 15 | balanceResponse = { unconfirmedBalance } 16 | walletBalanceStub = sinon.stub().returns(balanceResponse) 17 | clientStub = sinon.stub() 18 | 19 | getUnconfirmedBalance.__set__('walletBalance', walletBalanceStub) 20 | getUnconfirmedBalance.__set__('client', clientStub) 21 | }) 22 | 23 | beforeEach(async () => { 24 | res = await getUnconfirmedBalance() 25 | }) 26 | 27 | it('gets a wallet balance', () => { 28 | expect(walletBalanceStub).to.have.been.calledWith(sinon.match({ client: clientStub })) 29 | }) 30 | 31 | it('returns an unconfirmedBalance', () => { 32 | expect(res).to.be.eql(unconfirmedBalance) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/engine-actions/create-swap-hash.js: -------------------------------------------------------------------------------- 1 | const { addInvoice } = require('../lnd-actions') 2 | 3 | /** 4 | * default expiry for swap hashes is 3600 seconds - 1 hour. 5 | * @todo Should this be changed to something shorter or be configurable 6 | * @constant 7 | * @type {string} 8 | */ 9 | const SWAP_EXPIRY = '3600' 10 | 11 | /** 12 | * The memo prefix allows us to easily find SparkSwap-related invoices 13 | * in LND. In this case, the invoice is the end point of the swap, its 14 | * "terminus". 15 | * 16 | * @constant 17 | * @type {string} 18 | * @default 19 | */ 20 | const MEMO_PREFIX = 'sparkswap-swap-terminus:' 21 | 22 | /** 23 | * Creates a swap hash to prepare for a swap 24 | * 25 | * @param {string} orderId - order ID for the swap hash 26 | * @param {string} value - int64 27 | * @returns {Promise} rHash - hash of invoice from lnd 28 | */ 29 | async function createSwapHash (orderId, value) { 30 | const expiry = SWAP_EXPIRY 31 | const memo = `${MEMO_PREFIX}${orderId}` 32 | const { rHash } = await addInvoice({ memo, expiry, value }, { client: this.client }) 33 | return rHash 34 | } 35 | 36 | module.exports = createSwapHash 37 | -------------------------------------------------------------------------------- /src/engine-actions/settle-swap.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const settleSwap = rewire(path.resolve(__dirname, 'settle-swap')) 5 | 6 | describe('settleSwap', () => { 7 | let settleInvoiceStub 8 | let settleInvoiceResponse 9 | let preimage 10 | 11 | const client = sinon.stub() 12 | const engine = { 13 | client, 14 | secondsPerBlock: 600, 15 | logger: { 16 | info: sinon.stub(), 17 | error: sinon.stub(), 18 | debug: sinon.stub() 19 | } 20 | } 21 | 22 | beforeEach(() => { 23 | preimage = '1234' 24 | settleInvoiceResponse = { } 25 | settleInvoiceStub = sinon.stub().resolves(settleInvoiceResponse) 26 | 27 | settleSwap.__set__('settleInvoice', settleInvoiceStub) 28 | settleSwap.__set__('client', client) 29 | }) 30 | 31 | it('settles invoice through lnd', async () => { 32 | const response = await settleSwap.call(engine, preimage) 33 | expect(response).to.be.eql(settleInvoiceResponse) 34 | expect(settleInvoiceStub).to.have.been.calledWith( 35 | preimage, sinon.match({ client })) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/engine-actions/get-settled-swap-preimage.js: -------------------------------------------------------------------------------- 1 | const { lookupInvoice } = require('../lnd-actions') 2 | const { INVOICE_STATES } = require('../constants') 3 | 4 | /** 5 | * Gets the preimage for a settled swap hash. 6 | * 7 | * @see {lnd-actions#lookupinvoice} 8 | * @see http://api.lightning.community/#addinvoice 9 | * @param {string} swapHash - Base64 encoded hash for the invoice 10 | * @returns {Promise} Base64 encoded preimage for the hash 11 | * @throws {Error} If the invoice is not in a SETTLED state 12 | */ 13 | async function getSettledSwapPreimage (swapHash) { 14 | if (!swapHash) { 15 | throw new Error('Swap hash must be defined') 16 | } 17 | 18 | this.logger.debug('Looking up invoice by paymentHash:', { rHash: swapHash }) 19 | 20 | const { state, rPreimage } = await lookupInvoice({ rHash: swapHash }, { client: this.client }) 21 | 22 | this.logger.debug('Invoice has been retrieved: ', { state }) 23 | 24 | if (state !== INVOICE_STATES.SETTLED) { 25 | throw new Error(`Cannot retrieve preimage from an invoice in a ${state} state.`) 26 | } 27 | 28 | return rPreimage 29 | } 30 | 31 | module.exports = getSettledSwapPreimage 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # docs 61 | docs/ 62 | -------------------------------------------------------------------------------- /src/engine-actions/get-invoice-value.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getInvoiceValue = rewire(path.resolve(__dirname, 'get-invoice-value')) 5 | 6 | describe('getInvoiceValue', () => { 7 | let paymentRequestString 8 | let decodePaymentRequestStub 9 | let clientStub 10 | let res 11 | let expectedValue 12 | 13 | beforeEach(() => { 14 | paymentRequestString = '1234asdf' 15 | expectedValue = 100 16 | decodePaymentRequestStub = sinon.stub().resolves({ numSatoshis: expectedValue }) 17 | clientStub = sinon.stub() 18 | 19 | getInvoiceValue.__set__('decodePaymentRequest', decodePaymentRequestStub) 20 | getInvoiceValue.__set__('client', clientStub) 21 | }) 22 | 23 | beforeEach(async () => { 24 | res = await getInvoiceValue(paymentRequestString) 25 | }) 26 | 27 | it('gets decoded payment request details', () => { 28 | expect(decodePaymentRequestStub).to.have.been.calledWith(paymentRequestString, sinon.match({ client: clientStub })) 29 | }) 30 | 31 | it('returns the result', () => { 32 | expect(res).to.be.eql(expectedValue) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/engine-actions/get-payment-channel-network-address.js: -------------------------------------------------------------------------------- 1 | const { getInfo } = require('../lnd-actions') 2 | const { networkAddressFormatter } = require('../utils') 3 | 4 | /** 5 | * @constant 6 | * @type {string} 7 | * @default 8 | */ 9 | const HOST_DELIMITER = '@' 10 | 11 | /** 12 | * Returns the payment channel network address for this node 13 | * 14 | * @returns {Promise} 15 | */ 16 | async function getPaymentChannelNetworkAddress () { 17 | const { 18 | identityPubkey, 19 | uris = [] 20 | } = await getInfo({ client: this.client }) 21 | 22 | if (!identityPubkey) { 23 | throw new Error('No pubkey exists for engine') 24 | } 25 | 26 | // The uri will only exist if a user has set the `--external-ip` flag on the 27 | // LND instance. If this uri does not exist, we will simply use the lnd public key 28 | if (!uris.length) { 29 | return networkAddressFormatter.serialize({ publicKey: identityPubkey }) 30 | } 31 | 32 | const uri = uris[0] 33 | const [ publicKey, host ] = uri.split(HOST_DELIMITER) 34 | 35 | return networkAddressFormatter.serialize({ publicKey, host }) 36 | } 37 | 38 | module.exports = getPaymentChannelNetworkAddress 39 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-reserved-channel-balance.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get total reserved channel balance for all channels for a specific daemon. 6 | * 7 | * Note: For LND, the commitFee for an initiator is the only balance currently reserved. 8 | * 9 | * @returns {Promise} totalReservedChannelBalance (int64) 10 | */ 11 | async function getTotalReservedChannelBalance () { 12 | const { channels = [] } = await listChannels({ client: this.client }) 13 | 14 | // We filter for channels where the engine was the initiator because available balances for 15 | // a channel are calculated less commit fees when a party was the initiator. The rationale of 16 | // this calculation is to put the responsibility on the initiator to pay for a force-close 17 | const initiatorChannels = channels.filter(c => c.initiator) 18 | 19 | const totalReservedChannelBalance = initiatorChannels.reduce((acc, c) => { 20 | return acc.plus(c.commitFee) 21 | }, Big(0)) 22 | 23 | return totalReservedChannelBalance.toString() 24 | } 25 | 26 | module.exports = getTotalReservedChannelBalance 27 | -------------------------------------------------------------------------------- /src/engine-actions/unlock-wallet.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const unlockWallet = rewire(path.resolve(__dirname, 'unlock-wallet')) 5 | 6 | describe('unlock-wallet', () => { 7 | const walletPassword = 'mypassword' 8 | 9 | let lndUnlockWalletStub 10 | let engine 11 | let bufferStub 12 | let buffer 13 | 14 | beforeEach(() => { 15 | engine = { 16 | walletUnlocker: sinon.stub 17 | } 18 | buffer = sinon.stub() 19 | bufferStub = { 20 | from: sinon.stub().returns(buffer) 21 | } 22 | lndUnlockWalletStub = sinon.stub() 23 | 24 | unlockWallet.__set__('lndUnlockWallet', lndUnlockWalletStub) 25 | unlockWallet.__set__('Buffer', bufferStub) 26 | }) 27 | 28 | beforeEach(async () => { 29 | await unlockWallet.call(engine, walletPassword) 30 | }) 31 | 32 | it('converts a string to buffer', () => { 33 | expect(bufferStub.from).to.have.been.calledWith(walletPassword, sinon.match.any) 34 | }) 35 | 36 | it('unlocks a wallet', () => { 37 | expect(lndUnlockWalletStub).to.have.been.calledWith(buffer, { client: engine.walletUnlocker }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/engine-actions/create-swap-hash.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const createSwapHash = rewire(path.resolve(__dirname, 'create-swap-hash')) 5 | 6 | describe('createSwapHash', () => { 7 | let orderId 8 | let value 9 | let addInvoiceStub 10 | let clientStub 11 | let invoiceResponse 12 | let rHash 13 | let res 14 | 15 | beforeEach(() => { 16 | orderId = '928uq9afds8as9df_fasdfj' 17 | value = '100' 18 | rHash = Buffer.from('1234') 19 | invoiceResponse = { rHash } 20 | addInvoiceStub = sinon.stub().resolves(invoiceResponse) 21 | clientStub = sinon.stub() 22 | 23 | createSwapHash.__set__('addInvoice', addInvoiceStub) 24 | createSwapHash.__set__('client', clientStub) 25 | }) 26 | 27 | beforeEach(async () => { 28 | res = await createSwapHash(orderId, value) 29 | }) 30 | 31 | it('adds an invoice through lnd', () => { 32 | expect(addInvoiceStub).to.have.been.calledWith({ memo: `sparkswap-swap-terminus:${orderId}`, expiry: '3600', value }, sinon.match({ client: clientStub })) 33 | }) 34 | 35 | it('returns an invoice hash hash', () => { 36 | expect(res).to.be.eql(rHash) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/engine-actions/withdraw-funds.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const withdrawFunds = rewire(path.resolve(__dirname, 'withdraw-funds')) 5 | 6 | describe('withdrawFunds', () => { 7 | let sendCoinsStub 8 | let clientStub 9 | let addr 10 | let amount 11 | let res 12 | let logger 13 | let txid 14 | 15 | beforeEach(() => { 16 | clientStub = sinon.stub() 17 | addr = 'asdfasdf' 18 | amount = 20000000 19 | logger = { 20 | debug: sinon.stub() 21 | } 22 | 23 | txid = 'asdfasdfasdfsdf2134' 24 | sendCoinsStub = sinon.stub().resolves({ txid }) 25 | withdrawFunds.__set__('sendCoins', sendCoinsStub) 26 | withdrawFunds.__set__('client', clientStub) 27 | withdrawFunds.__set__('logger', logger) 28 | }) 29 | 30 | it('makes a request to lnd to move coins from lnd wallet to address', async () => { 31 | res = await withdrawFunds(addr, amount) 32 | expect(sendCoinsStub).to.have.been.calledWith(addr, amount, { client: clientStub }) 33 | }) 34 | 35 | it('returns the transaction id', async () => { 36 | res = await withdrawFunds(addr, amount) 37 | expect(res).to.eql(txid) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/engine-actions/create-invoice.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const createInvoice = rewire(path.resolve(__dirname, 'create-invoice')) 5 | 6 | describe('createInvoice', () => { 7 | let memo 8 | let expiry 9 | let value 10 | let addInvoiceStub 11 | let clientStub 12 | let invoiceResponse 13 | let paymentRequest 14 | let res 15 | 16 | beforeEach(() => { 17 | memo = 'MEMO' 18 | expiry = '2000' 19 | value = '100' 20 | paymentRequest = '1234' 21 | invoiceResponse = { paymentRequest } 22 | addInvoiceStub = sinon.stub().returns(invoiceResponse) 23 | clientStub = sinon.stub() 24 | 25 | createInvoice.__set__('addInvoice', addInvoiceStub) 26 | createInvoice.__set__('client', clientStub) 27 | }) 28 | 29 | beforeEach(async () => { 30 | res = await createInvoice(memo, expiry, value) 31 | }) 32 | 33 | it('adds an invoice through lnd', () => { 34 | expect(addInvoiceStub).to.have.been.calledWith(sinon.match({ memo, expiry, value }), sinon.match({ client: clientStub })) 35 | }) 36 | 37 | it('returns a paymentRequest hash', () => { 38 | expect(res).to.be.eql(paymentRequest) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/lnd-actions/add-invoice.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Creates an invoice on lnd 7 | * 8 | * @see http://api.lightning.community/#addinvoice 9 | * @param {object} params 10 | * @param {string} params.memo 11 | * @param {string} params.expiry - invoice expiry in seconds 12 | * @param {string} params.value 13 | * @param {boolean} [params.externalPreimage] - Whether the preimage is stored locally or on an external server 14 | * @param {string} [params.rHash] - Optional Base64 string of the hash for the invoice 15 | * @param {object} opts 16 | * @param {LndClient} opts.client 17 | * @returns {Promise<{rHash: string, paymentRequest: string}>} 18 | */ 19 | function addInvoice ({ memo, expiry, value, externalPreimage, rHash }, { client }) { 20 | const params = { 21 | memo, 22 | expiry, 23 | value, 24 | externalPreimage, 25 | rHash 26 | } 27 | 28 | return new Promise((resolve, reject) => { 29 | client.addInvoice(params, { deadline: deadline() }, (err, res) => { 30 | if (err) return reject(err) 31 | return resolve(res) 32 | }) 33 | }) 34 | } 35 | 36 | module.exports = addInvoice 37 | -------------------------------------------------------------------------------- /src/engine-actions/create-wallet.js: -------------------------------------------------------------------------------- 1 | const { 2 | genSeed, 3 | initWallet 4 | } = require('../lnd-actions') 5 | 6 | /** @typedef {import('../lnd-setup').LndWalletUnlockerClient} WalletUnlocker */ 7 | /** @typedef {{walletUnlocker: WalletUnlocker}} WalletUnlockerObject */ 8 | 9 | /** 10 | * Creates a wallet 11 | * 12 | * @param {string} password - wallet password, used to unlock lnd wallet 13 | * @returns {Promise>} 24 word cipher seed mnemonic 14 | * @this WalletUnlockerObject 15 | */ 16 | async function createWallet (password) { 17 | const { cipherSeedMnemonic } = await genSeed({ client: this.walletUnlocker }) 18 | 19 | if (typeof password !== 'string') { 20 | throw new Error('Provided password must be a string value') 21 | } 22 | 23 | // Password must be converted to buffer in order for lnd to accept 24 | // as it does not accept String password types at this time. 25 | const walletPassword = Buffer.from(password, 'utf8') 26 | 27 | // This call merely resolves or rejects, there is no return value when the 28 | // wallet is initialized 29 | await initWallet(walletPassword, cipherSeedMnemonic, { client: this.walletUnlocker }) 30 | 31 | return cipherSeedMnemonic 32 | } 33 | 34 | module.exports = createWallet 35 | -------------------------------------------------------------------------------- /src/lnd-actions/open-channel.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * Open a channel w/ LND 7 | * 8 | * @param {object} params 9 | * @param {string} params.nodePubkey - lnd public key to open channel with 10 | * @param {string} params.localFundingAmount - the amount to fund the channel w/ 11 | * @param {number} params.targetConf - Number of blocks the channel opening transaction should be confirmed by 12 | * @param {boolean} params.private - whether to make the channel private 13 | * @param {object} opts 14 | * @param {LndClient} opts.client 15 | * @returns {Promise} 16 | */ 17 | function openChannel (params, { client }) { 18 | // replace the bytes version of the pub key with the string version 19 | const pubkeyParams = params.nodePubkey 20 | ? { nodePubkeyString: params.nodePubkey, nodePubkey: undefined } 21 | : {} 22 | 23 | const lndParams = Object.assign({}, params, pubkeyParams) 24 | 25 | return new Promise((resolve, reject) => { 26 | client.openChannelSync(lndParams, { deadline: deadline() }, (err, res) => { 27 | if (err) return reject(err) 28 | return resolve(res) 29 | }) 30 | }) 31 | } 32 | 33 | module.exports = openChannel 34 | -------------------------------------------------------------------------------- /src/engine-actions/get-uncommitted-pending-balance.js: -------------------------------------------------------------------------------- 1 | const { walletBalance, listPendingChannels } = require('../lnd-actions') 2 | const { Big } = require('../utils') 3 | 4 | /** 5 | * Total balance of unspent funds 6 | * @returns {Promise} total 7 | */ 8 | async function getUncommittedPendingBalance () { 9 | const { unconfirmedBalance } = await walletBalance({ client: this.client }) 10 | const { pendingForceClosingChannels = [] } = await listPendingChannels({ client: this.client }) 11 | 12 | /** 13 | * When closing a channel, we were getting a duplicated pending unconfirmed balance. 14 | * This is because unconfirmedBalance for the wallet and waitingCloseChannels from listChannels contain the same information. 15 | * We do not need waitingCloseChannels for this reason, and we remove pendingClosingChannels because 16 | * it is deprecated (https://github.com/lightningnetwork/lnd/blob/15c9b389fa097b6beb0b0e8f24ba78a9d1254693/rpcserver.go#L2167). 17 | */ 18 | const totalPendingLocalBalance = pendingForceClosingChannels.reduce((acc, c) => { 19 | return acc.plus(c.channel.localBalance) 20 | }, Big(0)) 21 | 22 | return Big(unconfirmedBalance).add(totalPendingLocalBalance).toString() 23 | } 24 | 25 | module.exports = getUncommittedPendingBalance 26 | -------------------------------------------------------------------------------- /src/utils/load-proto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load Proto 3 | * @module src/utils/load-proto 4 | */ 5 | 6 | const grpc = require('grpc') 7 | const grpcProtoLoader = require('@grpc/proto-loader') 8 | 9 | /** 10 | * Default values for grpc/proto-loader that mimic the default behaivor 11 | * of grpc. 12 | * 13 | * @function 14 | * @param {string} basePath - path to lnrpc directory 15 | * @returns {object} 16 | */ 17 | function getGrpcOptions (basePath) { 18 | return { 19 | longs: String, 20 | bytes: String, 21 | enums: String, 22 | defaults: true, 23 | oneofs: true, 24 | includeDirs: [ 25 | basePath 26 | ] 27 | } 28 | } 29 | 30 | /** 31 | * Generates a proto definition for a specified proto file path 32 | * 33 | * @function 34 | * @private 35 | * @param {string} basePath - lnrpc directory path 36 | * @param {ReadonlyArray} relativePaths - path to proto file within basePath 37 | * @returns {object} 38 | * @throws {Error} proto file not found 39 | */ 40 | function loadProto (basePath, relativePaths) { 41 | const options = getGrpcOptions(basePath) 42 | // @ts-ignore 43 | const packageDefinition = grpcProtoLoader.loadSync(relativePaths, options) 44 | return grpc.loadPackageDefinition(packageDefinition) 45 | } 46 | 47 | module.exports = loadProto 48 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-channel-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getTotalChannelBalance = rewire(path.resolve(__dirname, 'get-total-channel-balance')) 5 | 6 | describe('get-total-channel-balance', () => { 7 | describe('getTotalChannelBalance', () => { 8 | let channels 9 | let listChannelsStub 10 | let logger 11 | 12 | beforeEach(() => { 13 | channels = [ 14 | { localBalance: '10' }, 15 | { localBalance: '1000' } 16 | ] 17 | 18 | logger = { 19 | debug: sinon.stub() 20 | } 21 | 22 | getTotalChannelBalance.__set__('logger', logger) 23 | }) 24 | 25 | it('returns 0 if no channels exist', async () => { 26 | listChannelsStub = sinon.stub().returns({}) 27 | getTotalChannelBalance.__set__('listChannels', listChannelsStub) 28 | return expect(await getTotalChannelBalance()).to.be.eql('0') 29 | }) 30 | 31 | it('returns the total balance of all channels on a daemon', async () => { 32 | listChannelsStub = sinon.stub().returns({ channels }) 33 | getTotalChannelBalance.__set__('listChannels', listChannelsStub) 34 | return expect(await getTotalChannelBalance()).to.be.eql('1010') 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/engine-actions/pay-invoice.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const payInvoice = rewire(path.resolve(__dirname, 'pay-invoice')) 5 | 6 | describe('pay-invoice', () => { 7 | let sendPaymentStub 8 | let clientStub 9 | let payment 10 | let paymentError 11 | let paymentRequest 12 | let res 13 | let logger 14 | let paymentPreimage 15 | 16 | beforeEach(() => { 17 | clientStub = sinon.stub() 18 | paymentError = null 19 | paymentPreimage = 'asdfasdf' 20 | payment = { paymentError, paymentPreimage } 21 | paymentRequest = 'INVOICE_PAYMENT_REQUEST' 22 | logger = { 23 | info: sinon.stub(), 24 | debug: sinon.stub() 25 | } 26 | 27 | sendPaymentStub = sinon.stub().returns(payment) 28 | payInvoice.__set__('sendPayment', sendPaymentStub) 29 | payInvoice.__set__('client', clientStub) 30 | payInvoice.__set__('logger', logger) 31 | }) 32 | 33 | beforeEach(async () => { 34 | res = await payInvoice(paymentRequest) 35 | }) 36 | 37 | it('sends a payment to lnd', () => { 38 | expect(sendPaymentStub).to.have.been.calledWith({ paymentRequest }, { client: clientStub }) 39 | }) 40 | 41 | it('returns the payment preimage', () => { 42 | expect(res).to.eql(paymentPreimage) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/lnd-actions/query-routes.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * @typedef {object} FeeLimit 7 | * @property {string} fixed 8 | */ 9 | 10 | /** 11 | * Find available routes to a destination 12 | * 13 | * @function 14 | * @param {object} params 15 | * @param {string} params.pubKey - Public key of the node to find routes to 16 | * @param {string} params.amt - Number of satoshis to send 17 | * @param {number} params.numRoutes - Max number of routes to return 18 | * @param {number} params.finalCltvDelta - CLTV delta to be used for the final hop 19 | * @param {FeeLimit} params.feeLimit - Int64 string of max number of satoshis to pay in fees 20 | * @param {object} opts 21 | * @param {LndClient} opts.client 22 | * @returns {Promise} 23 | */ 24 | function queryRoutes ({ pubKey, amt, numRoutes, finalCltvDelta, feeLimit }, { client }) { 25 | return new Promise((resolve, reject) => { 26 | try { 27 | client.queryRoutes({ pubKey, amt, numRoutes, finalCltvDelta, feeLimit }, { deadline: deadline() }, (err, res) => { 28 | if (err) return reject(err) 29 | return resolve(res) 30 | }) 31 | } catch (e) { 32 | reject(e) 33 | } 34 | }) 35 | } 36 | 37 | module.exports = queryRoutes 38 | -------------------------------------------------------------------------------- /src/lnd-actions/track-payment.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 2 | /** @typedef {import('grpc').ClientReadableStream} ClientReadableStream */ 3 | 4 | /** 5 | * Payment Status corresponding to the output of LND's TrackPayment RPC 6 | * 7 | * IN_FLIGHT - Payment is still in flight 8 | * SUCCEEDED - Payment completed successfully 9 | * FAILED_TIMEOUT - There are more routes to try, but the payment timeout was exceeded 10 | * FAILED_NO_ROUTE - All possible routes were tried and failed permanently. Or there 11 | * were no routes to the destination at all. 12 | * 13 | * @constant 14 | * @type {object} 15 | * @default 16 | */ 17 | const PAYMENT_STATUSES = Object.freeze({ 18 | IN_FLIGHT: 'IN_FLIGHT', 19 | SUCCEEDED: 'SUCCEEDED', 20 | FAILED_TIMEOUT: 'FAILED_TIMEOUT', 21 | FAILED_NO_ROUTE: 'FAILED_NO_ROUTE' 22 | }) 23 | 24 | /** 25 | * Tracks an existing payment 26 | * @param {string} paymentHash - Base64 encoded payment to track 27 | * @param {object} opts 28 | * @param {LndClient} opts.client 29 | * @returns {ClientReadableStream} Readable stream from grpc 30 | */ 31 | function trackPayment (paymentHash, { client }) { 32 | return client.router.trackPayment({ paymentHash }) 33 | } 34 | 35 | trackPayment.STATUSES = PAYMENT_STATUSES 36 | 37 | module.exports = trackPayment 38 | -------------------------------------------------------------------------------- /src/engine-actions/connect-user.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const connectUser = rewire(path.resolve(__dirname, 'connect-user')) 5 | 6 | describe('connect-user', () => { 7 | let addressFormatterStub 8 | let paymentChannelNetworkAddress 9 | let host 10 | let publicKey 11 | let connectPeerStub 12 | let client 13 | let logger 14 | 15 | beforeEach(() => { 16 | client = sinon.stub() 17 | logger = sinon.stub() 18 | publicKey = '1234' 19 | host = 'localhost' 20 | paymentChannelNetworkAddress = 'bolt:1234@localhost' 21 | 22 | connectPeerStub = sinon.stub() 23 | addressFormatterStub = { 24 | parse: sinon.stub().returns({ publicKey, host }) 25 | } 26 | 27 | connectUser.__set__('networkAddressFormatter', addressFormatterStub) 28 | connectUser.__set__('connectPeer', connectPeerStub) 29 | }) 30 | 31 | beforeEach(async () => { 32 | await connectUser.call({ client, logger }, paymentChannelNetworkAddress) 33 | }) 34 | 35 | it('parses a network address', () => { 36 | expect(addressFormatterStub.parse).to.have.been.calledWith(paymentChannelNetworkAddress) 37 | }) 38 | 39 | it('connects to a peer', () => { 40 | expect(connectPeerStub).to.have.been.calledWith(publicKey, host, { client, logger }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/lnd-setup/generate-wallet-unlocker-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wallet Unlocker Client Module 3 | * @module src/lnd-setup/generate-wallet-unlocker-client 4 | */ 5 | 6 | /** @typedef {import('.').LndWalletUnlockerClient} WalletUnlocker */ 7 | 8 | const grpc = require('grpc') 9 | const loadProto = require('../utils/load-proto') 10 | const fs = require('fs') 11 | 12 | /** 13 | * Array of lnd proto files to load to generate wallet unlocker 14 | * @type {ReadonlyArray} 15 | */ 16 | const PROTO_FILES = Object.freeze(['rpc.proto']) 17 | 18 | /** 19 | * Generates a lnrpc.WalletUnlocker client which is only used on initialization of the LND 20 | * node. 21 | * 22 | * @param {object} args 23 | * @param {string} args.host 24 | * @param {string} args.protoPath 25 | * @param {string} args.tlsCertPath 26 | * @returns {WalletUnlocker} 27 | */ 28 | function generateWalletUnlockerClient ({ host, protoPath, tlsCertPath }) { 29 | const { lnrpc } = loadProto(protoPath, PROTO_FILES) 30 | 31 | if (!fs.existsSync(tlsCertPath)) { 32 | throw new Error(`LND-ENGINE error - tls cert file not found at path: ${tlsCertPath}`) 33 | } 34 | 35 | const tls = fs.readFileSync(tlsCertPath) 36 | const tlsCredentials = grpc.credentials.createSsl(tls) 37 | 38 | return new lnrpc.WalletUnlocker(host, tlsCredentials) 39 | } 40 | 41 | module.exports = generateWalletUnlockerClient 42 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-pending-channel-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getTotalPendingChannelBalance = rewire(path.resolve(__dirname, 'get-total-pending-channel-balance')) 5 | 6 | describe('getTotalPendingChannelBalance', () => { 7 | let pendingOpenChannels 8 | let listPendingChannelsStub 9 | let logger 10 | 11 | beforeEach(() => { 12 | pendingOpenChannels = [ 13 | { channel: { localBalance: '10' } }, 14 | { channel: { localBalance: '1000' } } 15 | ] 16 | 17 | logger = { 18 | debug: sinon.stub() 19 | } 20 | 21 | getTotalPendingChannelBalance.__set__('logger', logger) 22 | }) 23 | 24 | it('returns 0 if no channels exist', async () => { 25 | listPendingChannelsStub = sinon.stub().returns({}) 26 | getTotalPendingChannelBalance.__set__('listPendingChannels', listPendingChannelsStub) 27 | return expect(await getTotalPendingChannelBalance()).to.be.eql('0') 28 | }) 29 | 30 | it('returns the total balance of all channels on a daemon', async () => { 31 | listPendingChannelsStub = sinon.stub().returns({ pendingOpenChannels }) 32 | getTotalPendingChannelBalance.__set__('listPendingChannels', listPendingChannelsStub) 33 | return expect(await getTotalPendingChannelBalance()).to.be.eql('1010') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/engine-actions/get-channels-for-remote-address.js: -------------------------------------------------------------------------------- 1 | const { networkAddressFormatter } = require('../utils') 2 | const { listChannels, listPendingChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get all channels to the given address 6 | * @param {string} address 7 | * @returns {Promise} channels 8 | */ 9 | async function getChannelsForRemoteAddress (address) { 10 | const [ 11 | { channels = [] }, 12 | { pendingOpenChannels = [] } 13 | ] = await Promise.all([ 14 | // The response from listChannels consists of channels that may be active or inactive 15 | listChannels({ client: this.client }), 16 | listPendingChannels({ client: this.client }) 17 | ]) 18 | 19 | const normalizedPendingChannels = pendingOpenChannels.map(chan => chan.channel) 20 | 21 | if (channels.length === 0 && normalizedPendingChannels.length === 0) { 22 | this.logger.debug('getChannelsForRemoteAddress: No channels exist') 23 | return [] 24 | } 25 | 26 | const { publicKey } = networkAddressFormatter.parse(address) 27 | 28 | const channelsForPubkey = channels.filter(channel => channel.remotePubkey === publicKey) 29 | 30 | const pendingChannelsForPubkey = normalizedPendingChannels.filter(channel => channel.remoteNodePub === publicKey) 31 | 32 | return channelsForPubkey.concat(pendingChannelsForPubkey) 33 | } 34 | 35 | module.exports = getChannelsForRemoteAddress 36 | -------------------------------------------------------------------------------- /src/engine-actions/get-peers.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getPeers = rewire(path.resolve(__dirname, 'get-peers')) 5 | 6 | describe('get-peers', () => { 7 | let listPeersStub 8 | let peers 9 | let client 10 | 11 | beforeEach(() => { 12 | peers = [ 13 | { pubKey: '1234', address: '192.168.0.1:10009', inbound: false }, 14 | { pubKey: '5678', address: '192.168.0.2:10009', inbound: false } 15 | ] 16 | client = sinon.stub() 17 | listPeersStub = sinon.stub().resolves({ peers }) 18 | getPeers.__set__('listPeers', listPeersStub) 19 | }) 20 | 21 | it('gets peers from lnd', async () => { 22 | await getPeers.call({ client }) 23 | expect(listPeersStub).to.have.been.calledWith({ client }) 24 | }) 25 | 26 | it('formats peers', async () => { 27 | const expectedRes = [ 28 | { pubKey: '1234', address: '192.168.0.1:10009' }, 29 | { pubKey: '5678', address: '192.168.0.2:10009' } 30 | ] 31 | const res = await getPeers.call({ client }) 32 | expect(res).to.eql(expectedRes) 33 | expect(res).to.not.eql(sinon.match(peers)) 34 | }) 35 | 36 | it('returns an empty array if no peers are available', async () => { 37 | listPeersStub.resolves({}) 38 | const res = await getPeers.call({ client }) 39 | expect(res).to.be.eql([]) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/engine-actions/get-max-channel-for-address.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const getChannelsForRemoteAddress = require('./get-channels-for-remote-address') 3 | 4 | /** 5 | * Get maximum balance from all channels (inbound or outbound) for a given address 6 | * @param {string} address 7 | * @param {object} [options={}] 8 | * @param {boolean} [options.outbound=true] - outbound is true if checking outbound channels, false if inbound 9 | * @returns {Promise<{maxBalance: string}>} - the max balance in all open channels. 10 | */ 11 | async function getMaxChannelForAddress (address, { outbound = true } = {}) { 12 | const channelsForAddress = await getChannelsForRemoteAddress.call(this, address) 13 | 14 | if (!channelsForAddress.length) { 15 | this.logger.debug('getMaxChannelForAddress: No open or pending channels exist') 16 | return { maxBalance: '0' } 17 | } 18 | 19 | const balanceType = outbound ? 'localBalance' : 'remoteBalance' 20 | 21 | const maxBalance = channelsForAddress.reduce((max, channel) => { 22 | if (Big(channel[balanceType]).gt(max)) { 23 | return Big(channel[balanceType]) 24 | } else { 25 | return max 26 | } 27 | }, Big('0')) 28 | 29 | this.logger.debug(`getMaxChannelForAddress: max open channel to address is: ${maxBalance.toString()}`) 30 | 31 | return { maxBalance: maxBalance.toString() } 32 | } 33 | 34 | module.exports = getMaxChannelForAddress 35 | -------------------------------------------------------------------------------- /src/lnd-actions/connect-peer.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('..').Logger} Logger */ 4 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 5 | 6 | /** 7 | * Given an error, detects if the error message says that the peer is already 8 | * connected 9 | * 10 | * @private 11 | * @param {object} err 12 | * @param {number} err.code 13 | * @param {string} err.details 14 | * @returns {boolean} 15 | */ 16 | function alreadyConnected (err) { 17 | return (err && err.code === 2 && err.details != null && err.details.includes('already connected to peer')) 18 | } 19 | 20 | /** 21 | * Creates a new connection to an lnd node 22 | * 23 | * @param {string} publicKey 24 | * @param {string} host 25 | * @param {object} opts 26 | * @param {LndClient} opts.client 27 | * @param {Logger} opts.logger 28 | * @returns {Promise} 29 | */ 30 | function connectPeer (publicKey, host, { client, logger }) { 31 | const addr = { 32 | pubkey: publicKey, 33 | host 34 | } 35 | 36 | return new Promise((resolve, reject) => { 37 | client.connectPeer({ addr }, { deadline: deadline() }, (err, res) => { 38 | if (alreadyConnected(err)) { 39 | logger.info(`Peer already connected: ${publicKey}`) 40 | return resolve() 41 | } else if (err) { 42 | return reject(err) 43 | } 44 | 45 | return resolve(res) 46 | }) 47 | }) 48 | } 49 | 50 | module.exports = connectPeer 51 | -------------------------------------------------------------------------------- /src/engine-actions/create-refund-invoice.js: -------------------------------------------------------------------------------- 1 | const { addInvoice, decodePaymentRequest } = require('../lnd-actions') 2 | 3 | /** 4 | * DEFAULT_INVOICE_EXPIRY 5 | * Default value is 1 year expiry for invoices (in seconds) 6 | * @constant 7 | * @type {string} 8 | * @default 9 | */ 10 | const DEFAULT_INVOICE_EXPIRY = '31536000' 11 | 12 | /** 13 | * @constant 14 | * @type {string} 15 | * @default 16 | */ 17 | const REFUND_MEMO_PREFIX = 'REFUND:' 18 | 19 | /** 20 | * Creates an invoice 21 | * 22 | * @param {string} paymentRequest 23 | * @returns {Promise} paymentRequest hash of invoice from lnd 24 | */ 25 | async function createRefundInvoice (paymentRequest) { 26 | const { numSatoshis: requestValue, description: requestDescription } = await decodePaymentRequest(paymentRequest, { client: this.client }) 27 | 28 | this.logger.debug('Attempting to create invoice', { requestValue, paymentRequest }) 29 | 30 | // TODO: Use the settled value from an invoice lookup instead of the value from a decoded 31 | // payment request 32 | // see: https://trello.com/c/wzxVUNZl/288-check-fee-refund-values-on-relayer 33 | const params = { 34 | memo: `${REFUND_MEMO_PREFIX} ${requestDescription}`, 35 | expiry: DEFAULT_INVOICE_EXPIRY, 36 | value: requestValue 37 | } 38 | const { paymentRequest: refundPaymentRequest } = await addInvoice(params, { client: this.client }) 39 | 40 | return refundPaymentRequest 41 | } 42 | 43 | module.exports = createRefundInvoice 44 | -------------------------------------------------------------------------------- /src/utils/load-proto.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { rewire, sinon, expect } = require('test/test-helper') 3 | 4 | const loadProto = rewire(path.resolve('src', 'utils', 'load-proto')) 5 | 6 | describe('loadProto', () => { 7 | let existsSyncStub 8 | let loadSyncStub 9 | let packageDefinition 10 | let loadPackageStub 11 | 12 | beforeEach(() => { 13 | existsSyncStub = sinon.stub() 14 | packageDefinition = sinon.stub() 15 | loadSyncStub = sinon.stub() 16 | loadPackageStub = sinon.stub() 17 | 18 | loadProto.__set__('fs', { 19 | existsSync: existsSyncStub 20 | }) 21 | loadProto.__set__('grpcProtoLoader', { 22 | loadSync: loadSyncStub.returns(packageDefinition) 23 | }) 24 | loadProto.__set__('grpc', { 25 | loadPackageDefinition: loadPackageStub 26 | }) 27 | }) 28 | 29 | it('creates a grpc package definition', () => { 30 | existsSyncStub.returns(true) 31 | const getGrpcOptions = loadProto.__get__('getGrpcOptions') 32 | const basePath = '/tmp/' 33 | const goodFile = 'myfile.good' 34 | const options = getGrpcOptions(basePath) 35 | loadProto(basePath, goodFile) 36 | expect(loadSyncStub).to.be.calledWith(goodFile, options) 37 | }) 38 | 39 | it('loads a grpc package definition', () => { 40 | existsSyncStub.returns(true) 41 | const goodFile = 'myfile.good' 42 | loadProto('/tmp', goodFile) 43 | expect(loadPackageStub).to.be.calledWith(packageDefinition) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/engine-actions/num-channels-for-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const numChannelsForAddress = rewire(path.resolve(__dirname, 'num-channels-for-address')) 5 | 6 | describe('numChannelsForAddress', () => { 7 | let getChannelsForRemoteAddress 8 | let channels 9 | let channel 10 | let reverts 11 | let address 12 | let loggerStub 13 | 14 | beforeEach(() => { 15 | address = 'bolt:asdf@localhost' 16 | channel = { remotePubkey: 'asdf' } 17 | channels = [channel, channel] 18 | loggerStub = { 19 | debug: sinon.stub(), 20 | error: sinon.stub() 21 | } 22 | getChannelsForRemoteAddress = sinon.stub().resolves(channels) 23 | reverts = [] 24 | reverts.push(numChannelsForAddress.__set__('getChannelsForRemoteAddress', getChannelsForRemoteAddress)) 25 | reverts.push(numChannelsForAddress.__set__('logger', loggerStub)) 26 | }) 27 | 28 | afterEach(() => { 29 | reverts.forEach(r => r()) 30 | }) 31 | 32 | it('gets all channels for the given address', async () => { 33 | await numChannelsForAddress(address) 34 | expect(getChannelsForRemoteAddress).to.have.been.called() 35 | expect(getChannelsForRemoteAddress).to.have.been.calledWith(address) 36 | }) 37 | 38 | it('returns the number of active and pending channels for the given pubkey', async () => { 39 | const res = await numChannelsForAddress(address) 40 | expect(res).to.eql(2) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/engine-actions/get-pending-channel-capacities.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getPendingChannelCapacities = rewire(path.resolve(__dirname, 'get-pending-channel-capacities')) 5 | 6 | describe('getPendingChannelCapacities', () => { 7 | let pendingOpenChannels 8 | let listChannelsStub 9 | let logger 10 | 11 | beforeEach(() => { 12 | pendingOpenChannels = [ 13 | { channel: { localBalance: '10', remoteBalance: '300' } }, 14 | { channel: { localBalance: '0', remoteBalance: '100' } } 15 | ] 16 | 17 | logger = { 18 | debug: sinon.stub() 19 | } 20 | 21 | getPendingChannelCapacities.__set__('logger', logger) 22 | }) 23 | 24 | it('returns 0 for active and inactive if no channels exist', async () => { 25 | const expectedRes = { localBalance: '0', remoteBalance: '0' } 26 | listChannelsStub = sinon.stub().resolves({}) 27 | getPendingChannelCapacities.__set__('listPendingChannels', listChannelsStub) 28 | return expect(await getPendingChannelCapacities()).to.eql(expectedRes) 29 | }) 30 | 31 | it('returns the total balance of all channels on a daemon', async () => { 32 | const expectedRes = { localBalance: '10', remoteBalance: '400' } 33 | listChannelsStub = sinon.stub().resolves({ pendingOpenChannels }) 34 | getPendingChannelCapacities.__set__('listPendingChannels', listChannelsStub) 35 | return expect(await getPendingChannelCapacities()).to.eql(expectedRes) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/engine-actions/get-invoices.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getInvoices = rewire(path.resolve(__dirname, 'get-invoices')) 5 | 6 | describe('getInvoices', () => { 7 | let pendingOnly 8 | let listInvoicesStub 9 | let clientStub 10 | let invoiceResponse 11 | 12 | beforeEach(() => { 13 | pendingOnly = true 14 | invoiceResponse = [] 15 | listInvoicesStub = sinon.stub().returns(invoiceResponse) 16 | clientStub = sinon.stub() 17 | 18 | getInvoices.__set__('listInvoices', listInvoicesStub) 19 | getInvoices.__set__('client', clientStub) 20 | }) 21 | 22 | describe('pendingOnly', () => { 23 | it('defaults to false if no params are passed in', async () => { 24 | await getInvoices() 25 | expect(listInvoicesStub).to.have.been.calledWith(false, sinon.match({ client: clientStub })) 26 | }) 27 | it('defaults to false if no pendingOnly value exists', async () => { 28 | await getInvoices({ banana: 1234 }) 29 | expect(listInvoicesStub).to.have.been.calledWith(false, sinon.match({ client: clientStub })) 30 | }) 31 | }) 32 | 33 | it('gets a list of invoices from lnd', async () => { 34 | await getInvoices({ pendingOnly }) 35 | expect(listInvoicesStub).to.have.been.calledWith(pendingOnly, sinon.match({ client: clientStub })) 36 | }) 37 | 38 | it('returns the result', async () => { 39 | const res = await getInvoices({ pendingOnly }) 40 | expect(res).to.be.eql(invoiceResponse) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/engine-actions/get-open-channel-capacities.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get local balance of all channels for a specific daemon 6 | * 7 | * @returns {Promise} active and inactive balances 8 | */ 9 | async function getOpenChannelCapacities () { 10 | const { channels = [] } = await listChannels({ client: this.client }) 11 | 12 | if (channels.length === 0) { 13 | this.logger.debug('getOpenChannelCapacities: No channels exist') 14 | } 15 | 16 | const activeLocalBalance = channels.filter((chan) => chan.active === true).reduce((acc, c) => { 17 | return acc.plus(c.localBalance) 18 | }, Big(0)) 19 | 20 | const activeRemoteBalance = channels.filter((chan) => chan.active === true).reduce((acc, c) => { 21 | return acc.plus(c.remoteBalance) 22 | }, Big(0)) 23 | 24 | const activeBalances = { localBalance: activeLocalBalance.toString(), remoteBalance: activeRemoteBalance.toString() } 25 | 26 | const inactiveLocalBalance = channels.filter((chan) => chan.active === false).reduce((acc, c) => { 27 | return acc.plus(c.localBalance) 28 | }, Big(0)) 29 | 30 | const inactiveRemoteBalance = channels.filter((chan) => chan.active === false).reduce((acc, c) => { 31 | return acc.plus(c.remoteBalance) 32 | }, Big(0)) 33 | 34 | const inactiveBalances = { localBalance: inactiveLocalBalance.toString(), remoteBalance: inactiveRemoteBalance.toString() } 35 | 36 | return { active: activeBalances, inactive: inactiveBalances } 37 | } 38 | 39 | module.exports = getOpenChannelCapacities 40 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "currencies": [ 3 | { 4 | "name": "Bitcoin", 5 | "chainName": "bitcoin", 6 | "symbol": "BTC", 7 | "quantumsPerCommon": "100000000", 8 | "secondsPerBlock": 600, 9 | "feeEstimate": "20000", 10 | "//1": "to prevent uneconomic channels", 11 | "minChannelBalance": "250000", 12 | "//2": "from bolt-0002, max channel size is 2^24 - 1 satoshis", 13 | "maxChannelBalance": "16777215", 14 | "//3": "LND specifies max payment size as UInt32 - 1 millisatohis (https://github.com/lightningnetwork/lnd/blob/v0.5.1-beta/rpcserver.go#L49-L58)", 15 | "maxPaymentSize": "4294967", 16 | "//4": "LND static channel backup file path", 17 | "backupFilePath": "/backup/lnd_btc.backup" 18 | }, 19 | { 20 | "name": "Litecoin", 21 | "chainName": "litecoin", 22 | "symbol": "LTC", 23 | "quantumsPerCommon": "100000000", 24 | "secondsPerBlock": 150, 25 | "feeEstimate": "2000000", 26 | "//1": "to prevent uneconomic channels", 27 | "minChannelBalance": "15000000", 28 | "//2": "LND specifies max channel size as (2^24 - 1)*60 millilitoshis (https://github.com/lightningnetwork/lnd/blob/v0.5.1-beta/fundingmanager.go#L68)", 29 | "maxChannelBalance": "1006632900", 30 | "//3": "LND specifies max payment size as (UInt32 - 1)*60 millilitoshis (https://github.com/lightningnetwork/lnd/blob/v0.5.1-beta/rpcserver.go#L49-L58)", 31 | "maxPaymentSize": "257698037", 32 | "//4": "LND static channel backup file path", 33 | "backupFilePath": "/backup/lnd_ltc.backup" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/engine-actions/get-open-channel-capacities.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getOpenChannelCapacities = rewire(path.resolve(__dirname, 'get-open-channel-capacities')) 5 | 6 | describe('getOpenChannelCapacities', () => { 7 | let channels 8 | let listChannelsStub 9 | let logger 10 | 11 | beforeEach(() => { 12 | channels = [ 13 | { localBalance: '10', remoteBalance: '300', active: true }, 14 | { localBalance: '0', remoteBalance: '100', active: true }, 15 | { localBalance: '20', remoteBalance: '1000', active: false }, 16 | { localBalance: '10', remoteBalance: '15', active: true } 17 | ] 18 | 19 | logger = { 20 | debug: sinon.stub() 21 | } 22 | 23 | getOpenChannelCapacities.__set__('logger', logger) 24 | }) 25 | 26 | it('returns 0 for active and inactive if no channels exist', async () => { 27 | const expectedRes = { active: { localBalance: '0', remoteBalance: '0' }, inactive: { localBalance: '0', remoteBalance: '0' } } 28 | listChannelsStub = sinon.stub().returns({}) 29 | getOpenChannelCapacities.__set__('listChannels', listChannelsStub) 30 | return expect(await getOpenChannelCapacities()).to.eql(expectedRes) 31 | }) 32 | 33 | it('returns the total balance of all channels on a daemon', async () => { 34 | const expectedRes = { active: { localBalance: '20', remoteBalance: '415' }, inactive: { localBalance: '20', remoteBalance: '1000' } } 35 | listChannelsStub = sinon.stub().returns({ channels }) 36 | getOpenChannelCapacities.__set__('listChannels', listChannelsStub) 37 | return expect(await getOpenChannelCapacities()).to.eql(expectedRes) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/engine-actions/get-payment-channel-network-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getPaymentChannelNetworkAddress = rewire(path.resolve(__dirname, 'get-payment-channel-network-address')) 5 | 6 | describe('getPaymentChannelNetworkAddress', () => { 7 | let getInfoResponse 8 | let getInfoStub 9 | let clientStub 10 | let engine 11 | let res 12 | 13 | beforeEach(() => { 14 | getInfoResponse = { 15 | identityPubkey: '1234', 16 | uris: [ '1234@localhost:100789' ] 17 | } 18 | getInfoStub = sinon.stub().returns(getInfoResponse) 19 | clientStub = sinon.stub() 20 | 21 | engine = { client: clientStub } 22 | 23 | getPaymentChannelNetworkAddress.__set__('getInfo', getInfoStub) 24 | }) 25 | 26 | beforeEach(async () => { 27 | res = await getPaymentChannelNetworkAddress.call(engine) 28 | }) 29 | 30 | it('gets info on a specified lnd instance', () => { 31 | expect(getInfoStub).to.have.been.calledWith(sinon.match({ client: clientStub })) 32 | }) 33 | 34 | it('returns the formatted network address', () => { 35 | expect(res).to.be.eql('bolt:1234@localhost:100789') 36 | }) 37 | 38 | it('throws an error if no pubkey is returned from lnd', () => { 39 | getInfoStub.returns({}) 40 | expect(getPaymentChannelNetworkAddress.call(engine)).to.eventually.be.rejectedWith(' No pubkey exists') 41 | }) 42 | 43 | it('excludes host if no uris exist', async () => { 44 | getInfoStub.returns({ identityPubkey: getInfoResponse.identityPubkey }) 45 | res = await getPaymentChannelNetworkAddress.call(engine, { includeHost: false }) 46 | expect(res).to.be.eql('bolt:1234') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/lnd-actions/init-wallet.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndWalletUnlockerClient} WalletUnlocker */ 4 | 5 | /** 6 | * Initializes an lnd wallet 7 | * 8 | * @see http://api.lightning.community/#initWallet 9 | * @param {Buffer} walletPassword - password in bytes 10 | * @param {Array} cipherSeedMnemonic - generated from lnd (24 string array) 11 | * @param {object} opts 12 | * @param {?Buffer} [opts.backup] - binary lnd backup data 13 | * @param {number} [opts.recoveryWindow] - number of blocks for address lookback when restoring a wallet 14 | * @param {WalletUnlocker} opts.client 15 | * @returns {Promise} res - empty object for success 16 | */ 17 | function initWallet (walletPassword, cipherSeedMnemonic, { backup, recoveryWindow, client }) { 18 | return new Promise((resolve, reject) => { 19 | let params = { 20 | walletPassword, 21 | cipherSeedMnemonic, 22 | recoveryWindow 23 | } 24 | 25 | // initWallet is used in both the creation or recovery of a wallet. `backup` 26 | // is only required when trying to use initWallet to recover in-channel funds 27 | // from an engine 28 | if (backup) { 29 | params.backup = backup 30 | } 31 | 32 | // initWallet is used in both the creation or recovery of a wallet. recoveryWindow 33 | // is only required when trying to use initWallet to recover on-chain funds 34 | // from an engine 35 | if (recoveryWindow) { 36 | params.recoveryWindow = recoveryWindow 37 | } 38 | 39 | client.initWallet(params, { deadline: deadline() }, (err, res) => { 40 | if (err) return reject(err) 41 | return resolve(res) 42 | }) 43 | }) 44 | } 45 | 46 | module.exports = initWallet 47 | -------------------------------------------------------------------------------- /src/engine-actions/is-invoice-paid.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const isInvoicePaid = rewire(path.resolve(__dirname, 'is-invoice-paid')) 5 | 6 | describe('isInvoicePaid', () => { 7 | let paymentRequest 8 | let clientStub 9 | let res 10 | let lookupInvoiceStub 11 | let logger 12 | let decodePayReqStub 13 | let paymentHash 14 | 15 | beforeEach(() => { 16 | paymentHash = 'deadbeef' 17 | paymentRequest = '2345' 18 | lookupInvoiceStub = sinon.stub().resolves({ state: 'SETTLED' }) 19 | decodePayReqStub = sinon.stub().resolves({ paymentHash }) 20 | clientStub = sinon.stub() 21 | logger = { 22 | debug: sinon.stub() 23 | } 24 | 25 | isInvoicePaid.__set__('lookupInvoice', lookupInvoiceStub) 26 | isInvoicePaid.__set__('decodePaymentRequest', decodePayReqStub) 27 | isInvoicePaid.__set__('client', clientStub) 28 | isInvoicePaid.__set__('logger', logger) 29 | }) 30 | 31 | beforeEach(async () => { 32 | res = await isInvoicePaid(paymentRequest) 33 | }) 34 | 35 | it('decodes an invoice from the provided payment request', () => { 36 | expect(decodePayReqStub).to.have.been.calledWith(paymentRequest, sinon.match({ client: clientStub })) 37 | }) 38 | 39 | it('looks up the invoice by invoice hash', () => { 40 | expect(lookupInvoiceStub).to.have.been.calledWith({ rHash: '3q2+7w==' }, sinon.match({ client: clientStub })) 41 | }) 42 | 43 | it('returns true if the invoice is settled', () => { 44 | expect(res).to.be.true() 45 | }) 46 | 47 | it('returns false if the invoice is not settled', async () => { 48 | lookupInvoiceStub.resolves({ state: 'ACCEPTED' }) 49 | expect(await isInvoicePaid(paymentRequest)).to.be.false() 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /src/engine-actions/change-wallet-password.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const changeWalletPassword = rewire(path.resolve(__dirname, 'change-wallet-password')) 5 | 6 | describe('change-password', () => { 7 | const currentPassword = 'currentpass' 8 | const newPassword = 'newpass' 9 | 10 | let lndChangePasswordStub 11 | let engine 12 | let bufferStub 13 | let currentPasswordBuffer 14 | let newPasswordBuffer 15 | let bufferFromStub 16 | 17 | beforeEach(() => { 18 | engine = { 19 | walletUnlocker: sinon.stub 20 | } 21 | currentPasswordBuffer = sinon.stub() 22 | newPasswordBuffer = sinon.stub() 23 | bufferFromStub = sinon.stub() 24 | bufferFromStub.withArgs(currentPassword).returns(currentPasswordBuffer) 25 | bufferFromStub.withArgs(newPassword).returns(newPasswordBuffer) 26 | bufferStub = { 27 | from: bufferFromStub 28 | } 29 | lndChangePasswordStub = sinon.stub() 30 | 31 | changeWalletPassword.__set__('lndChangePassword', lndChangePasswordStub) 32 | changeWalletPassword.__set__('Buffer', bufferStub) 33 | }) 34 | 35 | beforeEach(async () => { 36 | await changeWalletPassword.call(engine, currentPassword, newPassword) 37 | }) 38 | 39 | it('converts the `currentPassword` string to buffer', () => { 40 | expect(bufferStub.from).to.have.been.calledWith(currentPassword, 'utf8') 41 | }) 42 | 43 | it('converts the `newPassword` string to buffer', () => { 44 | expect(bufferStub.from).to.have.been.calledWith(newPassword, 'utf8') 45 | }) 46 | 47 | it('changes a wallet password', () => { 48 | expect(lndChangePasswordStub).to.have.been.calledWith(currentPasswordBuffer, newPasswordBuffer, { client: engine.walletUnlocker }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/engine-actions/get-max-channel.js: -------------------------------------------------------------------------------- 1 | const { Big } = require('../utils') 2 | const { listChannels, listPendingChannels } = require('../lnd-actions') 3 | 4 | /** 5 | * Get maximum balance from all channels (inbound or outbound) 6 | * @param {object} [options={}] 7 | * @param {boolean} [options.outbound=true] - outbound is true if checking outbound channels, false if inbound 8 | * @returns {Promise<{maxBalance: string}>} - the max balance in all open channels 9 | */ 10 | async function getMaxChannel ({ outbound = true } = {}) { 11 | const [ 12 | { channels = [] } = {}, 13 | { pendingOpenChannels = [] } = {} 14 | ] = await Promise.all([ 15 | // The response from listChannels consists of channels that may be active or inactive 16 | listChannels({ client: this.client }), 17 | listPendingChannels({ client: this.client }) 18 | ]) 19 | const balanceType = outbound ? 'localBalance' : 'remoteBalance' 20 | 21 | if (!channels.length && !pendingOpenChannels.length) { 22 | this.logger.debug('getMaxChannel: No open or pending channels exist') 23 | return { maxBalance: '0' } 24 | } 25 | 26 | // We need to normalize pendingChannels here because their format is different 27 | // than those received from `listChannels` 28 | const pendingChannels = pendingOpenChannels.map(chan => chan.channel) 29 | 30 | const allChannels = channels.concat(pendingChannels) 31 | 32 | const maxBalance = allChannels.reduce((max, channel) => { 33 | if (Big(channel[balanceType]).gt(max)) { 34 | return Big(channel[balanceType]) 35 | } else { 36 | return max 37 | } 38 | }, Big('0')) 39 | 40 | this.logger.debug(`getMaxChannel: max open channel is: ${maxBalance.toString()}`) 41 | 42 | return { maxBalance: maxBalance.toString() } 43 | } 44 | 45 | module.exports = getMaxChannel 46 | -------------------------------------------------------------------------------- /docker/lnd/start-lnd-btc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NODE=${NODE:-bitcoind} 4 | CONFIG_FILE=/home/lnd/lnd.conf 5 | 6 | echo "LND BTC starting with network: $NETWORK $NODE" 7 | 8 | PARAMS=$(echo \ 9 | "--configfile=$CONFIG_FILE" \ 10 | "--bitcoin.$NETWORK" \ 11 | "--debuglevel=$DEBUG" \ 12 | "--$NODE.rpcuser=$RPC_USER" \ 13 | "--$NODE.rpcpass=$RPC_PASS" \ 14 | "--$NODE.rpchost=$RPC_HOST" \ 15 | "--$NODE.zmqpubrawblock=$ZMQPUBRAWBLOCK" \ 16 | "--$NODE.zmqpubrawtx=$ZMQPUBRAWTX" 17 | ) 18 | 19 | if [[ -n "$EXTERNAL_ADDRESS" ]] && [[ -n "$EXTERNAL_PORT" ]]; then 20 | if [[ "$EXTERNAL_ADDRESS" == "host.docker.internal" ]]; then 21 | # Using host.docker.internal to access the host IP does not work for Linux 22 | # This is a known issue with Docker: https://github.com/docker/for-linux/issues/264 23 | # Here, we manually map host.docker.internal to the host IP in /etc/hosts 24 | ping -q -c1 $EXTERNAL_ADDRESS > /dev/null 2>&1 25 | # We map host.docker.internal only if the container cannot ping the address 26 | # This is typically the case only for Linux 27 | if [ $? -ne 0 ]; then 28 | HOST_IP=$(ip route | awk 'NR==1 {print $3}') 29 | echo -e "$HOST_IP\t$EXTERNAL_ADDRESS" >> /etc/hosts 30 | fi 31 | fi 32 | echo "Setting external address for lnd $EXTERNAL_ADDRESS:$EXTERNAL_PORT" 33 | PARAMS="$PARAMS --externalip=$EXTERNAL_ADDRESS:$EXTERNAL_PORT" 34 | fi 35 | 36 | if [[ -n "$LND_BASE_FEE" ]]; then 37 | echo "Setting custom base fee for bitcoin: $LND_BASE_FEE" 38 | PARAMS="$PARAMS --bitcoin.basefee=$LND_BASE_FEE" 39 | fi 40 | 41 | if [[ -n "$LND_FEE_RATE" ]]; then 42 | echo "Setting custom fee rate for bitcoin: $LND_FEE_RATE" 43 | PARAMS="$PARAMS --bitcoin.feerate=$LND_FEE_RATE" 44 | fi 45 | 46 | exec lnd $PARAMS "$@" 47 | -------------------------------------------------------------------------------- /src/engine-actions/is-balance-sufficient.js: -------------------------------------------------------------------------------- 1 | const { listChannels } = require('../lnd-actions') 2 | const { Big, networkAddressFormatter } = require('../utils') 3 | 4 | /** 5 | * Returns a boolean, true if there is an active channel between caller and remote 6 | * parties that sufficient funds for an order, false if they are not 7 | * 8 | * @param {string} paymentChannelNetworkAddress 9 | * @param {number} minValue - minimum value that needs to be in the channel 10 | * @param {object} [options={}] 11 | * @param {boolean} [options.outbound=true] - outbound is true if checking outbound channels, false if inbound 12 | * @returns {Promise} if a channel with sufficient funds exists 13 | */ 14 | async function isBalanceSufficient (paymentChannelNetworkAddress, minValue, { outbound = true } = {}) { 15 | const balance = outbound ? 'localBalance' : 'remoteBalance' 16 | const { publicKey } = networkAddressFormatter.parse(paymentChannelNetworkAddress) 17 | 18 | const { channels } = await listChannels({ client: this.client }) 19 | 20 | if (!channels || (channels && channels.length === 0)) { 21 | this.logger.debug('No channels are available', { publicKey }) 22 | return false 23 | } 24 | 25 | const activeChannels = channels.filter(c => c.active) 26 | 27 | if (activeChannels.length === 0) { 28 | this.logger.debug('No active channels exist', { publicKey }) 29 | return false 30 | } 31 | 32 | const activeChannelsFromDestination = activeChannels.filter(ac => ac.remotePubkey === publicKey) 33 | 34 | if (activeChannelsFromDestination.length === 0) { 35 | this.logger.debug('No active channels are available for destination', { publicKey }) 36 | return false 37 | } 38 | 39 | return activeChannelsFromDestination.some(channel => Big(channel[balance]).gte(minValue)) 40 | } 41 | 42 | module.exports = isBalanceSufficient 43 | -------------------------------------------------------------------------------- /src/utils/network-address-formatter.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('test/test-helper') 2 | 3 | const { parse, serialize } = require('./network-address-formatter') 4 | 5 | describe('network-address-formatter', () => { 6 | describe('parse', () => { 7 | it('parses a bolt network address', () => { 8 | expect(parse('bolt:123192380asfasdf@localhost')).to.be.eql({ 9 | publicKey: '123192380asfasdf', 10 | host: 'localhost' 11 | }) 12 | }) 13 | 14 | it('parses a network address without a host', () => { 15 | expect(parse('bolt:123192380asfasdf')).to.be.eql({ 16 | publicKey: '123192380asfasdf' 17 | }) 18 | }) 19 | 20 | it('parses a network address with a host with a port', () => { 21 | expect(parse('bolt:123192380asfasdf@localhost:1234')).to.be.eql({ 22 | publicKey: '123192380asfasdf', 23 | host: 'localhost:1234' 24 | }) 25 | }) 26 | 27 | it('throws if the network type is not bolt', () => { 28 | expect(() => parse('newnetwork:1234@localhost:1234')).to.throw('Unable to parse address') 29 | }) 30 | }) 31 | 32 | describe('serialize', () => { 33 | it('serializes a network address', () => { 34 | expect(serialize({ 35 | publicKey: '123192380asfasdf', 36 | host: 'localhost' 37 | })).to.be.eql('bolt:123192380asfasdf@localhost') 38 | }) 39 | 40 | it('serializes a network address without a host', () => { 41 | expect(serialize({ 42 | publicKey: '123192380asfasdf' 43 | })).to.be.eql('bolt:123192380asfasdf') 44 | }) 45 | 46 | it('serializes a network address with a host with a port', () => { 47 | expect(serialize({ 48 | publicKey: '123192380asfasdf', 49 | host: 'localhost:1234' 50 | })).to.be.eql('bolt:123192380asfasdf@localhost:1234') 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /docker/lnd/start-lnd-ltc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NODE=${NODE:-litecoind} 4 | CONFIG_FILE=/home/lnd/lnd.conf 5 | 6 | echo "LND LTC starting with network: $NETWORK $NODE" 7 | 8 | PARAMS=$(echo \ 9 | "--configfile=$CONFIG_FILE" \ 10 | "--litecoin.$NETWORK" \ 11 | "--debuglevel=$DEBUG" \ 12 | "--$NODE.rpcuser=$RPC_USER" \ 13 | "--$NODE.rpcpass=$RPC_PASS" \ 14 | "--$NODE.rpchost=$RPC_HOST" \ 15 | "--$NODE.zmqpubrawblock=$ZMQPUBRAWBLOCK" \ 16 | "--$NODE.zmqpubrawtx=$ZMQPUBRAWTX" 17 | ) 18 | 19 | if [[ -n "$EXTERNAL_ADDRESS" ]] && [[ -n "$EXTERNAL_PORT" ]]; then 20 | if [[ "$EXTERNAL_ADDRESS" == "host.docker.internal" ]]; then 21 | # Using host.docker.internal to access the host IP does not work for Linux 22 | # This is a known issue with Docker: https://github.com/docker/for-linux/issues/264 23 | # Here, we manually map host.docker.internal to the host IP in /etc/hosts 24 | ping -q -c1 $EXTERNAL_ADDRESS > /dev/null 2>&1 25 | # We map host.docker.internal only if the container cannot ping the address 26 | # This is typically the case only for Linux 27 | if [ $? -ne 0 ]; then 28 | HOST_IP=$(ip route | awk 'NR==1 {print $3}') 29 | echo -e "$HOST_IP\t$EXTERNAL_ADDRESS" >> /etc/hosts 30 | fi 31 | fi 32 | echo "Setting external address for lnd $EXTERNAL_ADDRESS:$EXTERNAL_PORT" 33 | PARAMS="$PARAMS --externalip=$EXTERNAL_ADDRESS:$EXTERNAL_PORT" 34 | fi 35 | 36 | if [[ -n "$LND_BASE_FEE" ]]; then 37 | echo "Setting custom base fee for litecoin: $LND_BASE_FEE" 38 | PARAMS="$PARAMS --litecoin.basefee=$LND_BASE_FEE" 39 | fi 40 | 41 | if [[ -n "$LND_FEE_RATE" ]]; then 42 | echo "Setting custom fee rate for litecoin: $LND_FEE_RATE" 43 | PARAMS="$PARAMS --litecoin.feerate=$LND_FEE_RATE" 44 | fi 45 | 46 | exec lnd $PARAMS "$@" 47 | -------------------------------------------------------------------------------- /src/utils/network-address-formatter.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Payment Channel Network Type delimiter 4 | * @constant 5 | * @type {string} 6 | * @default 7 | */ 8 | const DELIMITER = ':' 9 | 10 | /** 11 | * Network type for payment channel networks compatible with BOLT 12 | * @constant 13 | * @type {string} 14 | * @default 15 | */ 16 | const NETWORK_TYPE = 'bolt' 17 | 18 | /** 19 | * Parse a given payment channel network address string into a public key and host 20 | * @param {string} paymentChannelNetworkAddress 21 | * @returns {object} 22 | * @throws {Error} If network type is not `bolt` 23 | */ 24 | function parse (paymentChannelNetworkAddress) { 25 | const delimiterIndex = paymentChannelNetworkAddress.indexOf(DELIMITER) 26 | const [ networkType, networkAddress ] = [ paymentChannelNetworkAddress.slice(0, delimiterIndex), paymentChannelNetworkAddress.slice(delimiterIndex + 1) ] 27 | 28 | if (networkType !== NETWORK_TYPE) { 29 | throw new Error(`Unable to parse address for payment channel network type of '${networkType}'`) 30 | } 31 | 32 | const [ publicKey, host ] = networkAddress.split('@') 33 | 34 | const parsed = { publicKey } 35 | 36 | if (host) { 37 | parsed.host = host 38 | } 39 | 40 | return parsed 41 | } 42 | 43 | /** 44 | * Serialize a public key and host into a standard payment channel network address 45 | * @param {object} args 46 | * @param {string} args.publicKey - public key of the node 47 | * @param {string} [args.host] - host of the node - if omitted, it will be ommitted from the address 48 | * @returns {string} serialized payment channel network address 49 | */ 50 | function serialize ({ publicKey, host }) { 51 | let address = `${NETWORK_TYPE}${DELIMITER}${publicKey}` 52 | 53 | if (host) { 54 | address = `${address}@${host}` 55 | } 56 | 57 | return address 58 | } 59 | 60 | module.exports = { 61 | parse, 62 | deserialize: parse, 63 | serialize 64 | } 65 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-reserved-channel-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getTotalReservedChannelBalance = rewire(path.resolve(__dirname, 'get-total-reserved-channel-balance')) 5 | 6 | describe('get-total-reserved-channel-balance', () => { 7 | describe('getTotalReservedChannelBalance', () => { 8 | let channels 9 | let listChannelsStub 10 | let logger 11 | 12 | beforeEach(() => { 13 | channels = [ 14 | { initiator: true, commitFee: '10' }, 15 | { initiator: true, commitFee: '1000' }, 16 | { initiator: false, commitFee: '1000' } 17 | ] 18 | 19 | logger = { 20 | debug: sinon.stub() 21 | } 22 | 23 | getTotalReservedChannelBalance.__set__('logger', logger) 24 | }) 25 | 26 | it('returns 0 if no channels exist', async () => { 27 | listChannelsStub = sinon.stub().returns({}) 28 | getTotalReservedChannelBalance.__set__('listChannels', listChannelsStub) 29 | return expect(await getTotalReservedChannelBalance()).to.be.eql('0') 30 | }) 31 | 32 | it('returns 0 if no channels were initiator', async () => { 33 | const channels = [ 34 | { initiator: false, commitFee: 10 }, 35 | { initiator: false, commitFee: 1000 } 36 | ] 37 | 38 | listChannelsStub = sinon.stub().returns({ channels }) 39 | getTotalReservedChannelBalance.__set__('listChannels', listChannelsStub) 40 | return expect(await getTotalReservedChannelBalance()).to.be.eql('0') 41 | }) 42 | 43 | it('returns the total reserved channel balance of all channels that were initiator', async () => { 44 | listChannelsStub = sinon.stub().returns({ channels }) 45 | getTotalReservedChannelBalance.__set__('listChannels', listChannelsStub) 46 | return expect(await getTotalReservedChannelBalance()).to.be.eql('1010') 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /src/engine-actions/create-wallet.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const createWallet = rewire(path.resolve(__dirname, 'create-wallet')) 5 | 6 | describe('createWallet', () => { 7 | const password = 'password' 8 | const buffer = 'buffer' 9 | 10 | let genSeedStub 11 | let initWalletStub 12 | let seed 13 | let engine 14 | let bufferStub 15 | 16 | beforeEach(() => { 17 | engine = { 18 | walletUnlocker: sinon.stub() 19 | } 20 | seed = { 21 | cipherSeedMnemonic: ['bird', 'cat'] 22 | } 23 | genSeedStub = sinon.stub().returns(seed) 24 | initWalletStub = sinon.stub().resolves() 25 | bufferStub = sinon.stub().returns(buffer) 26 | 27 | createWallet.__set__('genSeed', genSeedStub) 28 | createWallet.__set__('initWallet', initWalletStub) 29 | createWallet.__set__('Buffer', { 30 | from: bufferStub 31 | }) 32 | }) 33 | 34 | it('errors if password is not of correct type', () => { 35 | expect(createWallet.call(engine)).to.eventually.be.rejectedWith('Provided password must be a string value') 36 | }) 37 | 38 | it('converts a specified password to a buffer', async () => { 39 | await createWallet.call(engine, password) 40 | expect(bufferStub).to.have.been.calledWith(password, 'utf8') 41 | }) 42 | 43 | it('generates a wallet seed', async () => { 44 | await createWallet.call(engine, password) 45 | expect(genSeedStub).to.have.been.calledWith(sinon.match({ client: engine.walletUnlocker })) 46 | }) 47 | 48 | it('initializes a wallet', async () => { 49 | await createWallet.call(engine, password) 50 | expect(initWalletStub).to.have.been.calledWith(buffer, sinon.match(seed.cipherSeedMnemonic)) 51 | }) 52 | 53 | it('returns a cipher seed for the created wallet', async () => { 54 | const res = await createWallet.call(engine, password) 55 | expect(res).to.be.eql(seed.cipherSeedMnemonic) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /docker/lnd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12-alpine as builder 2 | 3 | LABEL maintainer="sparkswap " 4 | 5 | ARG NETWORK 6 | RUN : "${NETWORK:?NETWORK Build argument needs to be set.}" 7 | 8 | # Force Go to use the cgo based DNS resolver. This is required to ensure DNS 9 | # queries required to connect to linked containers succeed. 10 | ENV GODEBUG netdns=cgo 11 | 12 | # Install dependencies and install/build lnd. 13 | RUN apk add --no-cache --update alpine-sdk \ 14 | git \ 15 | make 16 | 17 | WORKDIR $GOPATH/src/github.com/lightningnetwork/lnd 18 | 19 | # We use this cache date to always build LND instead of caching the files. This allows us 20 | # to continually grab changes from the LND_VERSION without tagging the release. 21 | # TODO: set this to a certain release commit 22 | ARG COMMIT_SHA 23 | RUN : "${COMMIT_SHA:?COMMIT_SHA Build argument needs to be set.}" 24 | 25 | RUN git clone https://github.com/lightningnetwork/lnd . \ 26 | && git checkout ${COMMIT_SHA} \ 27 | && make \ 28 | && make install tags="signrpc walletrpc chainrpc invoicesrpc routerrpc" 29 | 30 | # Start a new, final image to reduce size. 31 | FROM alpine as final 32 | 33 | ARG NETWORK 34 | RUN : "${NETWORK:?NETWORK Build argument needs to be set.}" 35 | 36 | # Copy the binaries and entrypoint from the builder image. 37 | COPY --from=builder /go/bin/lncli /bin/ 38 | COPY --from=builder /go/bin/lnd /bin/ 39 | 40 | # Add bash. 41 | RUN apk add --no-cache \ 42 | bash 43 | 44 | # Expose lnd ports (server, rpc). 45 | EXPOSE 9735 10009 46 | 47 | # Make lnd folder default. 48 | WORKDIR /home/lnd 49 | 50 | COPY "start-lnd-${NETWORK}.sh" "./start-lnd.sh" 51 | RUN chmod +x ./start-lnd.sh 52 | 53 | # Set NODE to an env so we can use it in the start script 54 | ARG NODE 55 | RUN : "${NODE:?NODE Build argument needs to be set.}" 56 | ENV NODE ${NODE} 57 | 58 | COPY "./conf/lnd-${NODE}.conf" "./lnd.conf" 59 | 60 | # Create backup directory for static channel backup file 61 | RUN mkdir -p /backup 62 | 63 | CMD ["bash", "-c", "NODE=${NODE} ./start-lnd.sh"] 64 | -------------------------------------------------------------------------------- /docker/bitcoind/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 as builder 2 | 3 | LABEL maintainer="Sparkswap " 4 | 5 | ARG BITCOIND_VERSION='0.17.1' 6 | 7 | # Install all deps needed for bitcoind verification 8 | RUN apt-get update && \ 9 | # We want to install recommended packages for software-props 10 | apt-get install -y software-properties-common && \ 11 | # We do not want to install recommended packages for the rest of these utils 12 | apt-get install -y --no-install-recommends \ 13 | ca-certificates \ 14 | wget \ 15 | gnupg2 \ 16 | gpg-agent \ 17 | dirmngr \ 18 | at \ 19 | iproute2 20 | 21 | ENV FILENAME bitcoin-${BITCOIND_VERSION}-x86_64-linux-gnu.tar.gz 22 | ENV CHECKSUM_FILENAME SHA256SUMS.asc 23 | 24 | # Verify bitcoin installation files and install bitcoind 25 | RUN wget -q https://bitcoin.org/bin/bitcoin-core-${BITCOIND_VERSION}/${FILENAME} 26 | RUN wget -q https://bitcoin.org/bin/bitcoin-core-${BITCOIND_VERSION}/${CHECKSUM_FILENAME} 27 | 28 | # We iterate through multiple keyservers to prevent docker failures in the case a 29 | # single gpg server fails 30 | RUN for KEYSERVER_NAME in ha.pool.sks-keyservers.net \ 31 | hkp://p80.pool.sks-keyservers.net:80 \ 32 | keyserver.ubuntu.com \ 33 | hkp://keyserver.ubuntu.com:80 \ 34 | pgp.mit.edu; \ 35 | do \ 36 | gpg2 --keyserver $KEYSERVER_NAME --recv-keys 0x90C8019E36C2E964 && \ 37 | break || echo "$KEYSERVER_NAME failed: Trying another gpg server"; \ 38 | done 39 | 40 | RUN gpg2 --verify ./${CHECKSUM_FILENAME} 41 | RUN tar xfz /${FILENAME} 42 | RUN mv bitcoin-${BITCOIND_VERSION}/bin/* /usr/local/bin/ 43 | RUN rm -rf bitcoin-* /root/.gnupg/ 44 | 45 | # Mainnet ports (rpc, http) 46 | EXPOSE 8332 8333 47 | 48 | # Testnet Ports (rpc, http) 49 | EXPOSE 18332 18333 50 | 51 | # RegTest (rpc, http) 52 | EXPOSE 18443 18444 53 | 54 | # zmq interfaces (block, tx) 55 | EXPOSE 28333 28334 56 | 57 | WORKDIR /home/bitcoind 58 | 59 | ADD "start-bitcoind.sh" ./start-bitcoind.sh 60 | RUN chmod +x ./start-bitcoind.sh 61 | 62 | CMD ["bash", "./start-bitcoind.sh"] 63 | -------------------------------------------------------------------------------- /docker/litecoind/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 as builder 2 | 3 | LABEL maintainer "sparkswap " 4 | 5 | ARG LITECOIND_VERSION='0.16.3' 6 | 7 | # Install all deps needed for litecoind verification 8 | RUN apt-get update && \ 9 | # We want to install recommended packages for software-props 10 | apt-get install -y software-properties-common && \ 11 | # We do not want to install recommended packages for the rest of these utils 12 | apt-get install -y --no-install-recommends \ 13 | ca-certificates \ 14 | wget \ 15 | gnupg2 \ 16 | gpg-agent \ 17 | dirmngr \ 18 | at \ 19 | iproute2 20 | 21 | ENV FILENAME litecoin-${LITECOIND_VERSION}-x86_64-linux-gnu.tar.gz 22 | ENV CHECKSUM_FILENAME litecoin-${LITECOIND_VERSION}-x86_64-linux-gnu.tar.gz.asc 23 | 24 | # Verify litecoin installation files and install litecoind 25 | RUN wget -q https://download.litecoin.org/litecoin-${LITECOIND_VERSION}/linux/${FILENAME} 26 | RUN wget -q https://download.litecoin.org/litecoin-${LITECOIND_VERSION}/linux/${CHECKSUM_FILENAME} 27 | 28 | # We iterate through multiple keyservers to prevent docker failures in the case a 29 | # single gpg server fails 30 | RUN for KEYSERVER_NAME in ha.pool.sks-keyservers.net \ 31 | hkp://p80.pool.sks-keyservers.net:80 \ 32 | keyserver.ubuntu.com \ 33 | hkp://keyserver.ubuntu.com:80 \ 34 | pgp.mit.edu; \ 35 | do \ 36 | gpg2 --keyserver $KEYSERVER_NAME --recv-keys FE3348877809386C && \ 37 | break || echo "$KEYSERVER_NAME failed: Trying another gpg server"; \ 38 | done 39 | 40 | RUN gpg2 --verify ./${CHECKSUM_FILENAME} 41 | RUN tar xfz /litecoin-${LITECOIND_VERSION}-x86_64-linux-gnu.tar.gz 42 | RUN mv litecoin-${LITECOIND_VERSION}/bin/* /usr/local/bin/ 43 | RUN rm -rf litecoin-* /root/.gnupg/ 44 | 45 | WORKDIR /home/litecoind 46 | 47 | # Mainnet (rpc, http) 48 | EXPOSE 9332 9333 49 | 50 | # Testnet (rpc, http) 51 | EXPOSE 19332 19333 52 | 53 | # Regtest (rpc, http) 54 | EXPOSE 19443 19444 55 | 56 | ADD "start-litecoind.sh" ./start-litecoind.sh 57 | RUN chmod +x ./start-litecoind.sh 58 | 59 | CMD ["bash", "./start-litecoind.sh"] 60 | -------------------------------------------------------------------------------- /src/engine-actions/get-uncommitted-pending-balance.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getUncommittedPendingBalance = rewire(path.resolve(__dirname, 'get-uncommitted-pending-balance')) 5 | 6 | describe('getUncommittedPendingBalance', () => { 7 | let listPendingChannelsStub 8 | let logger 9 | let pendingForceClosingChannels 10 | let unconfirmedBalance 11 | let balanceResponse 12 | let walletBalanceStub 13 | 14 | beforeEach(() => { 15 | pendingForceClosingChannels = [{ channel: { localBalance: '30' } }] 16 | 17 | logger = { 18 | debug: sinon.stub() 19 | } 20 | unconfirmedBalance = '1234' 21 | balanceResponse = { unconfirmedBalance } 22 | walletBalanceStub = sinon.stub().returns(balanceResponse) 23 | listPendingChannelsStub = sinon.stub().resolves({ pendingForceClosingChannels }) 24 | getUncommittedPendingBalance.__set__('walletBalance', walletBalanceStub) 25 | getUncommittedPendingBalance.__set__('logger', logger) 26 | }) 27 | 28 | it('returns 0 if no unconfirmed balance and no channels exist', async () => { 29 | listPendingChannelsStub.resolves({}) 30 | walletBalanceStub.resolves({ unconfirmedBalance: '0' }) 31 | getUncommittedPendingBalance.__set__('listPendingChannels', listPendingChannelsStub) 32 | return expect(await getUncommittedPendingBalance()).to.be.eql('0') 33 | }) 34 | 35 | it('returns the unconfirmed balance if no channels exist', async () => { 36 | listPendingChannelsStub = sinon.stub().resolves({}) 37 | getUncommittedPendingBalance.__set__('listPendingChannels', listPendingChannelsStub) 38 | return expect(await getUncommittedPendingBalance()).to.be.eql('1234') 39 | }) 40 | 41 | it('adds pendingForceClosingChannels to the unconfirmed balance if the channels exist', async () => { 42 | listPendingChannelsStub.resolves({ pendingForceClosingChannels }) 43 | getUncommittedPendingBalance.__set__('listPendingChannels', listPendingChannelsStub) 44 | return expect(await getUncommittedPendingBalance()).to.be.eql('1264') 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/engine-actions/recover-wallet.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const { initWallet } = require('../lnd-actions') 4 | 5 | /** 6 | * Wallet recovery window number in blocks 7 | * @constant 8 | * @type {number} 9 | * @default 10 | */ 11 | const RECOVERY_WINDOW_DEFAULT = 5000 12 | 13 | /** 14 | * @param {Array} seed 15 | * @returns {Promise} 16 | */ 17 | async function isValidSeed (seed) { 18 | if (!Array.isArray(seed)) return false 19 | if (seed.length !== 24) return false 20 | return true 21 | } 22 | 23 | /** 24 | * Recover wallet funds (on-chain and channel backups) 25 | * 26 | * @param {string} password - previous wallet password 27 | * @param {Array} seed - 24 word cipher seed mnemonic 28 | * @param {boolean} useChannelBackup 29 | * @param {number} [recoveryWindow=RECOVERY_WINDOW_DEFAULT] - number in blocks for look back 30 | * @returns {Promise} 24 word cipher seed mnemonic 31 | */ 32 | async function recoverWallet (password, seed, useChannelBackup, recoveryWindow = RECOVERY_WINDOW_DEFAULT) { 33 | if (!password) throw new Error('Password must be provided to recover wallet') 34 | if (!seed) throw new Error('Recovery seed must be provided to recover wallet') 35 | 36 | if (seed && !useChannelBackup) { 37 | this.logger.warn('Recovering wallet without a backup file. ONLY on-chain funds will be recovered') 38 | } 39 | 40 | let staticChannelBackup = null 41 | 42 | if (useChannelBackup) { 43 | staticChannelBackup = fs.readFileSync(this.currencyConfig.backupFilePath) 44 | } 45 | 46 | // Password must be converted to buffer in order for lnd to accept 47 | // as LND 0.7.0 does not accept String typed passwords 48 | const walletPassword = Buffer.from(password, 'utf8') 49 | 50 | // If the user has provided a seed, then we will try to recover the wallet 51 | // on creation 52 | await isValidSeed(seed) 53 | 54 | this.logger.debug(`Recovering wallet with recovery window: ${recoveryWindow}`) 55 | 56 | await initWallet(walletPassword, seed, { backup: staticChannelBackup, recoveryWindow, client: this.walletUnlocker }) 57 | } 58 | 59 | module.exports = recoverWallet 60 | -------------------------------------------------------------------------------- /src/lnd-actions/send-payment.js: -------------------------------------------------------------------------------- 1 | const { deadline } = require('../grpc-utils') 2 | 3 | /** @typedef {import('../lnd-setup').LndClient} LndClient */ 4 | 5 | /** 6 | * SendPayment deadline (in seconds) 7 | * @constant 8 | * @type {number} 9 | * @default 10 | */ 11 | const SEND_PAYMENT_DEADLINE = 30 12 | 13 | // TODO: verify this against BOLT 11 14 | /** @typedef {object} SendPaymentRequestFormatA 15 | * @property {string} paymentOptions.paymentHash - 16 | * Base64 string of the payment hash to use 17 | * @property {string} paymentOptions.destString - 18 | * destination public key 19 | * @property {string} paymentOptions.amt - 20 | * Int64 string of number of satoshis to send 21 | * @property {number} paymentOptions.finalCltvDelta - 22 | * Delta from the current block height to be used for the final hop 23 | * @property {object} paymentOptions.feeLimit 24 | * @property {string} [paymentOptions.feeLimit.fixed] - 25 | * Int64 string of maximum number of satoshis to pay in fees 26 | * @property {string} [paymentOptions.feeLimit.percent] - 27 | * Int64 string of fee limit exepressed as a pecentage of payment amount 28 | * @property {number} [paymentOptions.cltvLimit] - 29 | * Maximum number of blocks in the route timelock 30 | */ 31 | 32 | /** @typedef {object} SendPaymentRequestFormatB 33 | * @property {string} paymentOptions.paymentRequest - LN Payment Request 34 | */ 35 | 36 | /** @typedef {SendPaymentRequestFormatA | SendPaymentRequestFormatB} 37 | * SendPaymentRequest 38 | */ 39 | 40 | /** 41 | * Sends a payment to a specified invoice 42 | * 43 | * @see http://api.lightning.community/#sendPaymentSync 44 | * @param {SendPaymentRequest} paymentOptions 45 | * @param {object} opts 46 | * @param {LndClient} opts.client 47 | * @returns {Promise} Resolves with the response from LND 48 | */ 49 | function sendPayment (paymentOptions, { client }) { 50 | return new Promise((resolve, reject) => { 51 | client.sendPaymentSync(paymentOptions, { deadline: deadline(SEND_PAYMENT_DEADLINE) }, (err, res) => { 52 | if (err) return reject(err) 53 | return resolve(res) 54 | }) 55 | }) 56 | } 57 | 58 | module.exports = sendPayment 59 | -------------------------------------------------------------------------------- /src/engine-actions/create-refund-invoice.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const createRefundInvoice = rewire(path.resolve(__dirname, 'create-refund-invoice')) 5 | 6 | describe('create-refund-invoice', () => { 7 | let decodeStub 8 | let addInvoiceStub 9 | let clientStub 10 | let decodedPayment 11 | let invoiceValue 12 | let decodedDescription 13 | let refundInvoice 14 | let refundPaymentRequest 15 | let paymentRequest 16 | let res 17 | let logger 18 | 19 | beforeEach(() => { 20 | clientStub = sinon.stub() 21 | invoiceValue = 1000 22 | decodedDescription = 'INVOICE_DESCRIPTION' 23 | refundPaymentRequest = 'REFUND_PAYMENT_REQUEST' 24 | paymentRequest = 'INVOICE_PAYMENT_REQUEST' 25 | decodedPayment = { 26 | numSatoshis: invoiceValue, 27 | description: decodedDescription 28 | } 29 | logger = { 30 | info: sinon.stub(), 31 | debug: sinon.stub() 32 | } 33 | refundInvoice = { 34 | paymentRequest: refundPaymentRequest 35 | } 36 | 37 | decodeStub = sinon.stub().returns(decodedPayment) 38 | addInvoiceStub = sinon.stub().returns(refundInvoice) 39 | 40 | createRefundInvoice.__set__('decodePaymentRequest', decodeStub) 41 | createRefundInvoice.__set__('addInvoice', addInvoiceStub) 42 | createRefundInvoice.__set__('client', clientStub) 43 | createRefundInvoice.__set__('logger', logger) 44 | }) 45 | 46 | beforeEach(async () => { 47 | res = await createRefundInvoice(paymentRequest) 48 | }) 49 | 50 | it('decodes a paymentRequest', () => { 51 | expect(decodeStub).to.have.been.calledWith(paymentRequest, { client: clientStub }) 52 | }) 53 | 54 | it('creates a refund invoice', () => { 55 | const expiryPrefix = createRefundInvoice.__get__('REFUND_MEMO_PREFIX') 56 | const expiry = createRefundInvoice.__get__('DEFAULT_INVOICE_EXPIRY') 57 | 58 | expect(addInvoiceStub).to.have.been.calledWith( 59 | { 60 | memo: `${expiryPrefix} ${decodedDescription}`, 61 | expiry, 62 | value: invoiceValue 63 | }, 64 | { client: clientStub } 65 | ) 66 | }) 67 | 68 | it('returns a refund invoice', () => { 69 | expect(res).to.eql(refundPaymentRequest) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/engine-actions/initiate-swap.js: -------------------------------------------------------------------------------- 1 | const { sendPayment } = require('../lnd-actions') 2 | const { networkAddressFormatter } = require('../utils') 3 | 4 | /** 5 | * Default fee limit for swap routes. 6 | * We expect to route swaps through a fee-less hub. 7 | * @todo Make this value dynamic. 8 | */ 9 | const DEFAULT_FEE_LIMIT = '0' 10 | 11 | /** 12 | * Initiates a swap 13 | * 14 | * @param {string} address - Payment Channel Network Address for the last hop of this leg of the swap 15 | * @param {string} swapHash - base64 string of the swap hash associated with this swap 16 | * @param {string} amount - Int64 string of the amount of outbound currency in its integer units 17 | * @param {number} maxTimeLock - Maximum number of seconds that this swap should take to be finalized 18 | * @param {number} [finalDelta=0] - delta (in seconds) to use for the time-lock of the CLTV extended to the final hop. 19 | * If unspecified, defaults to the BOLT-11 value (9 Blocks for BTC) 20 | * @param {string} [feeLimit=0] - Maximum amount of fees to pay on the route 21 | * @returns {Promise} - Promise that resolves when the swap is settled 22 | */ 23 | async function initiateSwap (address, swapHash, amount, maxTimeLock, finalDelta = 0, feeLimit = DEFAULT_FEE_LIMIT) { 24 | this.logger.info(`Initiating swap for ${swapHash} with ${address}`, { address, swapHash, amount }) 25 | 26 | const { publicKey: counterpartyPubKey } = networkAddressFormatter.parse(address) 27 | 28 | // Since `maxTimeLock` is a maximum, we use Math.floor to be conservative 29 | const cltvLimit = Math.floor(maxTimeLock / this.secondsPerBlock) 30 | 31 | // We use Math.ceil to be sure that the last node in this leg of the swap accepts the payment. 32 | const finalCltvDelta = Math.ceil(finalDelta / this.secondsPerBlock) 33 | 34 | const request = { 35 | destString: counterpartyPubKey, 36 | paymentHash: swapHash, 37 | amt: amount, 38 | feeLimit: { 39 | fixed: feeLimit 40 | }, 41 | cltvLimit, 42 | finalCltvDelta 43 | } 44 | 45 | const { paymentError, paymentPreimage } = await sendPayment(request, { client: this.client }) 46 | 47 | if (paymentError) { 48 | this.logger.error('Failed to execute swap', { swapHash, address, paymentError }) 49 | throw new Error(paymentError) 50 | } 51 | 52 | return paymentPreimage 53 | } 54 | 55 | module.exports = initiateSwap 56 | -------------------------------------------------------------------------------- /src/engine-actions/prepare-swap.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const prepareSwap = rewire(path.resolve(__dirname, 'prepare-swap')) 5 | 6 | describe('prepareSwap', () => { 7 | let value 8 | let addHoldInvoiceStub 9 | let lookupInvoiceStub 10 | let invoiceResponse 11 | let paymentRequest 12 | let existingPaymentRequest 13 | let swapHash 14 | let expiryTime 15 | let cltvExpiry 16 | 17 | const client = sinon.stub() 18 | const engine = { 19 | client, 20 | secondsPerBlock: 600, 21 | logger: { 22 | info: sinon.stub(), 23 | error: sinon.stub(), 24 | debug: sinon.stub() 25 | } 26 | } 27 | 28 | beforeEach(() => { 29 | value = '100' 30 | paymentRequest = '1234' 31 | existingPaymentRequest = '12345' 32 | swapHash = '1234' 33 | // add 10ms to 60s so flooring to the nearest second in prepareSwap gives 60 34 | expiryTime = new Date((new Date()).getTime() + 60010) 35 | cltvExpiry = 3600 36 | invoiceResponse = { paymentRequest } 37 | addHoldInvoiceStub = sinon.stub().resolves(invoiceResponse) 38 | 39 | lookupInvoiceStub = sinon.stub().throws({}) 40 | prepareSwap.__set__('lookupInvoice', lookupInvoiceStub) 41 | prepareSwap.__set__('addHoldInvoice', addHoldInvoiceStub) 42 | prepareSwap.__set__('client', client) 43 | }) 44 | 45 | it('adds an invoice through lnd', async () => { 46 | await prepareSwap.call(engine, swapHash, value, expiryTime, cltvExpiry) 47 | expect(addHoldInvoiceStub).to.have.been.calledWith({ 48 | memo: 'sparkswap-swap-pivot', 49 | hash: swapHash, 50 | value, 51 | expiry: '60', 52 | cltvExpiry: '6' 53 | }, sinon.match({ client })) 54 | }) 55 | 56 | it('is idempotent', async () => { 57 | addHoldInvoiceStub.throws(new Error('Invoice with hash already exists')) 58 | lookupInvoiceStub.withArgs({ rHash: swapHash }).resolves({ 59 | paymentRequest: existingPaymentRequest, 60 | cltvExpiry: '6', 61 | value 62 | }) 63 | const res = await prepareSwap.call(engine, swapHash, value, expiryTime, cltvExpiry) 64 | expect(res).to.be.eql(existingPaymentRequest) 65 | }) 66 | 67 | it('returns an invoice hash', async () => { 68 | const res = await prepareSwap.call(engine, swapHash, value, expiryTime, cltvExpiry) 69 | expect(res).to.be.eql(paymentRequest) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/engine-actions/get-total-balance-for-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getTotalBalanceForAddress = rewire(path.resolve(__dirname, 'get-total-balance-for-address')) 5 | 6 | describe('getTotalBalanceForAddress', () => { 7 | let channels 8 | let getChannelsForRemoteAddressStub 9 | let address 10 | let logger 11 | let revertChannels 12 | 13 | beforeEach(() => { 14 | address = 'bolt:asdf@localhost' 15 | channels = [ 16 | { localBalance: '10', remoteBalance: '300' }, 17 | { localBalance: '0', remoteBalance: '100' }, 18 | { localBalance: '20', remoteBalance: '1000' }, 19 | { localBalance: '10', remoteBalance: '15' }, 20 | { localBalance: '50', remoteBalance: '30' }, 21 | { localBalance: '0', remoteBalance: '100' } 22 | ] 23 | 24 | logger = { 25 | debug: sinon.stub() 26 | } 27 | 28 | getChannelsForRemoteAddressStub = sinon.stub().resolves(channels) 29 | 30 | getTotalBalanceForAddress.__set__('logger', logger) 31 | revertChannels = getTotalBalanceForAddress.__set__('getChannelsForRemoteAddress', getChannelsForRemoteAddressStub) 32 | }) 33 | 34 | afterEach(() => { 35 | revertChannels() 36 | }) 37 | 38 | it('gets all channels for the given address', async () => { 39 | const ctx = { fake: 'context' } 40 | await getTotalBalanceForAddress.call(ctx, address) 41 | expect(getChannelsForRemoteAddressStub).to.have.been.called() 42 | expect(getChannelsForRemoteAddressStub).to.have.been.calledOn(ctx) 43 | expect(getChannelsForRemoteAddressStub).to.have.been.calledWith(address) 44 | }) 45 | 46 | it('returns 0 if no channels exist', async () => { 47 | getChannelsForRemoteAddressStub.resolves([]) 48 | 49 | const expectedRes = '0' 50 | const res = await getTotalBalanceForAddress(address) 51 | expect(res).to.eql(expectedRes) 52 | }) 53 | 54 | it('returns total local balance of all pending and open channels', async () => { 55 | const res = await getTotalBalanceForAddress(address) 56 | const expectedRes = '90' 57 | return expect(res).to.eql(expectedRes) 58 | }) 59 | 60 | it('returns total remote balance of all pending and open channels', async () => { 61 | const res = await getTotalBalanceForAddress(address, { outbound: false }) 62 | const expectedRes = '1545' 63 | return expect(res).to.eql(expectedRes) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/lnd-actions/index.js: -------------------------------------------------------------------------------- 1 | const addInvoice = require('./add-invoice') 2 | const addHoldInvoice = require('./add-hold-invoice') 3 | const connectPeer = require('./connect-peer') 4 | const getInfo = require('./get-info') 5 | const listInvoices = require('./list-invoices') 6 | const listPeers = require('./list-peers') 7 | const lookupInvoice = require('./lookup-invoice') 8 | const newAddress = require('./new-address') 9 | const openChannel = require('./open-channel') 10 | const walletBalance = require('./wallet-balance') 11 | const decodePaymentRequest = require('./decode-payment-request') 12 | const listChannels = require('./list-channels') 13 | const sendPayment = require('./send-payment') 14 | const describeGraph = require('./describe-graph') 15 | const sendToRoute = require('./send-to-route') 16 | const updateChannelPolicy = require('./update-channel-policy') 17 | const queryRoutes = require('./query-routes') 18 | const subscribeSingleInvoice = require('./subscribe-single-invoice') 19 | const listPendingChannels = require('./list-pending-channels') 20 | const closeChannel = require('./close-channel') 21 | const sendCoins = require('./send-coins') 22 | const genSeed = require('./gen-seed') 23 | const initWallet = require('./init-wallet') 24 | const unlockWallet = require('./unlock-wallet') 25 | const listPayments = require('./list-payments') 26 | const lookupPaymentStatus = require('./lookup-payment-status') 27 | const listClosedChannels = require('./list-closed-channels') 28 | const getTransactions = require('./get-transactions') 29 | const changePassword = require('./change-password') 30 | const cancelInvoice = require('./cancel-invoice') 31 | const settleInvoice = require('./settle-invoice') 32 | const trackPayment = require('./track-payment') 33 | 34 | module.exports = { 35 | addInvoice, 36 | addHoldInvoice, 37 | connectPeer, 38 | getInfo, 39 | listInvoices, 40 | listPeers, 41 | lookupInvoice, 42 | newAddress, 43 | openChannel, 44 | walletBalance, 45 | decodePaymentRequest, 46 | listChannels, 47 | sendPayment, 48 | describeGraph, 49 | sendToRoute, 50 | updateChannelPolicy, 51 | queryRoutes, 52 | subscribeSingleInvoice, 53 | listPendingChannels, 54 | closeChannel, 55 | sendCoins, 56 | genSeed, 57 | initWallet, 58 | unlockWallet, 59 | listPayments, 60 | lookupPaymentStatus, 61 | listClosedChannels, 62 | getTransactions, 63 | changePassword, 64 | cancelInvoice, 65 | settleInvoice, 66 | trackPayment 67 | } 68 | -------------------------------------------------------------------------------- /src/engine-actions/initiate-swap.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const initiateSwap = rewire(path.resolve(__dirname, 'initiate-swap')) 5 | 6 | describe('initiateSwap', () => { 7 | let networkAddressFormatter 8 | let sendPayment 9 | let swapHash 10 | let address 11 | let amount 12 | let client 13 | let engine 14 | let maxTimeLock 15 | let finalDelta 16 | 17 | beforeEach(() => { 18 | networkAddressFormatter = { 19 | parse: sinon.stub().withArgs('fake address').returns({ publicKey: 'fake pubkey' }) 20 | } 21 | sendPayment = sinon.stub().resolves({ paymentPreimage: 'fake preimage' }) 22 | 23 | initiateSwap.__set__('networkAddressFormatter', networkAddressFormatter) 24 | initiateSwap.__set__('sendPayment', sendPayment) 25 | 26 | swapHash = 'fake hash' 27 | address = 'fake address' 28 | amount = '100000' 29 | maxTimeLock = 1201 30 | finalDelta = 590 31 | client = 'fake client' 32 | engine = { 33 | client, 34 | logger: { 35 | error: sinon.stub(), 36 | info: sinon.stub() 37 | }, 38 | secondsPerBlock: 600 39 | } 40 | }) 41 | 42 | it('sends a payment', async () => { 43 | await initiateSwap.call(engine, address, swapHash, amount, maxTimeLock, finalDelta) 44 | 45 | expect(sendPayment).to.have.been.calledOnce() 46 | expect(sendPayment).to.have.been.calledWith(sinon.match.any, { client }) 47 | expect(networkAddressFormatter.parse).to.have.been.calledOnce() 48 | expect(networkAddressFormatter.parse).to.have.been.calledWith(address) 49 | expect(sendPayment).to.have.been.calledWith(sinon.match({ destString: 'fake pubkey' })) 50 | expect(sendPayment).to.have.been.calledWith(sinon.match({ paymentHash: swapHash })) 51 | expect(sendPayment).to.have.been.calledWith(sinon.match({ amt: amount })) 52 | expect(sendPayment).to.have.been.calledWith(sinon.match({ finalCltvDelta: 1 })) 53 | expect(sendPayment).to.have.been.calledWith(sinon.match({ cltvLimit: 2 })) 54 | }) 55 | 56 | it('throws on payment error', () => { 57 | sendPayment.resolves({ paymentError: new Error('fake error') }) 58 | 59 | return expect(initiateSwap.call(engine, address, swapHash, amount, maxTimeLock, finalDelta)).to.eventually.be.rejectedWith('fake error') 60 | }) 61 | 62 | it('returns the preimage', async () => { 63 | expect(await initiateSwap.call(engine, address, swapHash, amount, maxTimeLock, finalDelta)).to.be.eql('fake preimage') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/lnd-setup/generate-wallet-unlocker-client.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const generateWalletUnlockerClient = rewire(path.resolve('src', 'lnd-setup', 'generate-wallet-unlocker-client')) 5 | 6 | describe('generateWalletUnlockerClient', () => { 7 | const host = 'host' 8 | const protoPath = 'protopath' 9 | const tlsCertPath = 'tlscert' 10 | 11 | let engine 12 | let loadProtoStub 13 | let lnrpcProto 14 | let loggerErrorStub 15 | let logger 16 | let readFileSyncStub 17 | let existsSyncStub 18 | let tlsCert 19 | let createSslStub 20 | let tlsCreds 21 | 22 | beforeEach(() => { 23 | tlsCert = Buffer.from('cert') 24 | loggerErrorStub = sinon.stub() 25 | tlsCreds = sinon.stub() 26 | createSslStub = sinon.stub().returns(tlsCreds) 27 | logger = { 28 | error: loggerErrorStub 29 | } 30 | engine = { 31 | host, 32 | protoPath, 33 | tlsCertPath, 34 | logger 35 | } 36 | lnrpcProto = sinon.stub() 37 | loadProtoStub = sinon.stub().returns({ 38 | lnrpc: { 39 | WalletUnlocker: lnrpcProto 40 | } 41 | }) 42 | readFileSyncStub = sinon.stub().returns(tlsCert) 43 | existsSyncStub = sinon.stub().returns(true) 44 | 45 | generateWalletUnlockerClient.__set__('loadProto', loadProtoStub) 46 | generateWalletUnlockerClient.__set__('fs', { 47 | readFileSync: readFileSyncStub, 48 | existsSync: existsSyncStub 49 | }) 50 | generateWalletUnlockerClient.__set__('grpc', { 51 | credentials: { 52 | createSsl: createSslStub 53 | } 54 | }) 55 | }) 56 | 57 | it('loads a proto file from protoPath', () => { 58 | generateWalletUnlockerClient(engine) 59 | expect(loadProtoStub).to.have.been.calledWith(protoPath) 60 | }) 61 | 62 | it('throws an error if tls cert is not found', () => { 63 | existsSyncStub.returns(false) 64 | expect(() => generateWalletUnlockerClient(engine)).to.throw('tls cert file not found') 65 | }) 66 | 67 | it('reads a tls file', () => { 68 | generateWalletUnlockerClient(engine) 69 | expect(readFileSyncStub).to.have.been.calledWith(tlsCertPath) 70 | }) 71 | 72 | it('creates tls credentials', () => { 73 | generateWalletUnlockerClient(engine) 74 | expect(createSslStub).to.have.been.calledWith(tlsCert) 75 | }) 76 | 77 | it('returns a new WalletUnlocker rpc', () => { 78 | generateWalletUnlockerClient(engine) 79 | expect(lnrpcProto).to.have.been.calledWith(host, tlsCreds) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/engine-actions/get-max-channel-for-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getMaxChannelForAddress = rewire(path.resolve(__dirname, 'get-max-channel-for-address')) 5 | 6 | describe('getMaxChannelForAddress', () => { 7 | let channels 8 | let getChannelsForRemoteAddressStub 9 | let address 10 | let logger 11 | let revertChannels 12 | 13 | beforeEach(() => { 14 | address = 'bolt:asdf@localhost' 15 | channels = [ 16 | { localBalance: '10', remoteBalance: '300' }, 17 | { localBalance: '0', remoteBalance: '100' }, 18 | { localBalance: '20', remoteBalance: '1000' }, 19 | { localBalance: '10', remoteBalance: '15' }, 20 | { localBalance: '50', remoteBalance: '30' }, 21 | { localBalance: '0', remoteBalance: '100' } 22 | ] 23 | 24 | logger = { 25 | debug: sinon.stub() 26 | } 27 | 28 | getChannelsForRemoteAddressStub = sinon.stub().resolves(channels) 29 | 30 | getMaxChannelForAddress.__set__('logger', logger) 31 | revertChannels = getMaxChannelForAddress.__set__('getChannelsForRemoteAddress', getChannelsForRemoteAddressStub) 32 | }) 33 | 34 | afterEach(() => { 35 | revertChannels() 36 | }) 37 | 38 | it('gets all channels for the given address', async () => { 39 | await getMaxChannelForAddress(address) 40 | expect(getChannelsForRemoteAddressStub).to.have.been.called() 41 | expect(getChannelsForRemoteAddressStub).to.have.been.calledWith(address) 42 | }) 43 | 44 | it('returns zero if no channels exist', async () => { 45 | getChannelsForRemoteAddressStub.resolves({}) 46 | 47 | const expectedRes = { maxBalance: '0' } 48 | const res = await getMaxChannelForAddress(address) 49 | expect(logger.debug).to.have.been.calledWith('getMaxChannelForAddress: No open or pending channels exist') 50 | expect(res).to.eql(expectedRes) 51 | }) 52 | 53 | context('checking outbound channels', () => { 54 | it('returns max balance of all pending and open channels', async () => { 55 | const res = await getMaxChannelForAddress(address) 56 | const expectedRes = { maxBalance: '50' } 57 | return expect(res).to.eql(expectedRes) 58 | }) 59 | }) 60 | 61 | context('checking inbound channels', () => { 62 | it('returns max balance of both open and pending channels if open and pending channels exist', async () => { 63 | const res = await getMaxChannelForAddress(address, { outbound: false }) 64 | const expectedRes = { maxBalance: '1000' } 65 | return expect(res).to.eql(expectedRes) 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/engine-actions/get-settled-swap-preimage.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getSettledSwapPreimage = rewire(path.resolve(__dirname, 'get-settled-swap-preimage')) 5 | 6 | describe('getSettledSwapPreimage', () => { 7 | let clientStub 8 | let engine 9 | let lookupInvoiceStub 10 | let logger 11 | let swapHash 12 | let theSettledInvoice 13 | let theUnsettledInvoice 14 | 15 | beforeEach(() => { 16 | swapHash = 'aisudf0asufdhasdfnjasdofindf==' 17 | theSettledInvoice = { 18 | rHash: swapHash, 19 | rPreimage: 'fake preimage', 20 | state: 'SETTLED' 21 | } 22 | theUnsettledInvoice = { 23 | rHash: swapHash, 24 | state: 'OPEN' 25 | } 26 | lookupInvoiceStub = sinon.stub().resolves(theUnsettledInvoice) 27 | 28 | clientStub = sinon.stub() 29 | logger = { 30 | debug: sinon.stub(), 31 | error: sinon.stub() 32 | } 33 | engine = { 34 | logger, 35 | client: clientStub 36 | } 37 | 38 | getSettledSwapPreimage.__set__('lookupInvoice', lookupInvoiceStub) 39 | }) 40 | 41 | it('throws an error if the swapHash does not exist', () => { 42 | swapHash = undefined 43 | 44 | return expect( 45 | getSettledSwapPreimage.call(engine, swapHash) 46 | ).to.eventually.be.rejectedWith('Swap hash must be defined') 47 | }) 48 | 49 | it('looks up the invoice', async () => { 50 | lookupInvoiceStub.resolves(theSettledInvoice) 51 | await getSettledSwapPreimage.call(engine, swapHash) 52 | 53 | expect(lookupInvoiceStub).to.have.been.calledOnce() 54 | expect(lookupInvoiceStub).to.have.been.calledWith( 55 | { rHash: swapHash }, { client: clientStub } 56 | ) 57 | }) 58 | 59 | it('throws if the invoice does not exist', () => { 60 | lookupInvoiceStub.rejects(new Error('fake error')) 61 | 62 | return expect( 63 | getSettledSwapPreimage.call(engine, swapHash) 64 | ).to.eventually.be.rejectedWith('fake error') 65 | }) 66 | 67 | it('throws if the invoice is not settled', () => { 68 | lookupInvoiceStub.resolves(theUnsettledInvoice) 69 | 70 | return expect( 71 | getSettledSwapPreimage.call(engine, swapHash) 72 | ).to.eventually.be.rejectedWith('Cannot retrieve preimage from an invoice in a OPEN state.') 73 | }) 74 | 75 | it('returns the preimage if the invoice is already settled', async () => { 76 | lookupInvoiceStub.resolves(theSettledInvoice) 77 | const preimage = await getSettledSwapPreimage.call(engine, swapHash) 78 | 79 | expect(preimage).to.be.eql(theSettledInvoice.rPreimage) 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /src/engine-actions/prepare-swap.js: -------------------------------------------------------------------------------- 1 | const { addHoldInvoice, lookupInvoice } = require('../lnd-actions') 2 | 3 | /** 4 | * The memo prefix allows us to easily find SparkSwap-related invoices 5 | * in LND. In this case, the invoice is the pivot point of the swap 6 | * where it changes chains. 7 | * @constant 8 | * @type {string} 9 | * @default 10 | */ 11 | const MEMO_PREFIX = 'sparkswap-swap-pivot' 12 | 13 | /** 14 | * Prepares for a swap in which this node is the counterparty to the intiating node 15 | * 16 | * @param {string} swapHash - base64 hash that will be associated with the swap 17 | * @param {string} value - Int64 string of the value of inbound currency 18 | * @param {Date} expiryTime - absolute payment request expiry time 19 | * @param {number} cltvExpiry - delta to use for the time-lock of the CLTV 20 | * extended to the final hop (in seconds) 21 | * @returns {Promise} paymentRequest - lightning invoice for a payment 22 | */ 23 | async function prepareSwap (swapHash, value, expiryTime, cltvExpiry) { 24 | this.logger.info(`Preparing swap for ${swapHash}`, { value }) 25 | // Round up on number of blocks since a transaction that occurs right before 26 | // the expiration time will get processed in the following block. 27 | // The gRPC endpoint expects an int64 number, so we need to to convert to a 28 | // string prior to creating an invoice or comparing to an invoice. 29 | const cltvExpiryBlocks = Math.ceil(cltvExpiry / this.secondsPerBlock).toString() 30 | 31 | // make prepareSwap idempotent by returning the existing invoice if one exists 32 | try { 33 | const { 34 | paymentRequest, 35 | value: invoiceValue, 36 | cltvExpiry: invoiceCLTVExpiry 37 | } = await lookupInvoice({ rHash: swapHash }, { client: this.client }) 38 | 39 | if (invoiceValue !== value || invoiceCLTVExpiry !== cltvExpiryBlocks) { 40 | // if the invoice doesn't match, we log an error and prepare a new invoice 41 | const message = `Invoice found for hash ${swapHash} but parameters are wrong` 42 | this.logger.error(message) 43 | throw new Error(message) 44 | } 45 | return paymentRequest 46 | } catch (e) { 47 | const expiry = Math.floor((expiryTime.getTime() - (new Date()).getTime()) / 1000) 48 | const params = { 49 | memo: `${MEMO_PREFIX}`, 50 | hash: swapHash, 51 | value, 52 | expiry: expiry.toString(), 53 | cltvExpiry: cltvExpiryBlocks 54 | } 55 | 56 | const { paymentRequest } = await addHoldInvoice(params, { client: this.client }) 57 | return paymentRequest 58 | } 59 | } 60 | 61 | module.exports = prepareSwap 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lnd-engine", 3 | "version": "0.10.1-beta", 4 | "description": "A Sparkswap Engine for LND lightning implementation", 5 | "main": "src/index.js", 6 | "nyc": { 7 | "exclude": [ 8 | "**/*.spec.js", 9 | "src/lnd-actions/*" 10 | ] 11 | }, 12 | "scripts": { 13 | "build": "bash ./scripts/build.sh", 14 | "pretest": "npm run format", 15 | "test": "npm run check-for-cycles && npm run check-unused && npm run typecheck && npm run test-engine", 16 | "test-engine": "NODE_PATH=. mocha 'src/**/*.spec.js'", 17 | "coverage": "nyc npm run test", 18 | "lint": "eslint src test --ext .js --max-warnings=0", 19 | "format": "npm run lint -- --fix", 20 | "docs": "jsdoc -c .jsdoc.json", 21 | "postdoc": "http-server docs", 22 | "ci-test": "npm run typecheck && npm run format && npm run test-engine", 23 | "check-for-cycles": "madge --circular src/", 24 | "check-unused": "node ./scripts/check-unused.js", 25 | "typecheck": "tsc --alwaysStrict --allowJs --checkJs --noEmit --resolveJsonModule --downlevelIteration --noImplicitReturns --noUnusedLocals --noUnusedParameters --strictFunctionTypes --strictNullChecks --target es2018 --moduleResolution node src/index.js" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/sparkswap/lnd-engine.git" 30 | }, 31 | "keywords": [ 32 | "sparkswap", 33 | "lnd", 34 | "lightning", 35 | "btc", 36 | "bitcoin", 37 | "ltc", 38 | "litecoin" 39 | ], 40 | "author": "Sparkswap ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/sparkswap/lnd-engine/issues" 44 | }, 45 | "homepage": "https://github.com/sparkswap/lnd-engine#readme", 46 | "devDependencies": { 47 | "chai": "4.2.0", 48 | "chai-as-promised": "7.1.1", 49 | "dirty-chai": "2.0.1", 50 | "eslint": "5.16.0", 51 | "eslint-config-standard": "12.0.0", 52 | "eslint-plugin-import": "2.18.2", 53 | "eslint-plugin-jsdoc": "18.1.1", 54 | "eslint-plugin-node": "10.0.0", 55 | "eslint-plugin-promise": "4.1.1", 56 | "eslint-plugin-standard": "4.0.1", 57 | "http-server": "0.11.1", 58 | "jsdoc": "3.6.3", 59 | "madge": "3.6.0", 60 | "minami": "1.2.3", 61 | "mocha": "6.2.2", 62 | "nyc": "14.0.0", 63 | "rewire": "4.0.1", 64 | "sinon": "7.3.2", 65 | "sinon-chai": "3.3.0", 66 | "timekeeper": "2.2.0", 67 | "timeout-as-promise": "1.0.0", 68 | "typescript": "3.7.2" 69 | }, 70 | "dependencies": { 71 | "@grpc/proto-loader": "0.5.0", 72 | "big.js": "5.2.2", 73 | "compare-versions": "3.5.1", 74 | "grpc": "1.22.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/lnd-setup/index.js: -------------------------------------------------------------------------------- 1 | const generateWalletUnlockerClient = require('./generate-wallet-unlocker-client') 2 | const generateLightningClient = require('./generate-lightning-client') 3 | 4 | /** @typedef {import('grpc').ClientReadableStream} ClientReadableStream */ 5 | 6 | /** @typedef {object} LndClient 7 | * @property {(args: object, opts: object, cb: Function) => undefined} addInvoice 8 | * @property {(args: object) => ClientReadableStream} closeChannel 9 | * @property {(args: object, opts: object, cb: Function) => undefined} connectPeer 10 | * @property {(args: object, opts: object, cb: Function) => undefined} decodePayReq 11 | * @property {(args: object, opts: object, cb: Function) => undefined} describeGraph 12 | * @property {(args: object, opts: object, cb: Function) => undefined} getInfo 13 | * @property {(args: object, opts: object, cb: Function) => undefined} getTransactions 14 | * @property {(args: object, opts: object, cb: Function) => undefined} listChannels 15 | * @property {(args: object, opts: object, cb: Function) => undefined} closedChannels 16 | * @property {(args: object, opts: object, cb: Function) => undefined} listInvoices 17 | * @property {(args: object, opts: object, cb: Function) => undefined} listPayments 18 | * @property {(args: object, opts: object, cb: Function) => undefined} listPeers 19 | * @property {(args: object, opts: object, cb: Function) => undefined} pendingChannels 20 | * @property {(args: object, opts: object, cb: Function) => undefined} lookupInvoice 21 | * @property {(args: object, opts: object, cb: Function) => undefined} lookupPaymentStatus 22 | * @property {(args: object, opts: object, cb: Function) => undefined} newAddress 23 | * @property {(args: object, opts: object, cb: Function) => undefined} openChannelSync 24 | * @property {(args: object, opts: object, cb: Function) => undefined} queryRoutes 25 | * @property {(args: object, opts: object, cb: Function) => undefined} sendCoins 26 | * @property {(args: object, opts: object, cb: Function) => undefined} sendPaymentSync 27 | * @property {(args: object) => ClientReadableStream} sendToRoute 28 | * @property {(args: object, cb: Function) => undefined} updateChannelPolicy 29 | * @property {(args: object, opts: object, cb: Function) => undefined} walletBalance 30 | * @property {object} invoices 31 | * @property {(args: object) => ClientReadableStream} subscribeSingleInvoice 32 | * @property {object} router 33 | * @property {(args: object) => ClientReadableStream} trackPayment 34 | * @property {() => void} close 35 | */ 36 | 37 | /** @typedef {object} LndWalletUnlockerClient 38 | * @property {(args: object, opts: object, cb: Function) => undefined} genSeed 39 | * @property {(args: object, opts: object, cb: Function) => undefined} initWallet 40 | * @property {(args: object, opts: object, cb: Function) => undefined} unlockWallet 41 | * @property {(args: object, opts: object, cb: Function) => undefined} changePassword 42 | * @property {() => void} close 43 | */ 44 | 45 | module.exports = { 46 | generateWalletUnlockerClient, 47 | generateLightningClient 48 | } 49 | -------------------------------------------------------------------------------- /src/lnd-setup/generate-lightning-client.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Lnd Lightning Client Module 4 | * @module src/lnd-setup/generate-lightning0-client 5 | */ 6 | 7 | const grpc = require('grpc') 8 | const loadProto = require('../utils/load-proto') 9 | const fs = require('fs') 10 | const { ENGINE_STATUSES } = require('../constants') 11 | 12 | /** @typedef {import('.').LndClient} LndClient */ 13 | /** @typedef {import('..').Logger} Logger */ 14 | 15 | /** 16 | * Array of lnd proto files to load to generate lightning client 17 | * @type {ReadonlyArray} 18 | */ 19 | const PROTO_FILES = Object.freeze([ 20 | 'rpc.proto', 21 | 'invoicesrpc/invoices.proto', 22 | 'routerrpc/router.proto' 23 | ]) 24 | 25 | /** 26 | * Generates a lnrpc.Lightning client which allows full functionality of the LND node 27 | * 28 | * @function 29 | * @param {object} engine 30 | * @param {string} engine.host 31 | * @param {string} engine.protoPath 32 | * @param {string} engine.tlsCertPath 33 | * @param {string} engine.macaroonPath 34 | * @param {string} engine.status 35 | * @param {Logger} engine.logger 36 | * @returns {LndClient} lnrpc Lightning client definition 37 | */ 38 | function generateLightningClient ({ host, protoPath, tlsCertPath, macaroonPath, status, logger }) { 39 | const { lnrpc, invoicesrpc, routerrpc } = loadProto(protoPath, PROTO_FILES) 40 | 41 | const macaroonExists = fs.existsSync(macaroonPath) 42 | 43 | // We require all communication to be done via TLS, if there is no cert available, 44 | // we will throw 45 | if (!fs.existsSync(tlsCertPath)) { 46 | throw new Error(`LND-ENGINE error - tls cert file not found at path: ${tlsCertPath}`) 47 | } 48 | 49 | const tls = fs.readFileSync(tlsCertPath) 50 | 51 | let rpcCredentials 52 | 53 | // We do not require a macaroon to be available to use the lnd-engine, as the user 54 | // may have used the configuration `--no-macaroons` for testing. 55 | // 56 | // A macaroon will not be created if: 57 | // 1. An LND wallet hasn't been initialized (NEEDS_WALLET or UNKNOWN): expected 58 | // 2. The daemon/docker has messed up: unexpected 59 | const showMacaroonWarn = ![ ENGINE_STATUSES.UNKNOWN, ENGINE_STATUSES.NEEDS_WALLET ].includes(status) 60 | 61 | if (!macaroonExists) { 62 | if (showMacaroonWarn) { 63 | logger.warn(`LND-ENGINE warning - macaroon not found at path: ${macaroonPath}`) 64 | } 65 | rpcCredentials = grpc.credentials.createSsl(tls) 66 | } else { 67 | const macaroon = fs.readFileSync(macaroonPath) 68 | const metadata = new grpc.Metadata() 69 | metadata.add('macaroon', macaroon.toString('hex')) 70 | const macaroonCredentials = grpc.credentials.createFromMetadataGenerator((_, cb) => cb(null, metadata)) 71 | 72 | const sslCredentials = grpc.credentials.createSsl(tls) 73 | rpcCredentials = grpc.credentials.combineChannelCredentials(sslCredentials, macaroonCredentials) 74 | } 75 | 76 | const client = new lnrpc.Lightning(host, rpcCredentials, {}) 77 | client.invoices = new invoicesrpc.Invoices(host, rpcCredentials, {}) 78 | client.router = new routerrpc.Router(host, rpcCredentials, {}) 79 | return client 80 | } 81 | 82 | module.exports = generateLightningClient 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sparkswap - sparkswap.com 2 | 3 | [![CircleCI](https://circleci.com/gh/sparkswap/lnd-engine.svg?style=svg&circle-token=47c81b3a717f062885f159dfded078e134413db1)](https://circleci.com/gh/sparkswap/lnd-engine) 4 | 5 | Sparkswap LND Engine 6 | ==================== 7 | 8 | The following repo contains 2 modules that make up a Sparkswap Payment Channel Network Engine: 9 | 10 | 1. NPM module w/ LND abstraction layer (located in `src`) 11 | 2. Dockerfiles for all containers needed for the LND Engine to run on a broker daemon 12 | 13 | A current docker setup a functional BTC/LTC LND Engine: 14 | 1. Bitcoin node (currently bitcoind) 15 | 2. LND BTC node (Sparkswap fork) 16 | 3. Litecoin node (currently litecoind) 17 | 4. LND LTC node (Sparkswap fork) 18 | 19 | #### Installation (lnd-engine only) 20 | 21 | The following commands will install dependencies, import associated proto files for 22 | the lnd-engine codebase and will build all docker images 23 | 24 | ``` 25 | npm run build 26 | ``` 27 | 28 | To run tests, use `npm run test` 29 | 30 | #### Installation w/ Docker 31 | 32 | The lnd-engine docker files make use of Docker's internal image storage (to replicate the functionality of docker hub locally). Run the `npm run build` command to 33 | update all docker images on your local docker installation. 34 | 35 | #### Library Usage 36 | 37 | ``` 38 | const LndEngine = require('lnd-engine') 39 | 40 | const engineOptions = { 41 | logger: ..., 42 | tlsCertPath: /absolute/path/to/tls/cert, // required 43 | macaroonPath: /absolute/path/to/macaroon, // required 44 | } 45 | 46 | const engine = new LndEngine('0.0.0.0:10009', 'BTC', engineOptions) 47 | 48 | engine.getUncommittedBalance.... etc 49 | ``` 50 | 51 | # API 52 | 53 | Documentation is up-to-date as of v0.2.0-alpha-release 54 | 55 | **NOTE:** Please see detailed documentation at [sparkswap.com/docs/engines/lnd](https://sparkswap.com/docs/engines/lnd) 56 | 57 | ### Healthcheck 58 | 59 | ``` 60 | isAvailable 61 | validateEngine 62 | ``` 63 | 64 | ### Addresses 65 | 66 | ``` 67 | createNewAddress 68 | getPaymentChannelNetworkAddress 69 | getPublicKey 70 | ``` 71 | 72 | ### Invoices 73 | 74 | ``` 75 | createInvoice 76 | createRefundInvoice 77 | getInvoiceValue 78 | getInvoices 79 | isInvoicePaid 80 | payInvoice 81 | ``` 82 | 83 | ### Cross-Chain Atomic Swaps 84 | 85 | ``` 86 | createSwapHash 87 | executeSwap 88 | getSettledSwapPreimage 89 | prepareSwap 90 | translateSwap 91 | ``` 92 | 93 | ### Balances 94 | 95 | ``` 96 | getConfirmedBalance 97 | getUncommittedBalance 98 | getTotalChannelBalance 99 | getUnconfirmedBalance 100 | isBalanceSufficient 101 | getTotalPendingChannelBalance 102 | getUncommittedPendingBalance 103 | getPendingChannelCapacities 104 | getOpenChannelCapacities 105 | getMaxChannel 106 | getMaxChannelForAddress 107 | ``` 108 | 109 | ### Channels 110 | 111 | ``` 112 | createChannel 113 | numChannelsForAddress 114 | closeChannels 115 | getChannelsForRemoteAddress 116 | connectUser 117 | getPeers 118 | ``` 119 | 120 | ### Wallets 121 | ``` 122 | withdrawFunds 123 | changeWalletPassword 124 | ``` 125 | 126 | ## License 127 | 128 | The Sparkswap LND Engine is licensed under the [MIT License](./LICENSE). 129 | -------------------------------------------------------------------------------- /src/engine-actions/wait-for-swap-commitment.js: -------------------------------------------------------------------------------- 1 | const grpc = require('grpc') 2 | const { subscribeSingleInvoice } = require('../lnd-actions') 3 | const { INVOICE_STATES } = require('../constants') 4 | 5 | class SettledSwapError extends Error {} 6 | class CanceledSwapError extends Error {} 7 | class ExpiredSwapError extends Error {} 8 | 9 | /** 10 | * Waits for a swap to enter the ACCEPTED state and errors if it won't happen 11 | * 12 | * @param {string} swapHash - hash for a swap 13 | * @returns {Promise} - creation date of the HTLC 14 | */ 15 | async function waitForSwapCommitment (swapHash) { 16 | return new Promise((resolve, reject) => { 17 | let timer 18 | 19 | // subscribeSingleInvoice always sends out the initial invoice state 20 | const stream = subscribeSingleInvoice(swapHash, { client: this.client }) 21 | 22 | const cleanup = () => { 23 | stream.removeAllListeners() 24 | // stop the timer on invoice expiration if it is still active 25 | if (timer) { 26 | clearTimeout(timer) 27 | } 28 | } 29 | 30 | /** 31 | * Error handler for gRPC stream 32 | * @param {import('grpc').ServiceError} e 33 | */ 34 | const errHandler = (e) => { 35 | // CANCELLED events get emitted when we call `stream.cancel()` 36 | // so we want to handle those as expected, not errors 37 | if (e.code !== grpc.status.CANCELLED) { 38 | reject(e) 39 | } 40 | cleanup() 41 | } 42 | 43 | stream.on('error', errHandler) 44 | 45 | stream.on('end', () => { 46 | reject(new Error('Stream ended while waiting for commitment on ' + swapHash)) 47 | cleanup() 48 | }) 49 | 50 | stream.on('data', (invoice) => { 51 | switch (invoice.state) { 52 | case INVOICE_STATES.OPEN: 53 | const creationDate = new Date(invoice.creationDate * 1000) 54 | const expirationDate = new Date( 55 | creationDate.getTime() + (invoice.expiry * 1000) 56 | ) 57 | 58 | // if the invoice is open but expired, throw an error 59 | // indicating that. 60 | if (new Date() > expirationDate) { 61 | reject(new ExpiredSwapError(`Swap with hash (${swapHash}) is expired.`)) 62 | stream.cancel() 63 | return 64 | } 65 | // once the invoice is expired, treat it as such 66 | if (!timer) { 67 | timer = setTimeout(() => { 68 | reject(new ExpiredSwapError(`Swap with hash (${swapHash}) is expired.`)) 69 | stream.cancel() 70 | }, expirationDate.getTime() - (new Date()).getTime()) 71 | } 72 | break 73 | case INVOICE_STATES.SETTLED: 74 | reject(new SettledSwapError(`Swap with hash (${swapHash}) is already settled.`)) 75 | stream.cancel() 76 | break 77 | case INVOICE_STATES.CANCELED: 78 | reject(new CanceledSwapError(`Swap with hash (${swapHash}) is canceled.`)) 79 | stream.cancel() 80 | break 81 | case INVOICE_STATES.ACCEPTED: 82 | resolve(new Date(invoice.creationDate * 1000)) 83 | stream.cancel() 84 | break 85 | } 86 | }) 87 | }) 88 | } 89 | 90 | module.exports = { 91 | waitForSwapCommitment, 92 | SettledSwapError, 93 | CanceledSwapError, 94 | ExpiredSwapError 95 | } 96 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ################################################ 4 | # Build script for LND-engine 5 | # 6 | # Params: 7 | # - LND_PROTO_URL (optional) 8 | # - INCLUDE_DOCKER (optional, defaults to false) 9 | ################################################ 10 | 11 | set -e -u 12 | 13 | ARG=${1:-false} 14 | 15 | echo "" 16 | echo " ▄▄██████▄▄ ▄▄ " 17 | echo " ▄██████████▀█▄ ██ " 18 | echo " ████████▀▀ ▄████ ▄███▄ ████▄ ████▄ ███ ██ ██ ▄███▄ ██ ██ ██ ████▄ ████▄ " 19 | echo "█████▀ ▄███████ ██ ██ ██ █ ██▀ ████▀ ██ ██▄██▄██ █ ██ ██" 20 | echo "██████▄ ▀██████ ▀███▄ ██ ██ ▄████ ██ ████ ▀███▄ ██████ ▄████ ██ ██" 21 | echo "███████▀ ▄█████ ██ ██ ██ ██ █ ██ ██ ██ ██ ▀██ █▀ ██ █ ██ ██" 22 | echo " ████▀ ▄▄████████ ▀███▀ ████▀ █████ ██ ██ ██ ▀███▀ ██ █ █████ ████▀ " 23 | echo " ▀█▄██████████▀ ██ ██ " 24 | echo " ▀▀██████▀▀ ▀▀ ▀▀ " 25 | echo " https://sparkswap.com" 26 | echo "" 27 | echo "LND Engine Build starting..." 28 | echo "" 29 | echo "" 30 | 31 | LND_VERSION='v0.8.0-beta' 32 | 33 | # Downloads an LND proto file from the sparkswap/lnd fork 34 | LND_PROTO_URL="https://raw.githubusercontent.com/lightningnetwork/lnd/${LND_VERSION}/lnrpc/rpc.proto" 35 | INVOICES_PROTO_URL="https://raw.githubusercontent.com/lightningnetwork/lnd/${LND_VERSION}/lnrpc/invoicesrpc/invoices.proto" 36 | ROUTER_PROTO_URL="https://raw.githubusercontent.com/lightningnetwork/lnd/${LND_VERSION}/lnrpc/routerrpc/router.proto" 37 | 38 | rm -rf ./proto 39 | mkdir -p ./proto/invoicesrpc 40 | mkdir -p ./proto/routerrpc 41 | 42 | echo "Downloading lnd proto files for version: ${LND_VERSION}" 43 | curl -o ./proto/rpc.proto $LND_PROTO_URL 44 | curl -o ./proto/invoicesrpc/invoices.proto $INVOICES_PROTO_URL 45 | curl -o ./proto/routerrpc/router.proto $ROUTER_PROTO_URL 46 | 47 | echo "Building images for lnd-engine" 48 | 49 | # We add a COMMIT_SHA argument to the lnd dockerfile to trigger cache-invalidation 50 | # when get git clone the sparkswap/lnd repo. Without it, docker would continue 51 | # to cache old code and we would never receive updates from the fork. 52 | # COMMIT_SHA=`git ls-remote git://github.com/sparkswap/lnd | grep "refs/heads/$LND_VERSION$" | cut -f 1` 53 | 54 | # If our LND_VERSION is a tag or a commit, we can use it directly 55 | COMMIT_SHA=${LND_VERSION} 56 | 57 | LND_BTC_NODE=bitcoind 58 | LND_LTC_NODE=litecoind 59 | 60 | ENGINE_VERSION=$(node -pe "require('./package.json').version") 61 | 62 | # NOTE: The names specified with `-t` directly map to the our service names in 63 | # on sparkswap dockerhub 64 | docker build -t sparkswap/lnd_btc:$ENGINE_VERSION ./docker/lnd \ 65 | --build-arg NODE=$LND_BTC_NODE \ 66 | --build-arg NETWORK=btc \ 67 | --build-arg COMMIT_SHA=$COMMIT_SHA 68 | 69 | docker build -t sparkswap/lnd_ltc:$ENGINE_VERSION ./docker/lnd \ 70 | --build-arg NODE=$LND_LTC_NODE \ 71 | --build-arg NETWORK=ltc \ 72 | --build-arg COMMIT_SHA=$COMMIT_SHA 73 | 74 | # Create bitcoind and litecoind images 75 | docker build -t sparkswap/bitcoind:$ENGINE_VERSION ./docker/bitcoind 76 | docker build -t sparkswap/litecoind:$ENGINE_VERSION ./docker/litecoind 77 | -------------------------------------------------------------------------------- /src/engine-actions/get-channels-for-remote-address.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getChannelsForRemoteAddress = rewire(path.resolve(__dirname, 'get-channels-for-remote-address')) 5 | 6 | describe('getChannelsForRemoteAddress', () => { 7 | let listChannelsStub 8 | let listPendingChannelsStub 9 | let channel 10 | let channels 11 | let pendingOpenChannels 12 | let pendingChannel 13 | let reverts 14 | let address 15 | let loggerStub 16 | let networkAddressFormatterStub 17 | 18 | beforeEach(() => { 19 | address = 'bolt:asdf@localhost' 20 | channel = { remotePubkey: 'asdf' } 21 | channels = [channel, channel] 22 | pendingChannel = { channel: { remoteNodePub: 'asdf' } } 23 | pendingOpenChannels = [pendingChannel] 24 | listChannelsStub = sinon.stub().returns({ channels }) 25 | listPendingChannelsStub = sinon.stub().returns({ pendingOpenChannels }) 26 | loggerStub = { 27 | debug: sinon.stub(), 28 | error: sinon.stub() 29 | } 30 | networkAddressFormatterStub = { 31 | parse: sinon.stub().returns({ publicKey: 'asdf' }) 32 | } 33 | 34 | reverts = [] 35 | reverts.push(getChannelsForRemoteAddress.__set__('listChannels', listChannelsStub)) 36 | reverts.push(getChannelsForRemoteAddress.__set__('listPendingChannels', listPendingChannelsStub)) 37 | reverts.push(getChannelsForRemoteAddress.__set__('logger', loggerStub)) 38 | reverts.push(getChannelsForRemoteAddress.__set__('networkAddressFormatter', networkAddressFormatterStub)) 39 | }) 40 | 41 | afterEach(() => { 42 | reverts.forEach(r => r()) 43 | }) 44 | 45 | it('gets all available channels', async () => { 46 | await getChannelsForRemoteAddress(address) 47 | expect(listChannelsStub).to.have.been.called() 48 | }) 49 | 50 | it('gets all pending channels', async () => { 51 | await getChannelsForRemoteAddress(address) 52 | expect(listPendingChannelsStub).to.have.been.called() 53 | }) 54 | 55 | it('parses the publickey out of the network address', async () => { 56 | await getChannelsForRemoteAddress(address) 57 | expect(networkAddressFormatterStub.parse).to.have.been.called() 58 | expect(networkAddressFormatterStub.parse).to.have.been.calledWith(address) 59 | }) 60 | 61 | it('returns the number of active and pending channels for the given pubkey', async () => { 62 | const res = await getChannelsForRemoteAddress(address) 63 | expect(res.length).to.eql(3) 64 | }) 65 | 66 | it('filters out channels that do not have the pubkey', async () => { 67 | const anotherChannel = { remotePubkey: 'differentpubkey' } 68 | channels = [channel, anotherChannel] 69 | listChannelsStub.returns({ channels }) 70 | const res = await getChannelsForRemoteAddress(address) 71 | expect(res.length).to.eql(2) 72 | }) 73 | 74 | it('returns early if there are no active or pending channels', async () => { 75 | channels = [] 76 | pendingOpenChannels = [] 77 | listChannelsStub.returns({ channels }) 78 | listPendingChannelsStub.returns({ pendingOpenChannels }) 79 | const res = await getChannelsForRemoteAddress(address) 80 | expect(networkAddressFormatterStub.parse).to.not.have.been.called() 81 | expect(res.length).to.eql(0) 82 | expect(loggerStub.debug).to.have.been.calledWith('getChannelsForRemoteAddress: No channels exist') 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/engine-actions/get-channels.js: -------------------------------------------------------------------------------- 1 | const { 2 | listChannels, 3 | listClosedChannels 4 | } = require('../lnd-actions') 5 | const { networkAddressFormatter } = require('../utils') 6 | 7 | /** @typedef {object} Channel 8 | * @property {string} chanId 9 | * @property {boolean} active 10 | * @property {string} remotePubkey 11 | * @property {string} channelPoint 12 | * @property {string} closingTxHash 13 | * @property {string} capacity 14 | */ 15 | 16 | /** 17 | * @typedef {object} NormalizedChannel 18 | * @property {string} channelId Unique ID of the channel 19 | * @property {string} remoteAddress Payment Channel Network address of the peer with whom the channel exists 20 | * @property {string} openTransaction Transaction that represents the opening of the channel 21 | * @property {string} closeTransaction Transaction that closed the channel (if it is closed) 22 | * @property {boolean} active Whether the channel is currently active 23 | * @property {string} capacity Total capacity of the channel 24 | */ 25 | 26 | /** 27 | * Returns all channels, both open and closed (but not pending) 28 | * @returns {Promise>} 29 | */ 30 | async function getChannels () { 31 | const { client } = this 32 | 33 | const [ 34 | openChannels, 35 | closedChannels 36 | ] = await Promise.all([ 37 | getOpenChannels({ client }), 38 | getClosedChannels({ client }) 39 | ]) 40 | 41 | // merge open and closed channels, with closed channels overwriting 42 | // open ones since they contain more data (close transactions) 43 | const channels = new Map([...openChannels, ...closedChannels]) 44 | 45 | // return an array of all channels 46 | return Array.from(channels.values()) 47 | } 48 | 49 | /** 50 | * Get all open channels 51 | * @param {object} options 52 | * @param {object} options.client 53 | * @returns {Promise>} Map of channels by channel ID 54 | */ 55 | async function getOpenChannels ({ client }) { 56 | const { channels } = await listChannels({ client }) 57 | 58 | return normalizeChannels(channels) 59 | } 60 | 61 | /** 62 | * Get all closed channels 63 | * @param {object} options 64 | * @param {object} options.client 65 | * @returns {Promise>} Map of channels by channel ID 66 | */ 67 | async function getClosedChannels ({ client }) { 68 | const { channels } = await listClosedChannels({ client }) 69 | 70 | return normalizeChannels(channels) 71 | } 72 | 73 | /** 74 | * Normalize an array of LND channels 75 | * @param {Array} channels - Array of channels returned from LND 76 | * @returns {Map} Map of normalized channels keyed by their ID 77 | */ 78 | function normalizeChannels (channels) { 79 | return channels.reduce((map, channel) => { 80 | const normalized = normalizeChannel(channel) 81 | map.set(normalized.channelId, normalized) 82 | return map 83 | }, new Map()) 84 | } 85 | 86 | /** 87 | * Convert a channel from a returned value from LND into 88 | * a standard format. 89 | * @see https://api.lightning.community/#channel 90 | * @param {object} channel - channel object from LND 91 | * @returns {NormalizedChannel} Channel with standard field names 92 | */ 93 | function normalizeChannel (channel) { 94 | return { 95 | channelId: channel.chanId, 96 | active: channel.active, 97 | remoteAddress: networkAddressFormatter.serialize({ publicKey: channel.remotePubkey }), 98 | openTransaction: channel.channelPoint, 99 | closeTransaction: channel.closingTxHash, 100 | capacity: channel.capacity 101 | } 102 | } 103 | 104 | module.exports = getChannels 105 | -------------------------------------------------------------------------------- /src/engine-actions/get-max-channel.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { expect, rewire, sinon } = require('test/test-helper') 3 | 4 | const getMaxChannel = rewire(path.resolve(__dirname, 'get-max-channel')) 5 | 6 | describe('getMaxChannel', () => { 7 | let channels 8 | let listChannelsStub 9 | let pendingChannels 10 | let listPendingChannelsStub 11 | let logger 12 | let revertChannels 13 | let revertPendingChannels 14 | 15 | beforeEach(() => { 16 | channels = [ 17 | { localBalance: '10', remoteBalance: '300' }, 18 | { localBalance: '0', remoteBalance: '100' }, 19 | { localBalance: '20', remoteBalance: '1000' }, 20 | { localBalance: '10', remoteBalance: '15' } 21 | ] 22 | 23 | pendingChannels = [ 24 | { channel: { localBalance: '50', remoteBalance: '30' } }, 25 | { channel: { localBalance: '0', remoteBalance: '100' } } 26 | ] 27 | 28 | logger = { 29 | debug: sinon.stub() 30 | } 31 | 32 | listChannelsStub = sinon.stub().resolves({ channels }) 33 | listPendingChannelsStub = sinon.stub().resolves({ pendingOpenChannels: pendingChannels }) 34 | 35 | getMaxChannel.__set__('logger', logger) 36 | revertChannels = getMaxChannel.__set__('listChannels', listChannelsStub) 37 | revertPendingChannels = getMaxChannel.__set__('listPendingChannels', listPendingChannelsStub) 38 | }) 39 | 40 | afterEach(() => { 41 | revertChannels() 42 | revertPendingChannels() 43 | }) 44 | 45 | it('returns zero if no channels exist', async () => { 46 | listChannelsStub.resolves({}) 47 | listPendingChannelsStub.resolves({}) 48 | 49 | const expectedRes = { maxBalance: '0' } 50 | const res = await getMaxChannel() 51 | expect(logger.debug).to.have.been.calledWith('getMaxChannel: No open or pending channels exist') 52 | expect(res).to.eql(expectedRes) 53 | }) 54 | 55 | context('checking outbound channels', () => { 56 | it('returns max balance of open channels if there are open channels only', async () => { 57 | listPendingChannelsStub.resolves({}) 58 | const res = await getMaxChannel() 59 | const expectedRes = { maxBalance: '20' } 60 | return expect(res).to.eql(expectedRes) 61 | }) 62 | 63 | it('returns max pending balance of pending channels if there are pending channels only', async () => { 64 | listChannelsStub.resolves({}) 65 | const res = await getMaxChannel() 66 | const expectedRes = { maxBalance: '50' } 67 | return expect(res).to.eql(expectedRes) 68 | }) 69 | 70 | it('returns max balance of both open and pending channels if open and pending channels exist', async () => { 71 | const res = await getMaxChannel() 72 | const expectedRes = { maxBalance: '50' } 73 | return expect(res).to.eql(expectedRes) 74 | }) 75 | }) 76 | 77 | context('checking inbound channels', () => { 78 | it('returns max balance of open channels if there are open channels only', async () => { 79 | listPendingChannelsStub.resolves({}) 80 | const res = await getMaxChannel({ outbound: false }) 81 | const expectedRes = { maxBalance: '1000' } 82 | return expect(res).to.eql(expectedRes) 83 | }) 84 | 85 | it('returns max pending balance of pending channels if there are pending channels only', async () => { 86 | listChannelsStub.resolves({}) 87 | const res = await getMaxChannel({ outbound: false }) 88 | const expectedRes = { maxBalance: '100' } 89 | return expect(res).to.eql(expectedRes) 90 | }) 91 | 92 | it('returns max balance of both open and pending channels if open and pending channels exist', async () => { 93 | const res = await getMaxChannel({ outbound: false }) 94 | const expectedRes = { maxBalance: '1000' } 95 | return expect(res).to.eql(expectedRes) 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /proto/invoicesrpc/invoices.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/api/annotations.proto"; 4 | import "rpc.proto"; 5 | 6 | package invoicesrpc; 7 | 8 | option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"; 9 | 10 | // Invoices is a service that can be used to create, accept, settle and cancel 11 | // invoices. 12 | service Invoices { 13 | /** 14 | SubscribeSingleInvoice returns a uni-directional stream (server -> client) 15 | to notify the client of state transitions of the specified invoice. 16 | Initially the current invoice state is always sent out. 17 | */ 18 | rpc SubscribeSingleInvoice (SubscribeSingleInvoiceRequest) returns (stream lnrpc.Invoice); 19 | 20 | /** 21 | CancelInvoice cancels a currently open invoice. If the invoice is already 22 | canceled, this call will succeed. If the invoice is already settled, it will 23 | fail. 24 | */ 25 | rpc CancelInvoice(CancelInvoiceMsg) returns (CancelInvoiceResp); 26 | 27 | /** 28 | AddHoldInvoice creates a hold invoice. It ties the invoice to the hash 29 | supplied in the request. 30 | */ 31 | rpc AddHoldInvoice(AddHoldInvoiceRequest) returns (AddHoldInvoiceResp); 32 | 33 | /** 34 | SettleInvoice settles an accepted invoice. If the invoice is already 35 | settled, this call will succeed. 36 | */ 37 | rpc SettleInvoice(SettleInvoiceMsg) returns (SettleInvoiceResp); 38 | } 39 | 40 | message CancelInvoiceMsg { 41 | /// Hash corresponding to the (hold) invoice to cancel. 42 | bytes payment_hash = 1; 43 | } 44 | message CancelInvoiceResp {} 45 | 46 | message AddHoldInvoiceRequest { 47 | /** 48 | An optional memo to attach along with the invoice. Used for record keeping 49 | purposes for the invoice's creator, and will also be set in the description 50 | field of the encoded payment request if the description_hash field is not 51 | being used. 52 | */ 53 | string memo = 1 [json_name = "memo"]; 54 | 55 | /// The hash of the preimage 56 | bytes hash = 2 [json_name = "hash"]; 57 | 58 | /// The value of this invoice in satoshis 59 | int64 value = 3 [json_name = "value"]; 60 | 61 | /** 62 | Hash (SHA-256) of a description of the payment. Used if the description of 63 | payment (memo) is too long to naturally fit within the description field 64 | of an encoded payment request. 65 | */ 66 | bytes description_hash = 4 [json_name = "description_hash"]; 67 | 68 | /// Payment request expiry time in seconds. Default is 3600 (1 hour). 69 | int64 expiry = 5 [json_name = "expiry"]; 70 | 71 | /// Fallback on-chain address. 72 | string fallback_addr = 6 [json_name = "fallback_addr"]; 73 | 74 | /// Delta to use for the time-lock of the CLTV extended to the final hop. 75 | uint64 cltv_expiry = 7 [json_name = "cltv_expiry"]; 76 | 77 | /** 78 | Route hints that can each be individually used to assist in reaching the 79 | invoice's destination. 80 | */ 81 | repeated lnrpc.RouteHint route_hints = 8 [json_name = "route_hints"]; 82 | 83 | /// Whether this invoice should include routing hints for private channels. 84 | bool private = 9 [json_name = "private"]; 85 | } 86 | 87 | message AddHoldInvoiceResp { 88 | /** 89 | A bare-bones invoice for a payment within the Lightning Network. With the 90 | details of the invoice, the sender has all the data necessary to send a 91 | payment to the recipient. 92 | */ 93 | string payment_request = 1 [json_name = "payment_request"]; 94 | } 95 | 96 | message SettleInvoiceMsg { 97 | /// Externally discovered pre-image that should be used to settle the hold invoice. 98 | bytes preimage = 1; 99 | } 100 | 101 | message SettleInvoiceResp {} 102 | 103 | message SubscribeSingleInvoiceRequest { 104 | reserved 1; 105 | 106 | /// Hash corresponding to the (hold) invoice to subscribe to. 107 | bytes r_hash = 2 [json_name = "r_hash"]; 108 | } 109 | -------------------------------------------------------------------------------- /src/engine-actions/wait-for-swap-commitment.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const EventEmitter = require('events') 3 | const { expect, rewire, sinon, timekeeper } = require('test/test-helper') 4 | const { INVOICE_STATES } = require('../constants') 5 | const waitForSwapCommitmentModule = rewire(path.resolve(__dirname, 'wait-for-swap-commitment')) 6 | const { 7 | waitForSwapCommitment, 8 | SettledSwapError, 9 | CanceledSwapError, 10 | ExpiredSwapError 11 | } = waitForSwapCommitmentModule 12 | 13 | describe('wait-for-swap-commitment', () => { 14 | describe('waitForSwapCommitment', () => { 15 | const hash = '1234' 16 | const creationTimestamp = 1564789328 17 | const creationDate = new Date('2019-08-02T23:42:08.000Z') 18 | const shortExpirationDate = new Date('2019-08-02T23:42:19.000Z') 19 | const longExpirationDate = new Date('2019-08-02T23:42:27.950Z') 20 | const openInvoice = { 21 | state: INVOICE_STATES.OPEN, 22 | creationDate: creationTimestamp, 23 | expiry: 3600 24 | } 25 | const settledInvoice = { 26 | state: INVOICE_STATES.SETTLED 27 | } 28 | const canceledInvoice = { 29 | state: INVOICE_STATES.CANCELED 30 | } 31 | const acceptedInvoice = { 32 | state: INVOICE_STATES.ACCEPTED, 33 | creationDate: creationTimestamp 34 | } 35 | const expiredInvoice = { 36 | state: INVOICE_STATES.OPEN, 37 | creationDate: creationTimestamp, 38 | expiry: 10 39 | } 40 | const expiringInvoice = { 41 | state: INVOICE_STATES.OPEN, 42 | creationDate: creationTimestamp, 43 | expiry: 20 44 | } 45 | const stream = new EventEmitter() 46 | stream.cancel = sinon.stub().callsFake(() => { 47 | const err = new Error('CANCELLED') 48 | err.code = waitForSwapCommitmentModule.__get__('grpc').status.CANCELLED 49 | stream.emit('error', err) 50 | }) 51 | const subscribeStub = sinon.stub().returns(stream) 52 | const reverts = [] 53 | 54 | beforeEach(() => { 55 | reverts.push(waitForSwapCommitmentModule.__set__('subscribeSingleInvoice', subscribeStub)) 56 | timekeeper.travel(shortExpirationDate) 57 | }) 58 | 59 | afterEach(() => { 60 | reverts.forEach(revert => revert()) 61 | timekeeper.reset() 62 | }) 63 | 64 | it('waits on open invoice and resolves on accepted invoice', async () => { 65 | setTimeout(() => stream.emit('data', openInvoice), 1) 66 | setTimeout(() => stream.emit('data', openInvoice), 2) 67 | setTimeout(() => stream.emit('data', acceptedInvoice), 3) 68 | expect(await waitForSwapCommitment(hash)).to.be.eql(creationDate) 69 | }) 70 | 71 | it('rejects on an expired invoice', () => { 72 | setTimeout(() => stream.emit('data', expiredInvoice), 1) 73 | return expect(waitForSwapCommitment(hash)) 74 | .to.eventually.be.rejectedWith(ExpiredSwapError) 75 | }) 76 | 77 | it('rejects once an invoice expires', () => { 78 | timekeeper.travel(longExpirationDate) 79 | setTimeout(() => { 80 | stream.emit('data', expiringInvoice) 81 | }, 1) 82 | 83 | return expect(waitForSwapCommitment(hash)) 84 | .to.eventually.be.rejectedWith(ExpiredSwapError) 85 | }) 86 | 87 | it('rejects on stream end', () => { 88 | setTimeout(() => stream.emit('end'), new Error('fake error')) 89 | return expect(waitForSwapCommitment(hash)) 90 | .to.eventually.be.rejectedWith('Stream ended while waiting for commitment') 91 | }) 92 | 93 | it('rejects on stream error', () => { 94 | setTimeout(() => stream.emit('error', new Error('fake error'))) 95 | 96 | return expect(waitForSwapCommitment(hash)) 97 | .to.eventually.be.rejectedWith('fake error') 98 | }) 99 | 100 | it('rejects on canceled invoice', () => { 101 | setTimeout(() => stream.emit('data', canceledInvoice), 1) 102 | return expect(waitForSwapCommitment(hash)) 103 | .to.eventually.be.rejectedWith(CanceledSwapError) 104 | }) 105 | 106 | it('rejects on settled invoice', () => { 107 | setTimeout(() => stream.emit('data', settledInvoice), 1) 108 | return expect(waitForSwapCommitment(hash)) 109 | .to.eventually.be.rejectedWith(SettledSwapError) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /src/engine-actions/close-channels.js: -------------------------------------------------------------------------------- 1 | const { 2 | listChannels, 3 | listPendingChannels, 4 | closeChannel 5 | } = require('../lnd-actions') 6 | 7 | /** 8 | * Closes active channels on the given engine. Will try to close inactive/pending channels 9 | * if `force` option is given 10 | * 11 | * @param {object} options 12 | * @param {object} [options.force=false] - force is true if you want to force close all channels, false if not 13 | * @returns {Promise} returns void on success 14 | * @throws {Error} Inactive/Pending channels exist and can not be closed unless 'force' is set to true 15 | */ 16 | async function closeChannels ({ force = false } = {}) { 17 | const [ 18 | { channels: openChannels = [] } = {}, 19 | { pendingOpenChannels = [] } = {} 20 | ] = await Promise.all([ 21 | // The response from listChannels consists of channels that may be active or inactive 22 | listChannels({ client: this.client }), 23 | listPendingChannels({ client: this.client }) 24 | ]) 25 | 26 | if (!openChannels.length && !pendingOpenChannels.length) { 27 | this.logger.debug('closeChannels: No channels exist') 28 | return 29 | } 30 | 31 | this.logger.debug('Received channels from engine: ', { openChannels, pendingOpenChannels }) 32 | 33 | const activeChannels = openChannels.filter(chan => chan.active) 34 | const inactiveChannels = openChannels.filter(chan => !chan.active) 35 | 36 | // By default, we will always try to cancel active channels 37 | let channelsToClose = activeChannels 38 | 39 | // If we are force-closing channels, then we are safe to add inactive and pending 40 | // channels to be closed 41 | if (force) { 42 | // We need to normalize pendingChannels here because their format is different 43 | // than those received from `listChannels` 44 | const pendingChannels = pendingOpenChannels.map(chan => chan.channel) 45 | 46 | channelsToClose = channelsToClose.concat(inactiveChannels) 47 | channelsToClose = channelsToClose.concat(pendingChannels) 48 | } 49 | 50 | const closedChannelResponses = await Promise.all(channelsToClose.map(channel => close(channel, force, this.client, this.logger))) 51 | 52 | if (force) { 53 | this.logger.debug('Successfully closed channels', closedChannelResponses) 54 | } else { 55 | this.logger.debug('Successfully closed active channels', closedChannelResponses) 56 | } 57 | 58 | // If we have attempted to close channels, but still have inactive or pending channels 59 | // on the engine, then we want to fail and let the consumer know that they must force close 60 | // these channels in order to release ALL funds 61 | if (!force && (inactiveChannels.length || pendingOpenChannels.length)) { 62 | throw new Error('Inactive/pending channels exist. You must use `force` to close') 63 | } 64 | } 65 | 66 | async function close (channel, force, client, logger) { 67 | return new Promise((resolve, reject) => { 68 | try { 69 | const [fundingTxidStr, outputIndex] = channel.channelPoint.split(':') 70 | const channelPoint = { 71 | fundingTxidStr, 72 | outputIndex: parseInt(outputIndex) 73 | } 74 | const close = closeChannel(channelPoint, force, { client }) 75 | 76 | // Helper to make sure we tear down our listeners 77 | const finish = (err, response) => { 78 | close.removeListener('error', errorListener) 79 | close.removeListener('end', endListener) 80 | close.removeListener('data', dataListener) 81 | 82 | if (err) { 83 | return reject(err) 84 | } 85 | 86 | resolve(response) 87 | } 88 | 89 | const errorListener = (err) => { 90 | logger.error('Error from closeChannel stream', err) 91 | return finish(err) 92 | } 93 | 94 | const endListener = () => { 95 | const error = 'LND closed closeChannel stream before returning our value' 96 | logger.error(error) 97 | return finish(new Error(error)) 98 | } 99 | 100 | const dataListener = (response) => { 101 | logger.info('Closing channel', response) 102 | return finish(null, response) 103 | } 104 | 105 | close.on('error', errorListener) 106 | close.on('end', endListener) 107 | close.on('data', dataListener) 108 | } catch (e) { 109 | return reject(e) 110 | } 111 | }) 112 | } 113 | 114 | module.exports = closeChannels 115 | -------------------------------------------------------------------------------- /src/engine-actions/index.js: -------------------------------------------------------------------------------- 1 | const getInvoices = require('./get-invoices') 2 | const getPublicKey = require('./get-public-key') 3 | const getUncommittedBalance = require('./get-uncommitted-balance') 4 | const getConfirmedBalance = require('./get-confirmed-balance') 5 | const getUnconfirmedBalance = require('./get-unconfirmed-balance') 6 | const getInvoiceValue = require('./get-invoice-value') 7 | const getTotalChannelBalance = require('./get-total-channel-balance') 8 | const createChannels = require('./create-channels') 9 | const createInvoice = require('./create-invoice') 10 | const createNewAddress = require('./create-new-address') 11 | const createSwapHash = require('./create-swap-hash') 12 | const isInvoicePaid = require('./is-invoice-paid') 13 | const isBalanceSufficient = require('./is-balance-sufficient') 14 | const payInvoice = require('./pay-invoice') 15 | const prepareSwap = require('./prepare-swap') 16 | const createRefundInvoice = require('./create-refund-invoice') 17 | const getPaymentChannelNetworkAddress = require('./get-payment-channel-network-address') 18 | const { translateSwap, PermanentSwapError } = require('./translate-swap') 19 | const getSettledSwapPreimage = require('./get-settled-swap-preimage') 20 | const numChannelsForAddress = require('./num-channels-for-address') 21 | const getTotalPendingChannelBalance = require('./get-total-pending-channel-balance') 22 | const getUncommittedPendingBalance = require('./get-uncommitted-pending-balance') 23 | const getPendingChannelCapacities = require('./get-pending-channel-capacities') 24 | const getOpenChannelCapacities = require('./get-open-channel-capacities') 25 | const closeChannels = require('./close-channels') 26 | const getMaxChannel = require('./get-max-channel') 27 | const withdrawFunds = require('./withdraw-funds') 28 | const createWallet = require('./create-wallet') 29 | const unlockWallet = require('./unlock-wallet') 30 | const getStatus = require('./get-status') 31 | const getChannels = require('./get-channels') 32 | const getTotalReservedChannelBalance = require('./get-total-reserved-channel-balance') 33 | const getMaxChannelForAddress = require('./get-max-channel-for-address') 34 | const getChannelsForRemoteAddress = require('./get-channels-for-remote-address') 35 | const connectUser = require('./connect-user') 36 | const getPeers = require('./get-peers') 37 | const getChainTransactions = require('./get-chain-transactions') 38 | const changeWalletPassword = require('./change-wallet-password') 39 | const getTotalBalanceForAddress = require('./get-total-balance-for-address') 40 | const recoverWallet = require('./recover-wallet') 41 | const cancelSwap = require('./cancel-swap') 42 | const settleSwap = require('./settle-swap') 43 | const { 44 | waitForSwapCommitment, 45 | SettledSwapError, 46 | CanceledSwapError, 47 | ExpiredSwapError 48 | } = require('./wait-for-swap-commitment.js') 49 | const initiateSwap = require('./initiate-swap') 50 | const getInvoice = require('./get-invoice') 51 | 52 | module.exports = { 53 | validationDependentActions: { 54 | getInvoices, 55 | getUncommittedBalance, 56 | getConfirmedBalance, 57 | getUnconfirmedBalance, 58 | getInvoiceValue, 59 | getTotalChannelBalance, 60 | createChannels, 61 | createInvoice, 62 | createNewAddress, 63 | createSwapHash, 64 | isInvoicePaid, 65 | isBalanceSufficient, 66 | payInvoice, 67 | prepareSwap, 68 | createRefundInvoice, 69 | translateSwap, 70 | getSettledSwapPreimage, 71 | numChannelsForAddress, 72 | getTotalPendingChannelBalance, 73 | getUncommittedPendingBalance, 74 | getPendingChannelCapacities, 75 | getOpenChannelCapacities, 76 | closeChannels, 77 | getMaxChannel, 78 | withdrawFunds, 79 | getChannels, 80 | getTotalReservedChannelBalance, 81 | getMaxChannelForAddress, 82 | getChannelsForRemoteAddress, 83 | connectUser, 84 | getPeers, 85 | getChainTransactions, 86 | getTotalBalanceForAddress, 87 | cancelSwap, 88 | settleSwap, 89 | initiateSwap, 90 | getInvoice 91 | }, 92 | unlockedDependentActions: { 93 | getPublicKey, 94 | getPaymentChannelNetworkAddress 95 | }, 96 | validationIndependentActions: { 97 | createWallet, 98 | recoverWallet, 99 | getStatus, 100 | unlockWallet, 101 | changeWalletPassword, 102 | waitForSwapCommitment 103 | }, 104 | errors: { 105 | SettledSwapError, 106 | CanceledSwapError, 107 | ExpiredSwapError, 108 | PermanentSwapError 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/engine-actions/get-status.js: -------------------------------------------------------------------------------- 1 | const grpc = require('grpc') 2 | const compareVersions = require('compare-versions') 3 | 4 | const { ENGINE_STATUSES } = require('../constants') 5 | const { 6 | getInfo, 7 | genSeed 8 | } = require('../lnd-actions') 9 | const { 10 | generateLightningClient, 11 | generateWalletUnlockerClient 12 | } = require('../lnd-setup') 13 | 14 | /** 15 | * Error message that is returned from LND to let us identify if a wallet exists 16 | * on the current LND instance when using `genSeed`. 17 | * 18 | * @see {@link https://github.com/lightningnetwork/lnd/blob/master/walletunlocker/service.go} 19 | * @constant 20 | * @type {string} 21 | * @default 22 | */ 23 | const WALLET_EXISTS_ERROR_MESSAGE = 'wallet already exists' 24 | 25 | /** 26 | * Returns the state of the current lnd-engine. 27 | * @see {LndEngine#ENGINE_STATUS} 28 | * @returns {Promise} status - ENGINE_STATUS 29 | */ 30 | async function getStatusInternal () { 31 | // Make a call to getInfo to see if lnrpc is up on the current engine. If 32 | // this calls returns successful, then we will attempt to validate the node config. 33 | try { 34 | const info = await getInfo({ client: this.client }) 35 | 36 | // We validate an engines configuration here and return either an UNLOCKED 37 | // or VALIDATED status if the code doesn't error out 38 | const { chains = [], syncedToChain } = info 39 | 40 | if (chains.length === 0) { 41 | this.logger.error('LND has no chains configured.') 42 | return ENGINE_STATUSES.UNLOCKED 43 | } 44 | 45 | if (chains.length > 1) { 46 | this.logger.error(`LndEngine cannot support an LND instance with more than one active chain. Found: ${chains}`) 47 | return ENGINE_STATUSES.UNLOCKED 48 | } 49 | 50 | const { chain: chainName } = chains[0] 51 | 52 | if (chainName !== this.chainName) { 53 | this.logger.error(`Mismatched configuration: Engine is configured for ${this.chainName}, LND is configured for ${chainName}.`) 54 | return ENGINE_STATUSES.UNLOCKED 55 | } 56 | 57 | if (!syncedToChain) { 58 | this.logger.error(`Wallet is not yet synced to the main chain`) 59 | return ENGINE_STATUSES.NOT_SYNCED 60 | } 61 | 62 | const version = info.version.split(' ')[0] 63 | if (this.minVersion && compareVersions(version, this.minVersion) < 0) { 64 | this.logger.error(`LND version is too old: ${version} < ${this.minVersion}`) 65 | return ENGINE_STATUSES.OLD_VERSION 66 | } 67 | 68 | return ENGINE_STATUSES.VALIDATED 69 | } catch (e) { 70 | // If we received an error from the `getInfo` call above, then we want to check 71 | // the error status. If the error status is anything but UNIMPLEMENTED, we can 72 | // assume that the node is unavailable. 73 | // 74 | // UNIMPLEMENTED should only occur when the lnrpc has not started on the 75 | // engine's lnd instance 76 | if (e.code !== grpc.status.UNIMPLEMENTED) { 77 | this.logger.error('LndEngine failed to call getInfo', { error: e.message }) 78 | return ENGINE_STATUSES.UNAVAILABLE 79 | } 80 | 81 | // If the error code IS unimplemented, then we call `genSeed` and make 82 | // a determination if the wallet is locked or needs to be created. 83 | try { 84 | await genSeed({ client: this.walletUnlocker }) 85 | return ENGINE_STATUSES.NEEDS_WALLET 86 | } catch (e) { 87 | if (e.message && e.message.includes(WALLET_EXISTS_ERROR_MESSAGE)) { 88 | return ENGINE_STATUSES.LOCKED 89 | } 90 | 91 | this.logger.error('LndEngine failed to call genSeed', { error: e.message }) 92 | return ENGINE_STATUSES.UNAVAILABLE 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Sets the engine status and returns the state of the current lnd-engine. 99 | * @see {LndEngine#ENGINE_STATUS} 100 | * @returns {Promise} status - ENGINE_STATUS 101 | */ 102 | async function getStatus () { 103 | const { LOCKED, NEEDS_WALLET } = ENGINE_STATUSES 104 | if (this.status === LOCKED || this.status === NEEDS_WALLET) { 105 | // LND gets stuck in the LOCKED state due to the way the grpc library works 106 | // regenerating the lightning client and wallet unlocker is a workaround 107 | // see: https://github.com/grpc/grpc-node/issues/993 108 | this.client.close() 109 | this.walletUnlocker.close() 110 | this.client = generateLightningClient(this) 111 | this.walletUnlocker = generateWalletUnlockerClient(this) 112 | } 113 | this.status = await getStatusInternal.call(this) 114 | return this.status 115 | } 116 | 117 | module.exports = getStatus 118 | --------------------------------------------------------------------------------