public ActionResult SubmitPaymentRequest(string request) { var userId = User.Identity.GetUserId(); if (userId == null) { return(RedirectToAction("Login", "Account", new { returnUrl = Request.Url.ToString() })); } 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"]); string ip = GetClientIpAddress(Request); try { var paymentResult = paymentsService.TryWithdrawal(request, userId, ip, lndClient); return(Json(paymentResult)); } catch (Exception e) { MailingService.Send(new UserEmailModel() { Destination = "*****@*****.**", Body = " Exception: " + e.Message + "\r\n Stack: " + e.StackTrace + "\r\n invoice: " + request + "\r\n user: "******"", Name = "zapread.com Exception", Subject = "User withdraw error", }); return(Json(new { Result = "Error processing request." })); } }
private static List <LnChannelConnectionPoints> GetChanHist(LndRpcClient lndClient, CoinpanicContext db, Channel c) { List <LnChannelConnectionPoints> chanHist; Int64 otherchanId = Convert.ToInt64(c.chan_id); var ch = db.LnChannelHistory.Where(h => h.ChanId == otherchanId); if (ch.Count() > 0) { // already known - check status chanHist = ch.OrderByDescending(h => h.Timestamp).AsNoTracking().ToList(); } else { LnNode remoteNode = GetOrCreateNode(lndClient, c.remote_pubkey, db); // new channel history LnChannelConnectionPoints newChanHist = new LnChannelConnectionPoints() { IsConnected = c.active, LocalBalance = Convert.ToInt64(c.local_balance), RemoteBalance = Convert.ToInt64(c.remote_balance), Timestamp = DateTime.UtcNow, RemoteNode = remoteNode, ChanId = Convert.ToInt64(c.chan_id), }; db.LnChannelHistory.Add(newChanHist); db.SaveChanges(); chanHist = new List <LnChannelConnectionPoints>() { newChanHist }; } return(chanHist); }
private static LnNode GetOrCreateNode(LndRpcClient lndClient, string pubkey, CoinpanicContext db) { var nodeFind = db.LnNodes.Where(n => n.PubKey == pubkey).Include("Channels"); LnNode theNode; if (nodeFind.Count() < 1) { // no record yet of node! var nodeInfo = lndClient.GetNodeInfo(pubkey); if (nodeInfo.node == null) { return(null); } theNode = new LnNode() { Alias = nodeInfo.node.alias, Color = nodeInfo.node.color, last_update = nodeInfo.node.last_update, PubKey = nodeInfo.node.pub_key, }; theNode.Channels = new HashSet <LnChannel>(); db.LnNodes.Add(theNode); db.SaveChanges(); } else { theNode = nodeFind.First(); } return(theNode); }
/// <summary> /// /// </summary> /// <returns></returns> public ActionResult NodeSummary() { try { if (DateTime.Now - LastNodeSummaryUpdate > FwdingCacheTimeout) { bool useTestnet = GetUseTestnet(); LndRpcClient lndClient = GetLndClient(useTestnet); var xfers = lndClient.GetForwardingEvents(); //Total amount transferred nodeSummaryViewModel.TotalValueXfer = xfers.forwarding_events == null ? 0 : Convert.ToDouble(xfers.forwarding_events.Sum(f => Convert.ToInt64(f.amt_out))) / 100000000.0; nodeSummaryViewModel.NumXfer = xfers.forwarding_events == null ? 0 : xfers.forwarding_events.Count; nodeSummaryViewModel.TotalFees = xfers.forwarding_events == null ? 0.ToString("0.00000000") : (Convert.ToDouble(xfers.forwarding_events.Sum(f => Convert.ToInt64(f.fee))) / 100000000.0).ToString("0.00000000"); LastNodeSummaryUpdate = DateTime.Now; } } catch (Exception e) { ViewBag.TotalValueXfer = "Unknown"; ViewBag.NumXfer = "Unknown"; ViewBag.TotalFees = "Unknown"; MonitoringService.SendMessage("Lightning Error", " Exception: " + e.Message + "\r\n Stack: " + e.StackTrace); } return(PartialView("NodeSummary", nodeSummaryViewModel)); }
private static LndRpcClient GetLndClient() { usingTestnet = GetUseTestnet(); var lndClient = new LndRpcClient( host: System.Configuration.ConfigurationManager.AppSettings[usingTestnet ? "LnTestnetHost" : "LnMainnetHost"], macaroonAdmin: System.Configuration.ConfigurationManager.AppSettings[usingTestnet ? "LnTestnetMacaroonAdmin" : "LnMainnetMacaroonAdmin"], macaroonRead: System.Configuration.ConfigurationManager.AppSettings[usingTestnet ? "LnTestnetMacaroonRead" : "LnMainnetMacaroonRead"], macaroonInvoice: System.Configuration.ConfigurationManager.AppSettings[usingTestnet ? "LnMainnetMacaroonInvoice" : "LnMainnetMacaroonInvoice"]); return(lndClient); }
/// <summary> /// /// </summary> /// <returns></returns> public ActionResult NodeURI() { // Check if cache expired if (DateTime.Now - LastNodeURIUpdate > URICacheTimeout) { // Update cache Guid taskid = Guid.NewGuid(); UpdateTask updateTask = new UpdateTask() { id = taskid, task = new Task(() => { try { bool useTestnet = GetUseTestnet(); LndRpcClient lndClient = GetLndClient(useTestnet); var info = lndClient.GetInfo(); if (info == null) { } else { } nodeSummaryViewModel.NumChannelsActive = info.num_active_channels; nodeSummaryViewModel.NumChannels = info.num_peers; nodeURIViewModel.URI = info.uris.First(); nodeURIViewModel.Alias = info.alias; nodeURIViewModel.Node_Pubkey = info.identity_pubkey; UpdateTaskComplete(taskid); } catch (Exception e) { nodeURIViewModel.URI = "Error loading node information."; } }), }; updateTasks.TryAdd(taskid, updateTask); updateTask.task.Start(); LastNodeURIUpdate = DateTime.Now; if (nodeURIViewModel.URI == "") { //wait for the task to finish. while (updateTasks.ContainsKey(taskid)) { Thread.Sleep(1000); } } } return(PartialView("NodeURI", nodeURIViewModel)); }
public ActionResult CommunityJar(int page = 1) { //return RedirectToAction("Maintenance", "Home"); //return RedirectToAction(actionName: "Maintenance", controllerName:"Home"); LndRpcClient lndClient = GetLndClient(); // TODO: Added this try-catch to avoid errors ViewBag.URI = "03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf@13.92.254.226:9735"; string userId = SetOrUpdateUserCookie(); // This will be the list of transactions shown to the user LnCJTransactions latestTx = new LnCJTransactions(); using (CoinpanicContext db = new CoinpanicContext()) { var jar = db.LnCommunityJars.AsNoTracking().Where(j => j.IsTestnet == usingTestnet).First(); ViewBag.Balance = jar.Balance; int NumTransactions = jar.Transactions.Count(); // Code for the paging ViewBag.NumTransactions = NumTransactions; ViewBag.NumPages = Math.Ceiling(Convert.ToDouble(NumTransactions) / 20.0); ViewBag.ActivePage = page; ViewBag.FirstPage = (page - 3) < 1 ? 1 : (page - 3); ViewBag.LastPage = (page + 3) < 6 ? 6 : (page + 3); //Get user string ip = GetClientIpAddress(Request); var user = GetUserFromDb(userId, db, jar, ip); var userMax = (user.TotalDeposited - user.TotalWithdrawn); if (userMax < 150) { userMax = 150; } ViewBag.UserBalance = userMax; // Query and filter the transactions. Cast into view model. latestTx.Transactions = jar.Transactions.OrderByDescending(t => t.TimestampSettled).Skip((page - 1) * 20).Take(20).Select(t => new LnCJTransaction() { Timestamp = t.TimestampSettled == null ? DateTime.UtcNow : (DateTime)t.TimestampSettled, Amount = t.Value, Memo = t.Memo, Type = t.IsDeposit ? "Deposit" : "Withdrawal", Id = t.TransactionId, Fee = t.FeePaid_Satoshi ?? -1, }).ToList(); latestTx.Balance = jar.Balance; } return(View(latestTx)); }
private static LndRpcClient getLndClient(ZapContext db) { LndRpcClient lndClient; var g = db.ZapreadGlobals.Where(gl => gl.Id == 1) .AsNoTracking() .FirstOrDefault(); lndClient = new LndRpcClient( host: g.LnMainnetHost, macaroonAdmin: g.LnMainnetMacaroonAdmin, macaroonRead: g.LnMainnetMacaroonRead, macaroonInvoice: g.LnMainnetMacaroonInvoice); return(lndClient); }
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); }
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)); }
public ActionResult NodeChannels() { if (DateTime.Now - LastNodeChannelsUpdate > StatusCacheTimeout) { Guid taskid = Guid.NewGuid(); UpdateTask updateTask = new UpdateTask() { id = taskid, task = new Task(() => { try { bool useTestnet = GetUseTestnet(); LndRpcClient lndClient = GetLndClient(useTestnet); string pubkey = nodeURIViewModel.Node_Pubkey; if (pubkey == "") // If not already known { var info = lndClient.GetInfo(); pubkey = info.identity_pubkey; nodeURIViewModel.URI = info.uris.First(); nodeURIViewModel.Alias = info.alias; nodeURIViewModel.Node_Pubkey = info.identity_pubkey; } var channels = lndClient.GetChannels(); nodeChannelViewModel.channels = new List <LnChannelInfoModel>(); // Clear cache using (CoinpanicContext db = new CoinpanicContext()) { LnNode myNode = GetOrCreateNode(lndClient, nodeURIViewModel.Node_Pubkey, db); //Check each channel foreach (var c in channels.channels) { LnChannelInfoModel channelViewModel = new LnChannelInfoModel(); // Check if this is a new channel if (myNode.Channels.Where(ch => ch.ChannelId == c.chan_id).Count() < 1) { try { LnChannel thisChannel = GetOrCreateChannel(lndClient, db, c); if (thisChannel != null && !myNode.Channels.Contains(thisChannel)) { myNode.Channels.Add(thisChannel); db.SaveChanges(); } } catch (Exception e) { // TODO - manage errors reading channels LnChannel thisChannel = null; } } // Check if there is a history for the channel //List<LnChannelConnectionPoints> chanHist = GetChanHist(lndClient, db, c); DateTime cutoff = DateTime.UtcNow - TimeSpan.FromDays(30); Int64 otherchanid = Convert.ToInt64(c.chan_id); channelViewModel.History = db.LnChannelHistory .Where(ch => ch.ChanId == otherchanid) .Where(ch => ch.Timestamp > cutoff) .OrderByDescending(ch => ch.Timestamp) .Include("RemoteNode") .Take(30) .AsNoTracking() .ToList(); LnChannelConnectionPoints prevChanHist; if (channelViewModel.History.Count() > 0) { prevChanHist = channelViewModel.History.First(); } else { prevChanHist = new LnChannelConnectionPoints() { Timestamp = DateTime.UtcNow, }; } // check for changes if (prevChanHist.IsConnected != c.active || prevChanHist.LocalBalance != Convert.ToInt64(c.local_balance) || prevChanHist.RemoteBalance != Convert.ToInt64(c.remote_balance) || DateTime.UtcNow - prevChanHist.Timestamp > TimeSpan.FromHours(6)) { // update LnNode remoteNode = GetOrCreateNode(lndClient, c.remote_pubkey, db); LnChannelConnectionPoints newChanHist = new LnChannelConnectionPoints() { IsConnected = c.active, LocalBalance = Convert.ToInt64(c.local_balance), RemoteBalance = Convert.ToInt64(c.remote_balance), Timestamp = DateTime.UtcNow, RemoteNode = remoteNode, ChanId = Convert.ToInt64(c.chan_id), }; prevChanHist.RemoteNode = remoteNode; db.LnChannelHistory.Add(newChanHist); db.SaveChanges(); } if (c.remote_balance is null) { c.remote_balance = "0"; } if (c.local_balance is null) { c.local_balance = "0"; } channelViewModel.ChanInfo = c; channelViewModel.RemoteNode = prevChanHist.RemoteNode; nodeChannelViewModel.channels.Add(channelViewModel); } } // Updates to channelinfo nodeSummaryViewModel.NumChannels = channels.channels.Count; nodeSummaryViewModel.Capacity = Convert.ToDouble(channels.channels.Sum(c => Convert.ToInt64(c.capacity))) / 100000000.0; nodeSummaryViewModel.LocalCapacity = Convert.ToDouble(channels.channels.Sum(n => Convert.ToInt64(n.local_balance))) / 100000000.0; nodeSummaryViewModel.RemoteCapacity = Convert.ToDouble(channels.channels.Sum(n => Convert.ToInt64(n.remote_balance))) / 100000000.0; nodeSummaryViewModel.ActiveCapacity = Convert.ToDouble(channels.channels.Where(c => c.active).Sum(c => Convert.ToInt64(c.capacity))) / 100000000.0; nodeSummaryViewModel.ActiveLocalCapacity = Convert.ToDouble(channels.channels.Where(c => c.active).Sum(n => Convert.ToInt64(n.local_balance))) / 100000000.0; nodeSummaryViewModel.ActiveRemoteCapacity = Convert.ToDouble(channels.channels.Where(c => c.active).Sum(n => Convert.ToInt64(n.remote_balance))) / 100000000.0; UpdateTaskComplete(taskid); } catch (Exception e) { // Try again on next refresh LastNodeChannelsUpdate = DateTime.Now - StatusCacheTimeout; } }), }; updateTasks.TryAdd(taskid, updateTask); updateTask.task.Start(); LastNodeChannelsUpdate = DateTime.Now; } return(PartialView("NodeChannels", nodeChannelViewModel)); }
public ActionResult GetJarDepositInvoice(string amount, string memo) { string ip = GetClientIpAddress(Request);; if (memo == null || memo == "") { memo = "Coinpanic Community Jar"; } bool useTestnet = GetUseTestnet(); var lndClient = new LndRpcClient( host: System.Configuration.ConfigurationManager.AppSettings[useTestnet ? "LnTestnetHost" : "LnMainnetHost"], macaroonAdmin: System.Configuration.ConfigurationManager.AppSettings[useTestnet ? "LnTestnetMacaroonAdmin" : "LnMainnetMacaroonAdmin"], macaroonRead: System.Configuration.ConfigurationManager.AppSettings[useTestnet ? "LnTestnetMacaroonRead" : "LnMainnetMacaroonRead"], macaroonInvoice: System.Configuration.ConfigurationManager.AppSettings[useTestnet ? "LnTestnetMacaroonInvoice" : "LnMainnetMacaroonInvoice"]); var inv = lndClient.AddInvoice(Convert.ToInt64(amount), memo: memo, expiry: "432000"); LnRequestInvoiceResponse resp = new LnRequestInvoiceResponse() { Invoice = inv.payment_request, Result = "success", }; string userId = ""; //Check if user is returning if (HttpContext.Request.Cookies["CoinpanicCommunityJarUser"] != null) { var cookie = HttpContext.Request.Cookies.Get("CoinpanicCommunityJarUser"); cookie.Expires = DateTime.Now.AddDays(7); //update HttpContext.Response.Cookies.Remove("CoinpanicCommunityJarUser"); HttpContext.Response.SetCookie(cookie); userId = cookie.Value; } else { HttpCookie cookie = new HttpCookie("CoinpanicCommunityJarUser"); cookie.Value = Guid.NewGuid().ToString(); cookie.Expires = DateTime.Now.AddDays(7); HttpContext.Response.Cookies.Remove("CoinpanicCommunityJarUser"); HttpContext.Response.SetCookie(cookie); userId = cookie.Value; } //Create transaction record (not settled) using (CoinpanicContext db = new CoinpanicContext()) { var jar = db.LnCommunityJars.Where(j => j.IsTestnet == useTestnet).First(); //is this a previous user? LnCJUser user; user = GetUserFromDb(userId, db, jar, ip); //create a new transaction LnTransaction t = new LnTransaction() { UserId = user.LnCJUserId, IsSettled = false, Memo = memo, Value = Convert.ToInt64(amount), IsTestnet = GetUseTestnet(), HashStr = inv.r_hash, IsDeposit = true, //TimestampSettled = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date)), TimestampCreated = DateTime.Now, PaymentRequest = inv.payment_request, DestinationPubKey = System.Configuration.ConfigurationManager.AppSettings["LnPubkey"], }; db.LnTransactions.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)); }
/// <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 }); } }
/// <summary> /// /// </summary> /// <exception cref="Exception"></exception> public void CheckLNTransactions() { using (var db = new ZapContext()) { var website = db.ZapreadGlobals.Where(gl => gl.Id == 1) //.AsNoTracking() // need to track since it will get updated .FirstOrDefault(); if (website == null) { throw new Exception("Unable to load website settings."); } LndRpcClient lndClient = GetLNDClient(website); //var invv = lndClient.GetInvoice("8Td4xGBvz4nI2qRLIVC93S9mcTDodd/sylhd9IG7FEA=", out string responseStr, useQuery: false); //var allpayments = lndClient.GetPayments(out string responseStr, include_incomplete: true); // ** DANGER ZONE ** //lndClient.DeletePayments(out string responseStr); // ***************** // These are the unpaid invoices in database (incoming payments) var unpaidInvoices = db.LightningTransactions .Where(t => t.IsSettled == false) .Where(t => t.IsDeposit == true) .Where(t => t.IsIgnored == false) .Include(t => t.User) .Include(t => t.User.Funds); var invoiceDebug = unpaidInvoices.ToList(); foreach (var i in unpaidInvoices) { if (i.HashStr != null) { var inv = lndClient.GetInvoice(rhash: i.HashStr); if (inv != null && inv.settled != null && inv.settled == true) { // Paid but not applied in DB var use = i.UsedFor; if (use == TransactionUse.VotePost) { //if (false) // Disable for now //{ // var vc = new VoteController(); // var v = new VoteController.Vote() // { // a = Convert.ToInt32(i.Amount), // d = i.UsedForAction == TransactionUseAction.VoteDown ? 0 : 1, // Id = i.UsedForId, // tx = i.Id // }; // await vc.Post(v); // i.IsSpent = true; // i.IsSettled = true; // i.TimestampSettled = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(inv.settle_date)).UtcDateTime; //} } else if (use == TransactionUse.VoteComment) { // Not handled yet } else if (use == TransactionUse.UserDeposit) { if (i.User == null) { // Not sure how to deal with this other than add funds to Community website.CommunityEarnedToDistribute += i.Amount; i.IsSpent = true; i.IsSettled = true; i.TimestampSettled = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(inv.settle_date)).UtcDateTime; } else { // Deposit funds in user account var user = i.User; user.Funds.Balance += i.Amount; i.IsSettled = true; i.TimestampSettled = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(inv.settle_date)).UtcDateTime; } } else if (use == TransactionUse.Undefined) { if (i.User == null) { // Not sure how to deal with this other than add funds to Community website.CommunityEarnedToDistribute += i.Amount; i.IsSpent = true; i.IsSettled = true; i.TimestampSettled = DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(inv.settle_date)).UtcDateTime; } else { // Not sure what the user was doing - deposit into their account. ; } } } else if (inv != null) { // Not settled - check expiry var t1 = Convert.ToInt64(inv.creation_date); var tNow = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var tExpire = t1 + Convert.ToInt64(inv.expiry) + 10000; //Add a buffer time if (tNow > tExpire) { // Expired - let's stop checking this invoice i.IsIgnored = true; } else { ; // keep waiting } } } else { // No hash string to look it up. Must be an error somewhere. i.IsIgnored = true; } } db.SaveChanges(); // These are non-settled withdraws in the database var unpaidWithdraws = db.LightningTransactions .Where(t => t.IsSettled == false) // Not settled .Where(t => t.IsDeposit == false) // Withdraw .Where(t => t.IsIgnored == false) // Still valid .Where(t => t.IsLimbo) // Only check those in limbo .Include(t => t.User) .Include(t => t.User.Funds) .OrderByDescending(t => t.TimestampCreated); var numup = unpaidWithdraws.Count(); if (numup > 0) { // Check the unpaid withdraws var payments = lndClient.GetPayments(include_incomplete: true); foreach (var i in unpaidWithdraws) { var pmt = payments.payments.Where(p => p.payment_hash == i.HashStr).FirstOrDefault(); double amount = Convert.ToDouble(i.Amount); if (pmt != null) { // Paid? if (pmt.status == "SUCCEEDED") { // Payment succeeded - remove from Limbo if (i.IsLimbo) { if (i.User.Funds.LimboBalance - amount < 0) { // shouldn't happen! i.User.Funds.LimboBalance = 0; Services.MailingService.SendErrorNotification( title: "Tx caused limbo to be negative. - payment verified", message: "tx.id: " + Convert.ToString(i.Id) + " Reason -1"); } else { i.User.Funds.LimboBalance -= amount; if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } } } i.IsLimbo = false; i.IsIgnored = true; i.IsSettled = true; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored - payment verified", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 0"); } else if (pmt.status == "FAILED") { // Payment failed - refund user if (i.User.Funds.LimboBalance - amount < 0) { if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } i.User.Funds.Balance += i.User.Funds.LimboBalance; i.User.Funds.LimboBalance = 0; } else { i.User.Funds.LimboBalance -= amount; i.User.Funds.Balance += amount; if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } } i.IsLimbo = false; i.IsIgnored = true; i.IsSettled = false; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored - failure verified", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 0"); } // I really don't like these next options!! Should verify validity else if (i.ErrorMessage == "Error: invoice is already paid") { // Invoice is already paid - // This was a duplicate payment - funds were not sent and this payment hash should only have one paid version. if (i.User.Funds.LimboBalance - amount < 0) { if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } i.User.Funds.Balance += i.User.Funds.LimboBalance; i.User.Funds.LimboBalance = 0; } else { i.User.Funds.LimboBalance -= amount; i.User.Funds.Balance += amount; if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } } i.IsLimbo = false; i.IsIgnored = true; i.IsSettled = false; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 1"); } else if (i.ErrorMessage == "Error: payment is in transition") { // Double spend attempt stopped. No loss of funds i.IsIgnored = true; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 2"); } else if (i.ErrorMessage == "Error: FinalExpiryTooSoon") { i.IsIgnored = true; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 3"); } else if (i.ErrorMessage == "Error validating payment." || i.ErrorMessage == "Error executing payment.") { // Payment has come through // No longer in limbo i.User.Funds.LimboBalance -= amount; if (i.User.Funds.LimboBalance < 0) { // Should not happen! i.User.Funds.LimboBalance = 0; } i.IsIgnored = true; i.IsSettled = true; i.IsLimbo = false; Services.MailingService.SendErrorNotification( title: "User withdraw limbo complete (settled)", message: "Withdraw Invoice completed limbo (payment was found)." + "\r\n invoice: " + i.PaymentRequest + "\r\n user: "******"(" + i.User.AppId + ")" + "\r\n amount: " + Convert.ToString(i.Amount) + "\r\n error: " + (i.ErrorMessage ?? "null")); } else { // Payment may have gone through without recording in DB. ; if (i.ErrorMessage != null) { i.IsError = true; } i.IsSettled = true; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 4"); } } else { // Consider as not paid (for now) if not in DB - probably an error if (i.ErrorMessage == "Error: invoice is already paid") { // This was a duplicate payment - funds were not sent and this payment hash should only have one paid version. i.IsIgnored = true; Services.MailingService.SendErrorNotification( title: "Tx marked as ignored", message: "tx.id: " + Convert.ToString(i.Id) + " Reason 5"); } else if (i.ErrorMessage == "Error: amount must be specified when paying a zero amount invoice" || i.ErrorMessage == "Error: payment attempt not completed before timeout") { i.IsIgnored = true; if (i.User.Funds.LimboBalance - amount < 0) { if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } i.User.Funds.Balance += i.User.Funds.LimboBalance; i.User.Funds.LimboBalance = 0; } else { i.User.Funds.LimboBalance -= amount; i.User.Funds.Balance += amount; if (i.User.Funds.LimboBalance < 0) // shouldn't happen! { i.User.Funds.LimboBalance = 0; } } i.IsLimbo = false; // TODO: send user email notification update of result. Services.MailingService.SendErrorNotification( title: "User withdraw limbo expired (not settled - limbo returned)", message: "Withdraw Invoice expired (payment not found). Funds released to user." + "\r\n invoice: " + i.PaymentRequest + "\r\n user: "******"(" + i.User.AppId + ")" + "\r\n amount: " + Convert.ToString(i.Amount) + "\r\n error: " + (i.ErrorMessage ?? "null")); } else { var inv = lndClient.DecodePayment(i.PaymentRequest); var t1 = i.TimestampCreated.Value; var tNow = DateTime.UtcNow; // DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var tExpire = t1.AddSeconds(Convert.ToInt64(inv.expiry) + 10000); //Add a buffer time if (tNow > tExpire) { // Expired - let's stop checking this invoice i.IsIgnored = true; // The payment can't go through any longer. if (i.User.Funds.LimboBalance - amount < 0) { //shouldn't happen! if (i.User.Funds.LimboBalance < 0) { i.User.Funds.LimboBalance = 0; } i.User.Funds.Balance += i.User.Funds.LimboBalance; i.User.Funds.LimboBalance = 0; } else { i.User.Funds.LimboBalance -= amount; i.User.Funds.Balance += amount; } i.IsLimbo = false; // TODO: send user email notification update of result. Services.MailingService.SendErrorNotification( title: "User withdraw limbo expired (not settled - limbo returned)", message: "Withdraw Invoice expired (payment not found). Funds released to user." + "\r\n invoice: " + i.PaymentRequest + "\r\n user: "******"(" + i.User.AppId + ")" + "\r\n amount: " + Convert.ToString(i.Amount) + "\r\n error: " + (i.ErrorMessage ?? "null")); } else { ; // keep waiting } } } } } db.SaveChanges(); } }
private static LnChannel GetOrCreateChannel(LndRpcClient lndClient, CoinpanicContext db, Channel c) { LnChannel thisChannel; string chan_id = ""; // Temporary variable to use as the ChannelId identifier (should be unique) - used for db key if (c.chan_id == null) // This sometimes happens for private channels. { chan_id = c.channel_point; // this should always exist } else { chan_id = c.chan_id; // Use value if reported } var chanFind = db.LnChannels.Where(ch => ch.ChannelId == chan_id); if (chanFind.Count() < 1) { var chan = lndClient.GetChanInfo(chan_id); if (chan == null) { var Node1 = GetOrCreateNode(lndClient, c.remote_pubkey, db); var Node2 = GetOrCreateNode(lndClient, lndClient.GetInfo().identity_pubkey, db); if (Node1 == null || Node2 == null) { // Bad node, can't find info in lnd database. return(null); } // not in database thisChannel = new LnChannel() { Capacity = Convert.ToInt64(chan.capacity), ChannelId = chan_id, ChanPoint = chan.chan_point, Node1 = Node1, Node2 = Node2, }; } else { var Node1 = GetOrCreateNode(lndClient, chan.node1_pub, db); var Node2 = GetOrCreateNode(lndClient, chan.node2_pub, db); // not in database thisChannel = new LnChannel() { Capacity = Convert.ToInt64(chan.capacity), ChannelId = chan.channel_id, ChanPoint = chan.chan_point, Node1 = Node1, Node2 = Node2, }; } db.SaveChanges(); } else { thisChannel = chanFind.First(); } return(thisChannel); }
/// <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 }); } }
/// <summary> /// Method to do hourly status updates /// </summary> public void UpdateHourly() { using (var db = new ZapContext()) { LndRpcClient lndClient = GetLndClient(db); GetInfoResponse ni; try { ni = lndClient.GetInfo(); } catch (RestException e) { // Unable to communicate with LN Node // TODO - properly log error Console.WriteLine(e.Message); return; } var node = db.LNNodes .Include(n => n.Channels) .Where(n => n.PubKey == ni.identity_pubkey) .FirstOrDefault(); if (node == null) { db.LNNodes.Add(new Models.Lightning.LNNode() { PubKey = ni.identity_pubkey, Alias = ni.alias, Address = ni.uris[0], Version = ni.version, IsTestnet = ni.chains[0].network != "mainnet", }); db.SaveChanges(); node = db.LNNodes .Include(n => n.Channels) .Where(n => n.PubKey == ni.identity_pubkey) .FirstOrDefault(); } else { if (node.Version != ni.version) { if (node.VersionHistory == null) { node.VersionHistory = new List <LNNodeVersionHistory>(); } node.VersionHistory.Add(new LNNodeVersionHistory() { Node = node, TimeStamp = DateTime.UtcNow, Version = ni.version, }); node.Version = ni.version; db.SaveChanges(); } } // Check channels var channels = lndClient.GetChannels(); if (channels != null) { foreach (var channel in channels.channels) { // Check if channel in db var nodeChannel = node.Channels.Where(cn => cn.ChannelId == channel.chan_id).FirstOrDefault(); if (nodeChannel != null) { // Update channel if (true) // should this be done so frequently? { nodeChannel.TotalSent_MilliSatoshi = Convert.ToInt64(channel.total_satoshis_sent); nodeChannel.TotalReceived_MilliSatoshi = Convert.ToInt64(channel.total_satoshis_received); nodeChannel.IsOnline = channel.active.HasValue ? channel.active.Value : false; // Add history point nodeChannel.ChannelHistory.Add(new LNChannelHistory() { Channel = nodeChannel, IsOnline = channel.active.HasValue ? channel.active.Value : false, LocalBalance_MilliSatoshi = Convert.ToInt64(channel.local_balance), RemoteBalance_MilliSatoshi = Convert.ToInt64(channel.remote_balance), TimeStamp = DateTime.UtcNow }); db.SaveChanges(); } } else { // New channel var newChan = new LNChannel() { Capacity_MilliSatoshi = Convert.ToInt64(channel.capacity), ChannelHistory = new List <LNChannelHistory>(), ChannelId = channel.chan_id, ChannelPoint = channel.channel_point, IsLocalInitiator = channel.initiator.HasValue ? channel.initiator.Value : false, IsOnline = channel.active.HasValue ? channel.active.Value : false, IsPrivate = channel.@private, LocalReserve_MilliSatoshi = Convert.ToInt64(channel.local_chan_reserve_sat), RemotePubKey = channel.remote_pubkey, RemoteReserve_MilliSatoshi = Convert.ToInt64(channel.remote_chan_reserve_sat), TotalReceived_MilliSatoshi = Convert.ToInt64(channel.total_satoshis_received), TotalSent_MilliSatoshi = Convert.ToInt64(channel.total_satoshis_sent), RemoteAlias = "", }; node.Channels.Add(newChan); db.SaveChanges(); newChan.ChannelHistory.Add(new LNChannelHistory() { Channel = newChan, IsOnline = channel.active.HasValue ? channel.active.Value : false, LocalBalance_MilliSatoshi = Convert.ToInt64(channel.local_balance), RemoteBalance_MilliSatoshi = Convert.ToInt64(channel.remote_balance), TimeStamp = DateTime.UtcNow }); db.SaveChanges(); } } } } }
/// <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) { int maxWithdraw = 150; int maxWithdraw_firstuser = 150; if (lndClient == null) { throw new ArgumentNullException(nameof(lndClient)); } // Lock all threading lock (withdrawLock) { LnCJUser user; try { var decoded = lndClient.DecodePayment(request); // Check if payment request is ok if (decoded.destination == null) { return(new { Result = "Error decoding invoice." }); } // Check that there are funds in the Jar Int64 balance; LnCommunityJar jar; using (CoinpanicContext db = new CoinpanicContext()) { jar = db.LnCommunityJars .Where(j => j.IsTestnet == false) .AsNoTracking().First(); balance = jar.Balance; if (Convert.ToInt64(decoded.num_satoshis) > balance) { return(new { Result = "Requested amount is greater than the available balance." }); } //Get user user = GetUserFromDb(userId, db, jar, ip); var userMax = (user.TotalDeposited - user.TotalWithdrawn); if (userMax < maxWithdraw) { userMax = maxWithdraw; } if (Convert.ToInt64(decoded.num_satoshis) > userMax) { return(new { Result = "Requested amount is greater than maximum allowed." }); } } // Check for banned nodes if (IsNodeBanned(decoded.destination, out string banmessage)) { return(new { Result = "Banned. Reason: " + banmessage }); } if (decoded.destination == "03a9d79bcfab7feb0f24c3cd61a57f0f00de2225b6d31bce0bc4564efa3b1b5aaf") { return(new { Result = "Can not deposit from jar!" }); } //Check rate limits if (nodeWithdrawAttemptTimes.TryGetValue(decoded.destination, out DateTime lastWithdraw)) { if ((DateTime.UtcNow - lastWithdraw) < withdrawRateLimit) { return(new { Result = "Rate limit exceeded." }); } } bool isanon = false; using (CoinpanicContext db = new CoinpanicContext()) { //check if new user DateTime?LastWithdraw = user.TimesampLastWithdraw; //LastWithdraw = db.LnTransactions.Where(tx => tx.IsDeposit == false && tx.IsSettled == true && tx.UserId == user.LnCJUserId).OrderBy(tx => tx.TimestampCreated).AsNoTracking().First().TimestampCreated; if (user.NumWithdraws == 0 && user.NumDeposits == 0) { maxWithdraw = maxWithdraw_firstuser; isanon = true; //check ip (if someone is not using cookies to rob the site) var userIPs = db.LnCommunityJarUsers.Where(u => u.UserIP == ip).ToList(); if (userIPs.Count > 1) { //most recent withdraw LastWithdraw = userIPs.Max(u => u.TimesampLastWithdraw); } // Re-check limits if (Convert.ToInt64(decoded.num_satoshis) > maxWithdraw) { return(new { Result = "Requested amount is greater than maximum allowed for first time users (" + Convert.ToString(maxWithdraw) + "). Make a deposit." }); } } if (user.TotalDeposited - user.TotalWithdrawn < maxWithdraw) { //check for time rate limiting if (DateTime.UtcNow - LastWithdraw < TimeSpan.FromHours(1)) { return(new { Result = "You must wait another " + ((user.TimesampLastWithdraw + TimeSpan.FromHours(1)) - DateTime.UtcNow).Value.TotalMinutes.ToString("0.0") + " minutes before withdrawing again, or make a deposit first." }); } } //Check if already paid if (db.LnTransactions.Where(tx => tx.PaymentRequest == request && tx.IsSettled).Count() > 0) { return(new { Result = "Invoice has already been paid." }); } if (isanon && DateTime.Now - timeLastAnonWithdraw < TimeSpan.FromMinutes(60)) { return(new { Result = "Too many first-time user withdraws. You must wait another " + ((timeLastAnonWithdraw + TimeSpan.FromMinutes(60)) - DateTime.Now).TotalMinutes.ToString("0.0") + " minutes before withdrawing again, or make a deposit first." }); } } SendPaymentResponse paymentresult; if (WithdrawRequests.TryAdd(request, DateTime.UtcNow)) { paymentresult = lndClient.PayInvoice(request); } else { //double request! Thread.Sleep(1000); //Check if paid (in another thread) using (CoinpanicContext db = new CoinpanicContext()) { var txs = db.LnTransactions.Where(t => t.PaymentRequest == request && t.IsSettled).OrderByDescending(t => t.TimestampSettled).AsNoTracking(); if (txs.Count() > 0) { //var tx = txs.First(); WithdrawRequests.TryRemove(request, out DateTime reqInitTimeA); return(new { Result = "success", Fees = "0" }); } return(new { Result = "Please click only once. Payment already in processing." }); } } WithdrawRequests.TryRemove(request, out DateTime reqInitTime); if (paymentresult.payment_error != null) { // Save payment error to database using (CoinpanicContext db = new CoinpanicContext()) { user = GetUserFromDb(userId, db, jar, ip); LnTransaction t = new LnTransaction() { UserId = user.LnCJUserId, IsSettled = false, Memo = decoded.description ?? "Withdraw", Value = Convert.ToInt64(decoded.num_satoshis), IsTestnet = false, HashStr = decoded.payment_hash, IsDeposit = false, TimestampCreated = DateTime.UtcNow, //can't know PaymentRequest = request, DestinationPubKey = decoded.destination, IsError = true, ErrorMessage = paymentresult.payment_error, }; db.LnTransactions.Add(t); db.SaveChanges(); } return(new { Result = "Payment Error: " + paymentresult.payment_error }); } // We have a successful payment // Record time of withdraw to the node nodeWithdrawAttemptTimes.TryAdd(decoded.destination, DateTime.UtcNow); // Notify client(s) var context = GlobalHost.ConnectionManager.GetHubContext <NotificationHub>(); using (CoinpanicContext db = new CoinpanicContext()) { user = GetUserFromDb(userId, db, jar, ip); user.NumWithdraws += 1; user.TotalWithdrawn += Convert.ToInt64(decoded.num_satoshis); user.TimesampLastWithdraw = DateTime.UtcNow; //insert transaction LnTransaction t = new LnTransaction() { UserId = user.LnCJUserId, IsSettled = true, Memo = decoded.description == null ? "Withdraw" : decoded.description, Value = Convert.ToInt64(decoded.num_satoshis), IsTestnet = false, HashStr = decoded.payment_hash, IsDeposit = false, TimestampSettled = DateTime.UtcNow, TimestampCreated = DateTime.UtcNow, //can't know PaymentRequest = request, FeePaid_Satoshi = (paymentresult.payment_route.total_fees == null ? 0 : Convert.ToInt64(paymentresult.payment_route.total_fees)), NumberOfHops = paymentresult.payment_route.hops == null ? 0 : paymentresult.payment_route.hops.Count(), DestinationPubKey = decoded.destination, }; db.LnTransactions.Add(t); db.SaveChanges(); jar = db.LnCommunityJars.Where(j => j.IsTestnet == false).First(); jar.Balance -= Convert.ToInt64(decoded.num_satoshis); jar.Balance -= paymentresult.payment_route.total_fees != null?Convert.ToInt64(paymentresult.payment_route.total_fees) : 0; jar.Transactions.Add(t); db.SaveChanges(); var newT = new LnCJTransaction() { Timestamp = t.TimestampSettled == null ? DateTime.UtcNow : (DateTime)t.TimestampSettled, Amount = t.Value, Memo = t.Memo, Type = t.IsDeposit ? "Deposit" : "Withdrawal", Id = t.TransactionId, }; context.Clients.All.NotifyNewTransaction(newT); } if (isanon) { timeLastAnonWithdraw = DateTime.Now; } return(new { Result = "success", Fees = (paymentresult.payment_route.total_fees == null ? "0" : paymentresult.payment_route.total_fees) }); } catch (Exception e) { return(new { Result = "Error decoding request." }); } } return(new { Result = "Error decoding request." }); }
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)); }
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." })); }
public ActionResult SubmitPaymentRequest(string withdrawId)//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 wguid = new Guid(withdrawId); var lntx = db.LightningTransactions .Where(tx => tx.WithdrawId == wguid) .Where(tx => tx.User.AppId == userAppId) .AsNoTracking() .FirstOrDefault(); if (lntx == null) { Response.StatusCode = (int)HttpStatusCode.BadRequest; return(Json(new { success = false, message = "Invalid withdraw request." })); } if (lntx.IsSettled || lntx.IsIgnored || lntx.IsLimbo || lntx.IsDeposit || lntx.IsError) { Response.StatusCode = (int)HttpStatusCode.BadRequest; return(Json(new { success = false, message = "Invalid withdraw request." })); } // Verify lntx invoice is unique. This is a layer to protect against spam attacks var numTxsWithSamePayreq = db.LightningTransactions .Where(tx => tx.PaymentRequest == lntx.PaymentRequest) .Count(); if (numTxsWithSamePayreq > 1) { Response.StatusCode = (int)HttpStatusCode.BadRequest; return(Json(new { success = false, message = "Invalid withdraw request." })); } // Get interface to LND LndRpcClient lndClient = GetLndClient(); // This is used for DoS or other attack detection string ip = GetClientIpAddress(Request); try { // Submit Payment Request var paymentResult = PaymentsService.TryWithdrawal(lntx, userAppId, ip, lndClient); return(Json(paymentResult)); } catch (RestException e) { // The request to LND threw an exception MailingService.Send(new UserEmailModel() { Destination = System.Configuration.ConfigurationManager.AppSettings["ExceptionReportEmail"], Body = " Exception: " + e.Message + "\r\n Stack: " + e.StackTrace + "\r\n invoice: " + lntx.PaymentRequest + "\r\n user: "******"\r\n error Content: " + e.Content + "\r\n HTTP Status: " + e.StatusDescription, Email = "", Name = "zapread.com Exception", Subject = "User withdraw error 1", }); return(Json(new { Result = "Error processing request." })); } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { MailingService.Send(new UserEmailModel() { Destination = System.Configuration.ConfigurationManager.AppSettings["ExceptionReportEmail"], Body = " Exception: " + e.Message + "\r\n Stack: " + e.StackTrace + "\r\n invoice: " + lntx.PaymentRequest + "\r\n user: "******"", Name = "zapread.com Exception", Subject = "User withdraw error 1b", }); return(Json(new { Result = "Error processing request." })); } } }
/// <summary> /// Synchronize the database with the Lightning Node /// </summary> public void SyncNode() { using (var db = new ZapContext()) { if (running) { return; } running = true; var website = db.ZapreadGlobals.Where(gl => gl.Id == 1) //.AsNoTracking() .FirstOrDefault(); if (website == null) { throw new Exception("Unable to load website settings."); } LndRpcClient lndClient = GetLNDClient(website); int step = 300; int start = 63000; int max = 30; var paymentsResult = lndClient.GetPayments( include_incomplete: true, // Important for checking //reversed: true, // Start with most recent and work backwards max_payments: step); var paymentsResultEnd = lndClient.GetPayments( include_incomplete: true, // Important for checking reversed: true, // Start with most recent and work backwards max_payments: 1); start = Convert.ToInt32(paymentsResult.first_index_offset); max += Convert.ToInt32(paymentsResultEnd.last_index_offset); bool updated = false; bool flagged = false; while (start < max) { foreach (var payment in paymentsResult.payments) { var payment_hash = payment.payment_hash; var invoice = payment.payment_request; var dbMatches = db.LightningTransactions .Where(tx => tx.PaymentRequest == invoice) .ToList(); if (dbMatches.Count > 0) { if (dbMatches.Count == 1) { var tx = dbMatches[0]; if (payment.payment_hash != null && tx.PaymentHash == null) { tx.PaymentHash = payment.payment_hash; updated = true; } if (payment.failure_reason != null && tx.FailureReason == null) { tx.FailureReason = payment.failure_reason; updated = true; } if (payment.payment_index != null && tx.PaymentIndex == null) { tx.PaymentIndex = Convert.ToInt32(payment.payment_index); updated = true; } if (payment.payment_preimage != null && tx.PaymentPreimage == null) { tx.PaymentPreimage = payment.payment_preimage; updated = true; } if (payment.status != null && tx.PaymentStatus == null) { tx.PaymentStatus = payment.status; updated = true; if (payment.status == "UNKNOWN") { // not sure what happened. flagged = true; } if (!tx.IsSettled && payment.status == "SUCCEEDED") { // LND marked as settled but not in db! updated = false; // Don't save for now flagged = true; } if (tx.IsSettled && (payment.status == "FAILED" || payment.status == "IN_FLIGHT")) { // We settled an invoice we should not have updated = false; // Don't save for now flagged = true; } } if (updated) { tx.TimestampUpdated = DateTime.UtcNow; db.SaveChanges(); } else if (flagged) { if (tx.IsLimbo) { // Fix? } } } else { // shouldn't be here } updated = false; // for next round flagged = false; } } start = Convert.ToInt32(paymentsResult.last_index_offset); //get next batch paymentsResult = lndClient.GetPayments( include_incomplete: true, // Important for checking index_offset: start, max_payments: step); } } }
public ActionResult VerifyInvoices() { 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"]); using (var db = new ZapContext()) { // These are the unpaid invoices var unpaidInvoices = db.LightningTransactions .Where(t => t.IsSettled == false) .Where(t => t.IsDeposit == true) .Include(t => t.User); foreach (var i in unpaidInvoices) { if (i.HashStr != null) { var inv = lndClient.GetInvoice(rhash: i.HashStr); if (inv.settled != null && inv.settled == true) { // Paid but not applied in DB var use = i.UsedFor; if (use != TransactionUse.Undefined) { // Use case is recorded in database - perform action var useid = i.UsedForId; // Trigger any async listeners if (use == TransactionUse.UserDeposit) { var context = Microsoft.AspNet.SignalR.GlobalHost.ConnectionManager.GetHubContext <NotificationHub>(); var user = i.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."); //int z = 0; } else { // Update user balance - this is a deposit. // user.Funds.Balance += i.Amount; // userBalance = Math.Floor(user.Funds.Balance); // db.SaveChanges(); } // Notify clients the invoice was paid. context.Clients.All.NotifyInvoicePaid(new { invoice = i.PaymentRequest, balance = userBalance, txid = i.Id }); } else if (use == TransactionUse.Tip) { } else if (use == TransactionUse.VotePost) { } else if (use == TransactionUse.VoteComment) { } } else { // We can't perform any action on the invoice, but we should mark it as settled. // Unfortunately, we don't know who paid the invoice so we can't credit the funds to any account. // The lost funds should probably go to community pot in that case. i.IsSettled = true; i.TimestampSettled = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(inv.settle_date)); } } else if (inv.settled != null && inv.settled == false) { // Still waiting. // TODO } } else { // Darn, the hashstring wasn't recorded for some reason. Can't look up the invoice in LND. // Hide this transaction from appearing next time. i.IsSettled = true; i.TimestampSettled = DateTime.UtcNow; } } db.SaveChangesAsync(); } return(Json(new { result = "success" }, JsonRequestBehavior.AllowGet)); }