/// <summary> /// Notify web clients via Signalr that an invoice has been paid /// </summary> /// <param name="invoice"></param> private static void NotifyClientsInvoicePaid(Invoice invoice) { var context = GlobalHost.ConnectionManager.GetHubContext <NotificationHub>(); bool isTesnet = GetUseTestnet(); if (invoice.settle_date == "0" || invoice.settle_date == null) { // Was not settled return; } //Save in db using (CoinpanicContext db = new CoinpanicContext()) { var jar = db.LnCommunityJars.Where(j => j.IsTestnet == isTesnet).First(); //check if unsettled transaction exists var tx = db.LnTransactions.Where(tr => tr.PaymentRequest == invoice.payment_request).ToList(); DateTime settletime = DateTime.UtcNow; LnTransaction t; if (tx.Count > 0) { t = tx.First(); t.TimestampSettled = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date)); t.IsSettled = invoice.settled; } else { //insert transaction settletime = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date)); t = new LnTransaction() { IsSettled = invoice.settled, Memo = invoice.memo, Value = Convert.ToInt64(invoice.value), IsTestnet = GetUseTestnet(), 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, UserId = Guid.NewGuid().ToString(), }; db.LnTransactions.Add(t); } var userId = t.UserId; var user = GetUserFromDb(userId, db, jar, ip: "" // only used when creating a new user, so set blank for this. ); user.TotalDeposited += Convert.ToInt64(invoice.value); user.NumDeposits += 1; user.TimesampLastDeposit = DateTime.UtcNow; t.IsSettled = invoice.settled; if (t.IsDeposit && t.IsSettled) { jar.Balance += Convert.ToInt64(invoice.value); } jar.Transactions.Add(t); db.SaveChanges(); //re-fetch to get the transaction id // Ok, this may not be required. //var tnew = db.LnTransactions.AsNoTracking().FirstOrDefault(tr => tr.PaymentRequest == invoice.payment_request && (DateTime)tr.TimestampSettled == settletime); //if (tnew != null) // t = tnew; // Notify Web clients - this is shown to user // Client needs to check that the transaction received is theirs before marking successful. 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, Fee = t.FeePaid_Satoshi ?? -1, }; context.Clients.All.NotifyNewTransaction(newT); if (invoice.settled) { context.Clients.All.NotifyInvoicePaid(invoice.payment_request); } } }
/// <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." }); }