private static Invoice FetchInvoiceFromNode(string invoice) { LndRpcClient lndClient = GetLndClient(); LightningLib.DataEncoders.HexEncoder h = new LightningLib.DataEncoders.HexEncoder(); // Decode the payment request var decoded = lndClient.DecodePayment(invoice); // Get the hash var hash = decoded.payment_hash; // GetInvoice expects the hash in base64 encoded format var hash_bytes = h.DecodeData(hash); var hash_b64 = Convert.ToBase64String(hash_bytes); var inv = lndClient.GetInvoice(hash_b64); return(inv); }
/// <summary> /// Constructor with dependency injection for IOC and controller singleton control. /// </summary> /// <param name="paymentsService"></param> public LightningController(ILightningPayments paymentsService) { this.PaymentsService = paymentsService; this.HexEncoder = new LightningLib.DataEncoders.HexEncoder(); }
// We have received asynchronous notification that a lightning invoice has been paid private async static Task NotifyClientsInvoicePaid(Invoice invoice) { // Check if the invoice received was paid. LND also sends updates // for new invoices to the invoice stream. We want to listen for settled invoices here. if (!invoice.settled.HasValue) { // Bad invoice // Todo - logging return; } if (!invoice.settled.Value) { // Optional - add some logic to check invoices on the stream. These invoices // which are not settled are likely new deposit requests. For the purposes of // this function, we only care about settled invoices. return; } // This is the amount which was paid - needed in case of 0 (any) value invoices var amount = Convert.ToInt64(invoice.amt_paid_sat, CultureInfo.InvariantCulture); // Update LN transaction status in db using (ZapContext db = new ZapContext()) { // Check if unsettled transaction exists in db matching the invoice that was just settled. var tx = db.LightningTransactions .Include(tr => tr.User) .Where(tr => tr.PaymentRequest == invoice.payment_request) .ToList(); DateTime settletime = DateTime.UtcNow; LNTransaction t; if (tx.Count > 0) // Shouldn't ever be more than one entry - could add a check for this. { // We found it - mark it as settled. t = tx.First(); t.TimestampSettled = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date, CultureInfo.InvariantCulture)); t.IsSettled = true; if (t.Amount != amount) { if (t.Amount == 0) { // This was a zero-invoice t.Amount = amount; // this will be saved to DB } } } else { // This invoice is not in the db - it may not be related to this service. // We still record it in our database for any possible user forensics/history later. settletime = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date, CultureInfo.InvariantCulture)); var HexEncoder = new LightningLib.DataEncoders.HexEncoder(); // static method var rhash_bytes = Convert.FromBase64String(invoice.r_hash); var rhash_hex = HexEncoder.EncodeData(rhash_bytes); t = new LNTransaction() { IsSettled = invoice.settled.Value, Memo = invoice.memo.SanitizeXSS(), Amount = amount,//Convert.ToInt64(invoice.value, CultureInfo.InvariantCulture), HashStr = invoice.r_hash, PreimageHash = rhash_hex, IsDeposit = true, TimestampSettled = settletime, TimestampCreated = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.creation_date, CultureInfo.InvariantCulture)), PaymentRequest = invoice.payment_request, User = null, }; db.LightningTransactions.Add(t); } await db.SaveChangesAsync().ConfigureAwait(true); // Financial transaction double userBalance = 0.0; // This value will be returned later if (t.User != null) // the user could be null if it is an anonymous payment. { var userFunds = await db.Users .Where(u => u.Id == t.User.Id) .Select(u => u.Funds) .FirstOrDefaultAsync().ConfigureAwait(true); // Make every attempt to save user balance in DB int attempts = 0; bool saveFailed; bool saveAborted = false; do { attempts++; saveFailed = false; if (attempts < 50) { // This really shouldn't happen! if (userFunds == null) { // this should not happen? - verify. Maybe this is the case for transactions related to votes? // throw new Exception("Error accessing user information related to settled LN Transaction."); } else { // Update user balance - this is a deposit. userFunds.Balance += amount;// Convert.ToInt64(invoice.value, CultureInfo.InvariantCulture); userBalance = Math.Floor(userFunds.Balance); } try { db.SaveChanges(); // synchronous } catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException ex) { saveFailed = true; var entry = ex.Entries.Single(); entry.Reload(); } } else { saveAborted = true; } }while (saveFailed); // Don't record as settled if save was aborted due to DB concurrency failure. // LND database will show it was settled, but DB not. // Another process can check DB sync and correct later. if (saveAborted == false) { t.IsSettled = invoice.settled.Value; await db.SaveChangesAsync().ConfigureAwait(true); } } else { t.IsSettled = invoice.settled.Value; await db.SaveChangesAsync().ConfigureAwait(true); } // Send live signal to listening clients on websockets/SignalR //var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>(); //context.Clients.All.NotifyInvoicePaid(new { invoice = invoice.payment_request, balance = userBalance, txid = t.Id }); await NotificationService.SendPaymentNotification( t.User == null? "" : t.User.AppId, invoice : invoice.payment_request, userBalance : userBalance, txid : t.Id).ConfigureAwait(true); } }
/// <summary> /// /// </summary> /// <param name="request"></param> /// <param name="userAppId"></param> /// <param name="lndClient"></param> /// <param name="ip"></param> /// <returns></returns> public object TryWithdrawal(Models.LNTransaction request, string userAppId, string ip, LndRpcClient lndClient) { if (request == null) { return(new { success = false, message = "Internal error." }); } if (lndClient == null) { return(HandleLndClientIsNull()); } long FeePaid_Satoshi; // This is used later if the invoice succeeds. User user = null; SendPaymentResponse paymentresult = null; using (var db = new ZapContext()) { // Check when user has made last LN transaction var lasttx = db.LightningTransactions .Where(tx => tx.User.AppId == userAppId) // This user .Where(tx => tx.Id != request.Id) // Not the one being processed now .OrderByDescending(tx => tx.TimestampCreated) // Most recent .AsNoTracking() .FirstOrDefault(); if (lasttx != null && (DateTime.UtcNow - lasttx.TimestampCreated < TimeSpan.FromMinutes(5))) { return(new { success = false, message = "Please wait 5 minutes between Lightning transaction requests." }); } // Check if user has sufficient balance var userFunds = db.Users .Where(u => u.AppId == userAppId) .Select(usr => usr.Funds) .FirstOrDefault(); if (userFunds == null) { return(HandleUserIsNull()); } if (userFunds.IsWithdrawLocked) { return(new { success = false, message = "User withdraw is locked. Please contact an administrator." }); } string responseStr = ""; //all (should be) ok - make the payment if (WithdrawRequests.TryAdd(request.PaymentRequest, DateTime.UtcNow)) // This is to prevent flood attacks { // Check if user has sufficient balance if (userFunds.Balance < Convert.ToDouble(request.Amount, CultureInfo.InvariantCulture)) { return(new { success = false, message = "Insufficient Funds. You have " + userFunds.Balance.ToString("0.", CultureInfo.CurrentCulture) + ", invoice is for " + request.Amount.ToString(CultureInfo.CurrentCulture) + "." }); } // Mark funds for withdraw as "in limbo" - will be resolved if verified as paid. userFunds.LimboBalance += Convert.ToDouble(request.Amount, CultureInfo.InvariantCulture); userFunds.Balance -= Convert.ToDouble(request.Amount, CultureInfo.InvariantCulture); // Funds are checked for optimistic concurrency here. If the Balance has been updated, // we shouldn't proceed with the withdraw, so we will abort it. try { db.SaveChanges(); // Synchronous to ensure balance is locked. } catch (DbUpdateConcurrencyException) { // The balance has changed - don't do withdraw. // This may trigger if the user also gets funds added - such as a tip. // For now, we will fail the withdraw under any condition. // In the future, we may consider ignoring changes increasing balance. // Remove this request from the lock so the user can retry. WithdrawRequests.TryRemove(request.PaymentRequest, out DateTime reqInitTimeReset); return(new { success = false, message = "Failed. User balances changed during withdraw." }); } // Get an update-able entity for the transaction from the DB var t = db.LightningTransactions .Where(tx => tx.Id == request.Id) .Where(tx => tx.User.AppId == userAppId) .FirstOrDefault(); if (t == null) { return(new { success = false, message = "Validated invoice not found in database." }); } t.IsLimbo = true; // Mark the transaction as in limbo as we try to pay it // Save the transaction state try { db.SaveChanges(); } catch (DbUpdateConcurrencyException) { // Remove this request from the lock so the user can retry. WithdrawRequests.TryRemove(request.PaymentRequest, out DateTime reqInitTimeReset); return(new { success = false, message = "Failed. Validated invoice modified during transaction." }); } // Execute payment try { paymentresult = lndClient.PayInvoice(request.PaymentRequest, out responseStr); } catch (RestException e) { user = db.Users .Where(u => u.AppId == userAppId) .FirstOrDefault(); t.IsError = true; t.ErrorMessage = "REST exception executing payment"; // Save the transaction state try { db.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { return(HandleClientRestException(userAppId, request.Id, "DB Cuncurrency exception: " + ex.Message + " REST Exception: " + responseStr, e)); } // A RestException happens when there was an error with the LN node. // At this point, the funds will remain in limbo until it is verified as paid by the // periodic LNTransactionMonitor service. return(HandleClientRestException(userAppId, request.Id, responseStr, e)); } } else { //double request! return(new { success = false, message = "Please click only once. Payment already in processing." }); } // If we are at this point, we are now checking the status of the payment. if (paymentresult == null) { // Something went wrong. Check if the payment went through var payments = lndClient.GetPayments(include_incomplete: true); var pmt = payments.payments .Where(p => p.payment_hash == request.HashStr) .FirstOrDefault(); MailingService.Send(new UserEmailModel() { Destination = System.Configuration.ConfigurationManager.AppSettings["ExceptionReportEmail"], Body = " Withdraw error: PayInvoice returned null result. \r\n hash: " + request.HashStr + "\r\n recovered by getpayments: " + (pmt != null ? "true" : "false") + "\r\n invoice: " + request + "\r\n user: "******"", Name = "zapread.com Exception", Subject = "User withdraw error 3", }); if (pmt != null && pmt.status == "SUCCEEDED") { // Looks like the payment may have gone through. // the payment went through process withdrawal paymentresult = new SendPaymentResponse() { payment_route = new PaymentRoute() { total_fees = "0", } }; MailingService.Send(new UserEmailModel() { Destination = System.Configuration.ConfigurationManager.AppSettings["ExceptionReportEmail"], Body = " Withdraw error: payment error " + "\r\n user: "******"\r\n username: "******"\r\n <br><br> response: " + responseStr, Email = "", Name = "zapread.com Exception", Subject = "User withdraw possible error 6", }); } else { // Not recovered - it will be cued for checkup later. This could be caused by LND being "laggy" // Reserve the user funds to prevent another withdraw //user.Funds.LimboBalance += Convert.ToDouble(decoded.num_satoshis); //user.Funds.Balance -= Convert.ToDouble(decoded.num_satoshis); return(HandlePaymentRecoveryFailed(userAppId, request.Id)); } } // This shouldn't ever be hit - this response is obsolete. // TODO watch for this error, and if not found by June 2020 - delete this code if (paymentresult.error != null && paymentresult.error != "") { return(HandleLegacyPaymentRecoveryFailed(userAppId, request.Id, paymentresult, responseStr)); } // The LND node returned an error if (!String.IsNullOrEmpty(paymentresult.payment_error))//paymentresult.payment_error != null && paymentresult.payment_error != "") { // Funds will remain in Limbo until failure verified by LNTransactionMonitor // TODO: verify trust in this method - funds could be returned to user here. return(HandleLNPaymentError(userAppId, request.Id, paymentresult, responseStr)); } FeePaid_Satoshi = (paymentresult.payment_route.total_fees == null ? 0 : Convert.ToInt64(paymentresult.payment_route.total_fees, CultureInfo.InvariantCulture)); // Unblock this request since it was successful WithdrawRequests.TryRemove(request.PaymentRequest, out DateTime reqInitTime); } // We're going to start a new context as we are updating the Limbo Balance using (var db = new ZapContext()) { user = db.Users .Where(u => u.AppId == userAppId) .FirstOrDefault(); // Get an update-able entity for the transaction from the DB var t = db.LightningTransactions .Where(tx => tx.Id == request.Id) .Where(tx => tx.User.AppId == userAppId) .FirstOrDefault(); // We have already subtracted the balance from the user account, since the payment was // successful, we leave it subtracted from the account and we remove the balance from limbo. bool saveFailed; int attempts = 0; do { attempts++; if (attempts > 50) { // We REALLY should never get to this point. If we're here, there is some strange // deadlock, or the user is being abusive and the funds will stay in Limbo. } saveFailed = false; user.Funds.LimboBalance -= Convert.ToDouble(request.Amount, CultureInfo.InvariantCulture); //update transaction status in DB // payment hash and preimage are B64 encoded. Here we convert to hex strings var HexEncoder = new LightningLib.DataEncoders.HexEncoder(); // static method string payment_hash_str = null; // default string payment_preimage_str = null; // default if (paymentresult != null && paymentresult.payment_hash != null) { payment_hash_str = HexEncoder.EncodeData(Convert.FromBase64String(paymentresult.payment_hash)); } if (paymentresult != null && paymentresult.payment_preimage != null) { payment_preimage_str = HexEncoder.EncodeData(Convert.FromBase64String(paymentresult.payment_preimage)); } t.IsSettled = true; t.IsLimbo = false; t.PaymentHash = payment_hash_str; t.PaymentPreimage = payment_preimage_str; // Important: this is proof that we paid it try { t.FeePaid_Satoshi = FeePaid_Satoshi;// (paymentresult.payment_route.total_fees == null ? 0 : Convert.ToInt64(paymentresult.payment_route.total_fees, CultureInfo.InvariantCulture)); } catch { t.FeePaid_Satoshi = 0; } try { db.SaveChanges(); } catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException ex) { saveFailed = true; foreach (var entry in ex.Entries)//.Single(); { entry.Reload(); } } }while (saveFailed); return(new { success = true, message = "success", Fees = 0, userBalance = user.Funds.Balance }); } }