diff --git a/class/Paym.js b/class/Paym.js index f63f40b..2c32492 100644 --- a/class/Paym.js +++ b/class/Paym.js @@ -2,23 +2,128 @@ var crypto = require('crypto'); var lightningPayReq = require('bolt11'); import { BigNumber } from 'bignumber.js'; -export class Payment { +export class Paym { constructor(redis, bitcoindrpc, lightning) { this._redis = redis; this._bitcoindrpc = bitcoindrpc; this._lightning = lightning; this._decoded = false; + this._bolt11 = false; + this._isPaid = null; + } + + setInvoice(bolt11) { + this._bolt11 = bolt11; } async decodePayReqViaRpc(invoice) { + let that = this; return new Promise(function(resolve, reject) { - this._lightning.decodePayReq({ pay_req: invoice }, function(err, info) { + that._lightning.decodePayReq({ pay_req: invoice }, function(err, info) { if (err) return reject(err); + that._decoded = info; return resolve(info); }); }); } + async queryRoutes() { + if (!this._bolt11) throw new Error('bolt11 is not provided'); + if (!this._decoded) await this.decodePayReqViaRpc(this._bolt11); + + var request = { + pub_key: this._decoded.destination, + amt: this._decoded.num_satoshis, + num_routes: 1, + final_cltv_delta: 144, + fee_limit: { fixed: Math.floor(this._decoded.num_satoshis * 0.01) }, + }; + let that = this; + return new Promise(function(resolve, reject) { + that._lightning.queryRoutes(request, function(err, response) { + if (err) return reject(err); + resolve(response); + }); + }); + } + + async sendToRouteSync(routes) { + if (!this._bolt11) throw new Error('bolt11 is not provided'); + if (!this._decoded) await this.decodePayReqViaRpc(this._bolt11); + + let request = { + payment_hash_string: this._decoded.payment_hash, + routes: routes, + }; + + let that = this; + return new Promise(function(resolve, reject) { + that._lightning.sendToRouteSync(request, function(err, response) { + if (err) reject(err); + resolve(that.processSendPaymentResponse(response)); + }); + }); + } + + processSendPaymentResponse(payment) { + if (payment && payment.payment_route && payment.payment_route.total_amt_msat) { + // paid just now + this._isPaid = true; + payment.payment_route.total_fees = +payment.payment_route.total_fees + Math.floor(+payment.payment_route.total_amt * 0.01); + if (this._bolt11) payment.pay_req = this._bolt11; + if (this._decoded) payment.decoded = this._decoded; + } + + if (payment.payment_error && payment.payment_error.indexOf('already paid') !== -1) { + // already paid + this._isPaid = true; + if (this._decoded) { + payment.decoded = this._decoded; + if (this._bolt11) payment.pay_req = this._bolt11; + // trying to guess the fee + payment.payment_route = payment.payment_route || {}; + payment.payment_route.total_fees = Math.floor(this._decoded.num_satoshis * 0.01); + payment.payment_route.total_amt = this._decoded.num_satoshis; + } + } + + if (payment.payment_error && payment.payment_error.indexOf('unable to') !== -1) { + // failed to pay + this._isPaid = false; + } + + if (payment.payment_error && payment.payment_error.indexOf('FinalExpiryTooSoon') !== -1) { + this._isPaid = false; + } + + if (payment.payment_error && payment.payment_error.indexOf('payment is in transition') !== -1) { + this._isPaid = null; // null is default, but lets set it anyway + } + + return payment; + } + + /** + * Returns NULL if unknown, true if its paid, false if its unpaid + * (judging by error in sendPayment response) + * + * @returns {boolean|null} + */ + getIsPaid() { + return this._isPaid; + } + + async attemptPayToRoute() { + let routes = await this.queryRoutes(); + return await this.sendToRouteSync(routes.routes); + } + + async isExpired() { + if (!this._bolt11) throw new Error('bolt11 is not provided'); + const decoded = await this.decodePayReqViaRpc(this._bolt11); + return +decoded.timestamp + +decoded.expiry < +new Date() / 1000; + } + decodePayReq(payReq) { this._decoded = lightningPayReq.decode(payReq); } diff --git a/class/User.js b/class/User.js index a2b62e0..3920e4b 100644 --- a/class/User.js +++ b/class/User.js @@ -453,4 +453,20 @@ export class User { .digest() .toString('hex'); } + + /** + * Shuffles array in place. ES6 version + * @param {Array} a items An array containing the items. + */ + static _shuffle(a) { + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; + } + + static async _sleep(s) { + return new Promise(r => setTimeout(r, s * 1000)); + } } diff --git a/class/index.js b/class/index.js index 4bb7d4d..16370be 100644 --- a/class/index.js +++ b/class/index.js @@ -1,2 +1,3 @@ export * from './User'; export * from './Lock'; +export * from './Paym'; diff --git a/controllers/api.js b/controllers/api.js index 922be87..34267a1 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -1,4 +1,4 @@ -import { User, Lock } from '../class/'; +import { User, Lock, Paym } from '../class/'; const config = require('../config'); let express = require('express'); let router = express.Router(); @@ -187,7 +187,8 @@ router.post('/payinvoice', async function(req, res) { // payment callback await u.unlockFunds(req.body.invoice); if (payment && payment.payment_route && payment.payment_route.total_amt_msat) { - payment.payment_route.total_fees = +payment.payment_route.total_fees + Math.floor(+payment.payment_route.total_amt * 0.01); + let PaymentShallow = new Paym(false, false, false); + payment = PaymentShallow.processSendPaymentResponse(payment); userBalance -= +payment.payment_route.total_fees + +payment.payment_route.total_amt; u.saveBalance(userBalance); payment.pay_req = req.body.invoice; diff --git a/rpc.proto b/rpc.proto index e453779..d557f25 100644 --- a/rpc.proto +++ b/rpc.proto @@ -3,6 +3,9 @@ syntax = "proto3"; // import "google/api/annotations.proto"; package lnrpc; + +option go_package = "github.com/lightningnetwork/lnd/lnrpc"; + /** * Comments in this file will be directly parsed into the API * Documentation as descriptions of the associated method, message, or field. @@ -231,6 +234,16 @@ service Lightning { }; } + /** lncli: `listunspent` + ListUnspent returns a list of all utxos spendable by the wallet with a + number of confirmations between the specified minimum and maximum. + */ + rpc ListUnspent (ListUnspentRequest) returns (ListUnspentResponse) { + option (google.api.http) = { + get: "/v1/utxos" + }; + } + /** SubscribeTransactions creates a uni-directional stream from the server to the client in which any newly discovered transactions relevant to the @@ -260,7 +273,12 @@ service Lightning { signature string is `zbase32` encoded and pubkey recoverable, meaning that only the message digest and signature are needed for verification. */ - rpc SignMessage (SignMessageRequest) returns (SignMessageResponse); + rpc SignMessage (SignMessageRequest) returns (SignMessageResponse) { + option (google.api.http) = { + post: "/v1/signmessage" + body: "*" + }; + } /** lncli: `verifymessage` VerifyMessage verifies a signature over a msg. The signature must be @@ -268,7 +286,12 @@ service Lightning { channel database. In addition to returning the validity of the signature, VerifyMessage also returns the recovered pubkey from the signature. */ - rpc VerifyMessage (VerifyMessageRequest) returns (VerifyMessageResponse); + rpc VerifyMessage (VerifyMessageRequest) returns (VerifyMessageResponse) { + option (google.api.http) = { + post: "/v1/verifymessage" + body: "*" + }; + } /** lncli: `connect` ConnectPeer attempts to establish a connection to a remote peer. This is at @@ -336,6 +359,14 @@ service Lightning { }; } + /** lncli: `subscribechannelevents` + SubscribeChannelEvents creates a uni-directional stream from the server to + the client in which any updates relevant to the state of the channels are + sent over. Events include new active channels, inactive channels, and closed + channels. + */ + rpc SubscribeChannelEvents (ChannelEventSubscription) returns (stream ChannelEventUpdate); + /** lncli: `closedchannels` ClosedChannels returns a description of all the closed channels that this node was a participant in. @@ -455,10 +486,8 @@ service Lightning { paginated responses, allowing users to query for specific invoices through their add_index. This can be done by using either the first_index_offset or last_index_offset fields included in the response as the index_offset of the - next request. The reversed flag is set by default in order to paginate - backwards. If you wish to paginate forwards, you must explicitly set the - flag to false. If none of the parameters are specified, then the last 100 - invoices will be returned. + next request. By default, the first 100 invoices created will be returned. + Backwards pagination is also supported through the Reversed flag. */ rpc ListInvoices (ListInvoiceRequest) returns (ListInvoiceResponse) { option (google.api.http) = { @@ -647,6 +676,26 @@ service Lightning { }; } +message Utxo { + /// The type of address + AddressType type = 1 [json_name = "address_type"]; + + /// The address + string address = 2 [json_name = "address"]; + + /// The value of the unspent coin in satoshis + int64 amount_sat = 3 [json_name = "amount_sat"]; + + /// The pkscript in hex + string pk_script = 4 [json_name = "pk_script"]; + + /// The outpoint in format txid:n + OutPoint outpoint = 5 [json_name = "outpoint"]; + + /// The number of confirmations for the Utxo + int64 confirmations = 6 [json_name = "confirmations"]; +} + message Transaction { /// The transaction hash string tx_hash = 1 [ json_name = "tx_hash" ]; @@ -725,11 +774,18 @@ message SendRequest { send the payment. */ FeeLimit fee_limit = 8; + + /** + The channel id of the channel that must be taken to the first hop. If zero, + any channel may be used. + */ + uint64 outgoing_chan_id = 9; } message SendResponse { string payment_error = 1 [json_name = "payment_error"]; bytes payment_preimage = 2 [json_name = "payment_preimage"]; Route payment_route = 3 [json_name = "payment_route"]; + bytes payment_hash = 4 [json_name = "payment_hash"]; } message SendToRouteRequest { @@ -739,8 +795,16 @@ message SendToRouteRequest { /// An optional hex-encoded payment hash to be used for the HTLC. string payment_hash_string = 2; - /// The set of routes that should be used to attempt to complete the payment. - repeated Route routes = 3; + /** + Deprecated. The set of routes that should be used to attempt to complete the + payment. The possibility to pass in multiple routes is deprecated and + instead the single route field below should be used in combination with the + streaming variant of SendToRoute. + */ + repeated Route routes = 3 [deprecated = true]; + + /// Route that should be used to attempt to complete the payment. + Route route = 4; } message ChannelPoint { @@ -756,6 +820,17 @@ message ChannelPoint { uint32 output_index = 3 [json_name = "output_index"]; } +message OutPoint { + /// Raw bytes representing the transaction id. + bytes txid_bytes = 1 [json_name = "txid_bytes"]; + + /// Reversed, hex-encoded string representing the transaction id. + string txid_str = 2 [json_name = "txid_str"]; + + /// The index of the output on the transaction. + uint32 output_index = 3 [json_name = "output_index"]; +} + message LightningAddress { /// The identity pubkey of the Lightning node string pubkey = 1 [json_name = "pubkey"]; @@ -791,24 +866,45 @@ message SendCoinsRequest { /// A manual fee rate set in sat/byte that should be used when crafting the transaction. int64 sat_per_byte = 5; + + /** + If set, then the amount field will be ignored, and lnd will attempt to + send all the coins under control of the internal wallet to the specified + address. + */ + bool send_all = 6; } message SendCoinsResponse { /// The transaction ID of the transaction string txid = 1 [json_name = "txid"]; } +message ListUnspentRequest { + /// The minimum number of confirmations to be included. + int32 min_confs = 1; + + /// The maximum number of confirmations to be included. + int32 max_confs = 2; +} +message ListUnspentResponse { + /// A list of utxos + repeated Utxo utxos = 1 [json_name = "utxos"]; +} + /** `AddressType` has to be one of: - `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0) - `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1) */ -message NewAddressRequest { - enum AddressType { +enum AddressType { WITNESS_PUBKEY_HASH = 0; NESTED_PUBKEY_HASH = 1; - } + UNUSED_WITNESS_PUBKEY_HASH = 2; + UNUSED_NESTED_PUBKEY_HASH = 3; +} +message NewAddressRequest { /// The address type AddressType type = 1; } @@ -944,8 +1040,11 @@ message Channel { */ uint32 csv_delay = 16 [json_name = "csv_delay"]; - /// Whether this channel is advertised to the network or not + /// Whether this channel is advertised to the network or not. bool private = 17 [json_name = "private"]; + + /// True if we were the ones that created the channel. + bool initiator = 18 [json_name = "initiator"]; } @@ -1075,11 +1174,13 @@ message GetInfoResponse { /// Whether the wallet's view is synced to the main chain bool synced_to_chain = 9 [json_name = "synced_to_chain"]; - /// Whether the current node is connected to testnet - bool testnet = 10 [json_name = "testnet"]; + /** + Whether the current node is connected to testnet. This field is + deprecated and the network field should be used instead + **/ + bool testnet = 10 [json_name = "testnet", deprecated = true]; - /// A list of active chains the node is connected to - repeated string chains = 11 [json_name = "chains"]; + reserved 11; /// The URIs of the current node. repeated string uris = 12 [json_name = "uris"]; @@ -1092,6 +1193,17 @@ message GetInfoResponse { /// Number of inactive channels uint32 num_inactive_channels = 15 [json_name = "num_inactive_channels"]; + + /// A list of active chains the node is connected to + repeated Chain chains = 16 [json_name = "chains"]; +} + +message Chain { + /// The blockchain the node is on (eg bitcoin, litecoin) + string chain = 1 [json_name = "chain"]; + + /// The network the node is on (eg regtest, testnet, mainnet) + string network = 2 [json_name = "network"]; } message ConfirmationUpdate { @@ -1132,7 +1244,6 @@ message CloseChannelRequest { message CloseStatusUpdate { oneof update { PendingUpdate close_pending = 1 [json_name = "close_pending"]; - ConfirmationUpdate confirmation = 2 [json_name = "confirmation"]; ChannelCloseUpdate chan_close = 3 [json_name = "chan_close"]; } } @@ -1179,7 +1290,6 @@ message OpenChannelRequest { message OpenStatusUpdate { oneof update { PendingUpdate chan_pending = 1 [json_name = "chan_pending"]; - ConfirmationUpdate confirmation = 2 [json_name = "confirmation"]; ChannelOpenUpdate chan_open = 3 [json_name = "chan_open"]; } } @@ -1306,6 +1416,27 @@ message PendingChannelsResponse { repeated WaitingCloseChannel waiting_close_channels = 5 [ json_name = "waiting_close_channels" ]; } +message ChannelEventSubscription { +} + +message ChannelEventUpdate { + oneof channel { + Channel open_channel = 1 [ json_name = "open_channel" ]; + ChannelCloseSummary closed_channel = 2 [ json_name = "closed_channel" ]; + ChannelPoint active_channel = 3 [ json_name = "active_channel" ]; + ChannelPoint inactive_channel = 4 [ json_name = "inactive_channel" ]; + } + + enum UpdateType { + OPEN_CHANNEL = 0; + CLOSED_CHANNEL = 1; + ACTIVE_CHANNEL = 2; + INACTIVE_CHANNEL = 3; + } + + UpdateType type = 5 [ json_name = "type" ]; +} + message WalletBalanceRequest { } message WalletBalanceResponse { @@ -1468,6 +1599,7 @@ message RoutingPolicy { int64 fee_base_msat = 3 [json_name = "fee_base_msat"]; int64 fee_rate_milli_msat = 4 [json_name = "fee_rate_milli_msat"]; bool disabled = 5 [json_name = "disabled"]; + uint64 max_htlc_msat = 6 [json_name = "max_htlc_msat"]; } /** @@ -1626,8 +1758,10 @@ message Invoice { */ string memo = 1 [json_name = "memo"]; - /// An optional cryptographic receipt of payment - bytes receipt = 2 [json_name = "receipt"]; + /** Deprecated. An optional cryptographic receipt of payment which is not + implemented. + */ + bytes receipt = 2 [json_name = "receipt", deprecated = true]; /** The hex-encoded preimage (32 byte) which will allow settling an incoming @@ -1642,7 +1776,7 @@ message Invoice { int64 value = 5 [json_name = "value"]; /// Whether this invoice has been fulfilled - bool settled = 6 [json_name = "settled"]; + bool settled = 6 [json_name = "settled", deprecated = true]; /// When this invoice was created int64 creation_date = 7 [json_name = "creation_date"]; @@ -1720,7 +1854,19 @@ message Invoice { here as well. */ int64 amt_paid_msat = 20 [json_name = "amt_paid_msat"]; + + enum InvoiceState { + OPEN = 0; + SETTLED = 1; + CANCELED = 2; + } + + /** + The state the invoice is in. + */ + InvoiceState state = 21 [json_name = "state"]; } + message AddInvoiceResponse { bytes r_hash = 1 [json_name = "r_hash"]; @@ -1953,15 +2099,18 @@ message ForwardingEvent { /// The outgoing channel ID that carried the preimage that completed the circuit. uint64 chan_id_out = 4 [json_name = "chan_id_out"]; - /// The total amount of the incoming HTLC that created half the circuit. + /// The total amount (in satoshis) of the incoming HTLC that created half the circuit. uint64 amt_in = 5 [json_name = "amt_in"]; - /// The total amount of the outgoign HTLC that created the second half of the circuit. + /// The total amount (in satoshis) of the outgoing HTLC that created the second half of the circuit. uint64 amt_out = 6 [json_name = "amt_out"]; - /// The total fee that this payment circuit carried. + /// The total fee (in satoshis) that this payment circuit carried. uint64 fee = 7 [json_name = "fee"]; + /// The total fee (in milli-satoshis) that this payment circuit carried. + uint64 fee_msat = 8 [json_name = "fee_msat"]; + // TODO(roasbeef): add settlement latency? // * use FPE on the chan id? // * also list failures? diff --git a/scripts/process-locked-payments.js b/scripts/process-locked-payments.js new file mode 100644 index 0000000..757837a --- /dev/null +++ b/scripts/process-locked-payments.js @@ -0,0 +1,58 @@ +import { User, Lock, Paym } from '../class/'; +const config = require('../config'); + +var Redis = require('ioredis'); +var redis = new Redis(config.redis); + +let bitcoinclient = require('../bitcoin'); +let lightning = require('../lightning'); + +(async () => { + let keys = await redis.keys('locked_payments_for_*'); + keys = User._shuffle(keys); + + for (let key of keys) { + const userid = key.replace('locked_payments_for_', ''); + console.log('==================================================================================='); + console.log('userid=', userid); + let user = new User(redis, bitcoinclient, lightning); + user._userid = userid; + let lockedPayments = await user.getLockedPayments(); + + for (let lockedPayment of lockedPayments) { + console.log('processing lockedPayment=', lockedPayment); + + let payment = new Paym(redis, bitcoinclient, lightning); + payment.setInvoice(lockedPayment.pay_req); + if (await payment.isExpired()) { + let sendResult; + try { + sendResult = await payment.attemptPayToRoute(); + } catch (_) { + console.log(_); + await user.unlockFunds(lockedPayment.pay_req); + continue; + } + console.log('sendResult=', sendResult); + console.log('payment.getIsPaid() = ', payment.getIsPaid()); + if (payment.getIsPaid() === true) { + console.log('paid successfully'); + sendResult = payment.processSendPaymentResponse(sendResult); // adds fees + console.log('sendResult=', sendResult); + await user.savePaidLndInvoice(sendResult); + await user.unlockFunds(lockedPayment.pay_req); + } else if (payment.getIsPaid() === false) { + console.log('not paid, just evict the lock'); + await user.unlockFunds(lockedPayment.pay_req); + } else { + console.log('payment is in unknown state'); + } + console.log('sleeping 5 sec...'); + console.log('-----------------------------------------------------------------------------------'); + await User._sleep(5); + } + } + } + console.log('done'); + process.exit(); +})();