public async Task <ActionResult> ValidatePaymentRequest(string request) { var userAppId = User.Identity.GetUserId(); if (userAppId == null) { Response.StatusCode = (int)HttpStatusCode.Unauthorized; return(Json(new { success = false, message = "User not authorized." })); } using (var db = new ZapContext()) { var invoice = request.SanitizeXSS(); LNTransaction t; try { // Check if the request has previously been submitted t = await db.LightningTransactions .Where(tx => tx.PaymentRequest == invoice) .SingleOrDefaultAsync().ConfigureAwait(true); } catch (InvalidOperationException) { // source has more than one element. return(Json(new { success = false, message = "Duplicate invoice - please use a new invoice." })); } if (t == null) { // first time // Get interface to LND LndRpcClient lndClient = GetLndClient(); // Decode invoice var decoded = lndClient.DecodePayment(invoice); if (decoded != null) { double amount = Convert.ToDouble(decoded.num_satoshis, CultureInfo.InvariantCulture); if (amount < 1) { return(Json(new { success = false, message = "Zero- or any-value invoices are not supported at this time" })); } if (amount > 50000) { return(Json(new { success = false, message = "Withdraws temporarily limited to 50000 Satoshi" })); } // Check user balance var userFunds = await db.Users .Where(u => u.AppId == userAppId) .Select(u => new { u.Funds.Balance }) .FirstOrDefaultAsync().ConfigureAwait(true); if (userFunds == null) { Response.StatusCode = (int)HttpStatusCode.InternalServerError; return(Json(new { success = false, message = "User not found in database." })); } if (userFunds.Balance < amount) { return(Json(new { success = false, message = "Insufficient Funds. You have " + userFunds.Balance.ToString("0.", CultureInfo.InvariantCulture) + ", invoice is for " + decoded.num_satoshis + "." })); } // This is less than ideal for time checks... // Check how much user withdrew previous 24 hours var DayAgo = DateTime.UtcNow - TimeSpan.FromDays(1); var txs = await db.LightningTransactions .Where(tx => tx.User.AppId == userAppId) .Where(tx => tx.TimestampCreated != null && tx.TimestampCreated > DayAgo) .Where(tx => !tx.IsDeposit) .Select(tx => tx.Amount).ToListAsync().ConfigureAwait(true); var withdrawn24h = txs.Sum(); if (withdrawn24h > 100000) { return(Json(new { success = false, message = "Withdraws limited to 100,000 Satoshi within a 24 hour limit." })); } var HourAgo = DateTime.UtcNow - TimeSpan.FromHours(1); var txs1h = await db.LightningTransactions .Where(tx => tx.User.AppId == userAppId) .Where(tx => tx.TimestampCreated != null && tx.TimestampCreated > HourAgo) .Where(tx => !tx.IsDeposit) .Select(tx => tx.Amount).ToListAsync().ConfigureAwait(true); var withdrawn1h = txs1h.Sum(); if (withdrawn1h > 50000) { return(Json(new { success = false, message = "Withdraws limited to 50,000 Satoshi within a 1 hour limit." })); } // Save the invoice to database var user = await db.Users .Where(u => u.AppId == userAppId) .FirstAsync().ConfigureAwait(true); //create a new transaction record in database t = new LNTransaction() { IsSettled = false, Memo = (decoded.description ?? "Withdraw").SanitizeXSS(), HashStr = decoded.payment_hash, PaymentHash = decoded.payment_hash, Amount = Convert.ToInt64(decoded.num_satoshis, CultureInfo.InvariantCulture), IsDeposit = false, TimestampSettled = null, TimestampCreated = DateTime.UtcNow, //can't know PaymentRequest = invoice, FeePaid_Satoshi = 0, NodePubKey = decoded.destination, User = user, WithdrawId = Guid.NewGuid(), }; db.LightningTransactions.Add(t); await db.SaveChangesAsync().ConfigureAwait(true); return(Json(new { success = true, withdrawId = t.WithdrawId, decoded.num_satoshis, decoded.destination, })); } } else { // re-submitted - don't create new DB entry // Safety checks if (t.IsSettled || t.IsIgnored || t.IsLimbo || t.IsDeposit || t.IsError) { Response.StatusCode = (int)HttpStatusCode.BadRequest; return(Json(new { success = false, message = "Invalid withdraw request." })); } // Check balance now var userFunds = await db.Users .Where(u => u.AppId == userAppId) .Select(u => new { u.Funds.Balance }) .FirstOrDefaultAsync().ConfigureAwait(true); if (userFunds == null) { Response.StatusCode = (int)HttpStatusCode.InternalServerError; return(Json(new { success = false, message = "User not found in database." })); } double amount = Convert.ToDouble(t.Amount, CultureInfo.InvariantCulture); if (userFunds.Balance < amount) { return(Json(new { success = false, message = "Insufficient Funds. You have " + userFunds.Balance.ToString("0.", CultureInfo.InvariantCulture) + ", invoice is for " + t.Amount + "." })); } return(Json(new { success = true, withdrawId = t.WithdrawId,//t.Id, num_satoshis = t.Amount, destination = t.NodePubKey, })); } } return(Json(new { success = false, message = "Error decoding invoice." })); }
// 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); } }
public ActionResult GetDepositInvoice(string amount, string memo, string anon, string use, int?useId, int?useAction) { Response.AddHeader("X-Frame-Options", "DENY"); bool isAnon = !(anon == null || anon != "1"); if (!isAnon && !User.Identity.IsAuthenticated) { // This is a user-related invoice, and no user is logged in. return(RedirectToAction("Login", "Account", new { returnUrl = Request.Url.ToString() })); } string userId; if (isAnon) { userId = null; } else { userId = User.Identity.GetUserId(); } if (string.IsNullOrEmpty(memo)) { memo = "Zapread.com"; } if (Convert.ToInt64(amount, CultureInfo.InvariantCulture) > 50000) { return(Json(new { success = false, message = "Deposits temporarily limited to 50000 satoshi" })); } LndRpcClient lndClient = GetLndClient(); var inv = lndClient.AddInvoice(Convert.ToInt64(amount, CultureInfo.InvariantCulture), memo: memo.SanitizeXSS(), expiry: "3600"); LnRequestInvoiceResponse resp = new LnRequestInvoiceResponse() { Invoice = inv.payment_request, Result = "success", success = true, }; //Create transaction record (not settled) using (ZapContext db = new ZapContext()) { // TODO: ensure user exists? User user = null; if (userId != null) { user = db.Users.Where(u => u.AppId == userId).First(); } TransactionUse usedFor = TransactionUse.Undefined; TransactionUseAction usedForAction = TransactionUseAction.Undefined; int usedForId = useId != null ? useId.Value : -1; if (use == "tip") { usedFor = TransactionUse.Tip; } else if (use == "votePost") { usedFor = TransactionUse.VotePost; } else if (use == "voteComment") { usedFor = TransactionUse.VoteComment; } else if (use == "userDeposit") { usedFor = TransactionUse.UserDeposit; usedForId = userId != null ? user.Id : -1; } if (useAction != null) { if (useAction.Value == 0) { usedForAction = TransactionUseAction.VoteDown; } else if (useAction.Value == 1) { usedForAction = TransactionUseAction.VoteUp; } } var rhash_bytes = Convert.FromBase64String(inv.r_hash); var rhash_hex = HexEncoder.EncodeData(rhash_bytes); //create a new transaction record in database LNTransaction t = new LNTransaction() { User = user, IsSettled = false, IsSpent = false, Memo = memo.SanitizeXSS(), Amount = Convert.ToInt64(amount, CultureInfo.InvariantCulture), HashStr = inv.r_hash, // B64 encoded PreimageHash = rhash_hex, IsDeposit = true, TimestampCreated = DateTime.Now, PaymentRequest = inv.payment_request, UsedFor = usedFor, UsedForId = usedForId, UsedForAction = usedForAction, }; db.LightningTransactions.Add(t); db.SaveChanges(); resp.Id = t.Id; } if (true) // debugging { // If a listener is not already running, this should start // Check if there is one already online. var numListeners = lndTransactionListeners.Count(kvp => kvp.Value.IsLive); // If we don't have one running - start it and subscribe if (numListeners < 1) { var listener = lndClient.GetListener(); lndTransactionListeners.TryAdd(listener.ListenerId, listener); // keep alive while we wait for payment listener.InvoicePaid += async(invoice) => await NotifyClientsInvoicePaid(invoice) .ConfigureAwait(true); // handle payment message listener.StreamLost += OnListenerLost; // stream lost var a = new Task(() => listener.Start()); // listen for payment a.Start(); } } return(Json(resp)); }
/// <summary> /// /// </summary> /// <param name="request"></param> /// <param name="userId"></param> /// <param name="lndClient"></param> /// <returns></returns> public object TryWithdrawal(string request, string userId, string ip, LndRpcClient lndClient) { // Check if payment request is ok // Check if already paid var decoded = lndClient.DecodePayment(request); if (decoded == null) { return(new { Result = "Error decoding invoice." }); } if (decoded.destination == null) { return(new { Result = "Error decoding invoice." }); } using (var db = new ZapContext()) { var user = db.Users .Include(usr => usr.Funds) .FirstOrDefault(u => u.AppId == userId); if (user == null) { MailingService.Send(new UserEmailModel() { Destination = "*****@*****.**", Body = " Withdraw from user which doesn't exist.", Email = "", Name = "zapread.com Monitoring", Subject = "User withdraw error", }); // Don't reveal information that user doesn't exist return(new { Result = "Error processing request." }); } if (user.Funds.Balance < Convert.ToDouble(decoded.num_satoshis)) { return(new { Result = "Insufficient Funds. You have " + user.Funds.Balance.ToString("0.") + ", invoice is for " + decoded.num_satoshis + "." }); } //insert transaction as pending LNTransaction t = new LNTransaction() { IsSettled = false, Memo = decoded.description ?? "Withdraw", HashStr = decoded.payment_hash, Amount = Convert.ToInt64(decoded.num_satoshis), IsDeposit = false, TimestampSettled = DateTime.UtcNow, TimestampCreated = DateTime.UtcNow, //can't know PaymentRequest = request, FeePaid_Satoshi = 0, NodePubKey = decoded.destination, User = user, }; db.LightningTransactions.Add(t); db.SaveChanges(); SendPaymentResponse paymentresult; //all (should be) ok - make the payment if (WithdrawRequests.TryAdd(request, DateTime.UtcNow)) { paymentresult = lndClient.PayInvoice(request); } else { //double request! return(new { Result = "Please click only once. Payment already in processing." }); } WithdrawRequests.TryRemove(request, out DateTime reqInitTime); if (paymentresult == null) { t.ErrorMessage = "Error executing payment."; db.SaveChanges(); return(new { Result = "Error executing payment." }); } if (paymentresult.error != null && paymentresult.error != "") { t.ErrorMessage = "Error: " + paymentresult.error; db.SaveChanges(); return(new { Result = "Error: " + paymentresult.error }); } if (paymentresult.payment_error != null) { t.ErrorMessage = "Error: " + paymentresult.payment_error; db.SaveChanges(); return(new { Result = "Error: " + paymentresult.payment_error }); } // should this be done here? Is there an async/sync check that payment was sent successfully? user.Funds.Balance -= Convert.ToDouble(decoded.num_satoshis); db.SaveChanges(); //update transaction status t.IsSettled = true; t.FeePaid_Satoshi = (paymentresult.payment_route.total_fees == null ? 0 : Convert.ToInt64(paymentresult.payment_route.total_fees)); //db.LightningTransactions.Add(t); db.SaveChanges(); return(new { Result = "success", Fees = 0 }); } }
public ActionResult GetDepositInvoice(string amount, string memo, string anon) { bool isAnon = !(anon == null || anon != "1"); if (!isAnon && !User.Identity.IsAuthenticated) { // This is a user-related invoice, and no user is logged in. return(RedirectToAction("Login", "Account", new { returnUrl = Request.Url.ToString() })); } string userId; if (isAnon) { userId = null; } else { userId = User.Identity.GetUserId(); } if (memo == null || memo == "") { memo = "Zapread.com"; } var lndClient = new LndRpcClient( host: System.Configuration.ConfigurationManager.AppSettings["LnMainnetHost"], macaroonAdmin: System.Configuration.ConfigurationManager.AppSettings["LnMainnetMacaroonAdmin"], macaroonRead: System.Configuration.ConfigurationManager.AppSettings["LnMainnetMacaroonRead"], macaroonInvoice: System.Configuration.ConfigurationManager.AppSettings["LnMainnetMacaroonInvoice"]); var inv = lndClient.AddInvoice(Convert.ToInt64(amount), memo: memo, expiry: "432000"); LnRequestInvoiceResponse resp = new LnRequestInvoiceResponse() { Invoice = inv.payment_request, Result = "success", }; //Create transaction record (not settled) using (ZapContext db = new ZapContext()) { // TODO: ensure user exists? zapread.com.Models.User user = null; if (userId != null) { user = db.Users.Where(u => u.AppId == userId).First(); } //create a new transaction LNTransaction t = new LNTransaction() { User = user, IsSettled = false, IsSpent = false, Memo = memo, Amount = Convert.ToInt64(amount), HashStr = inv.r_hash, IsDeposit = true, TimestampCreated = DateTime.Now, PaymentRequest = inv.payment_request, UsedFor = TransactionUse.UserDeposit, UsedForId = userId != null ? user.Id : -1, }; db.LightningTransactions.Add(t); db.SaveChanges(); } // If a listener is not already running, this should start // Check if there is one already online. var numListeners = lndTransactionListeners.Count(kvp => kvp.Value.IsLive); // If we don't have one running - start it and subscribe if (numListeners < 1) { var listener = lndClient.GetListener(); lndTransactionListeners.TryAdd(listener.ListenerId, listener); // keep alive while we wait for payment listener.InvoicePaid += NotifyClientsInvoicePaid; // handle payment message listener.StreamLost += OnListenerLost; // stream lost var a = new Task(() => listener.Start()); // listen for payment a.Start(); } return(Json(resp)); }
// We have received asynchronous notification that a lightning invoice has been paid private static void 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.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; } var context = GlobalHost.ConnectionManager.GetHubContext <NotificationHub>(); // 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) { // 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)); t.IsSettled = true; } 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)); t = new LNTransaction() { IsSettled = invoice.settled.Value, Memo = invoice.memo, Amount = Convert.ToInt64(invoice.value), HashStr = invoice.r_hash, IsDeposit = true, TimestampSettled = settletime, TimestampCreated = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.creation_date)), PaymentRequest = invoice.payment_request, User = null, }; db.LightningTransactions.Add(t); } var user = t.User; double userBalance = 0.0; if (user == 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. user.Funds.Balance += Convert.ToInt64(invoice.value); userBalance = Math.Floor(user.Funds.Balance); } t.IsSettled = invoice.settled.Value; db.SaveChanges(); // Send live signal to listening clients on websockets/SignalR context.Clients.All.NotifyInvoicePaid(new { invoice = invoice.payment_request, balance = userBalance, txid = t.Id }); } }