Пример #1
0
        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." }));
            }
        }
Пример #2
0
        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);
        }
Пример #3
0
        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);
        }
Пример #4
0
        /// <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));
        }
Пример #5
0
        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);
        }
Пример #6
0
        /// <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));
        }
Пример #7
0
        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));
        }
Пример #8
0
        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);
        }
Пример #9
0
        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);
        }
Пример #10
0
        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));
        }
Пример #11
0
        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));
        }
Пример #12
0
        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));
        }
Пример #13
0
        /// <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 });
            }
        }
Пример #14
0
        /// <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();
            }
        }
Пример #15
0
        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);
        }
Пример #16
0
        /// <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 });
            }
        }
Пример #17
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();
                        }
                    }
                }
            }
        }
Пример #18
0
        /// <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." });
        }
Пример #19
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));
        }
Пример #20
0
        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."
            }));
        }
Пример #21
0
        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." }));
                }
            }
        }
Пример #22
0
        /// <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);
                }
            }
        }
Пример #23
0
        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));
        }