ADD: unlock stuck payments script

This commit is contained in:
Overtorment 2019-03-10 23:32:34 +00:00
parent 1ad26e21b8
commit c1ac67a853
6 changed files with 358 additions and 28 deletions

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -1,2 +1,3 @@
export * from './User';
export * from './Lock';
export * from './Paym';

View File

@ -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;

197
rpc.proto
View File

@ -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?

View File

@ -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();
})();