示例#1
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 });
            }
        }
示例#2
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 });
            }
        }