コード例 #1
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);
        }
コード例 #2
0
 /// <summary>
 /// Constructor with dependency injection for IOC and controller singleton control.
 /// </summary>
 /// <param name="paymentsService"></param>
 public LightningController(ILightningPayments paymentsService)
 {
     this.PaymentsService = paymentsService;
     this.HexEncoder      = new LightningLib.DataEncoders.HexEncoder();
 }
コード例 #3
0
        // We have received asynchronous notification that a lightning invoice has been paid
        private async static Task NotifyClientsInvoicePaid(Invoice invoice)
        {
            // Check if the invoice received was paid.  LND also sends updates
            // for new invoices to the invoice stream.  We want to listen for settled invoices here.
            if (!invoice.settled.HasValue)
            {
                // Bad invoice
                // Todo - logging
                return;
            }
            if (!invoice.settled.Value)
            {
                // Optional - add some logic to check invoices on the stream.  These invoices
                // which are not settled are likely new deposit requests.  For the purposes of
                // this function, we only care about settled invoices.
                return;
            }

            // This is the amount which was paid - needed in case of 0 (any) value invoices
            var amount = Convert.ToInt64(invoice.amt_paid_sat, CultureInfo.InvariantCulture);

            // Update LN transaction status in db
            using (ZapContext db = new ZapContext())
            {
                // Check if unsettled transaction exists in db matching the invoice that was just settled.
                var tx = db.LightningTransactions
                         .Include(tr => tr.User)
                         .Where(tr => tr.PaymentRequest == invoice.payment_request)
                         .ToList();

                DateTime      settletime = DateTime.UtcNow;
                LNTransaction t;
                if (tx.Count > 0) // Shouldn't ever be more than one entry - could add a check for this.
                {
                    // We found it - mark it as settled.
                    t = tx.First();
                    t.TimestampSettled = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date, CultureInfo.InvariantCulture));
                    t.IsSettled        = true;

                    if (t.Amount != amount)
                    {
                        if (t.Amount == 0)
                        {
                            // This was a zero-invoice
                            t.Amount = amount; // this will be saved to DB
                        }
                    }
                }
                else
                {
                    // This invoice is not in the db - it may not be related to this service.
                    // We still record it in our database for any possible user forensics/history later.
                    settletime = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc)
                                 + TimeSpan.FromSeconds(Convert.ToInt64(invoice.settle_date, CultureInfo.InvariantCulture));

                    var HexEncoder  = new LightningLib.DataEncoders.HexEncoder(); // static method
                    var rhash_bytes = Convert.FromBase64String(invoice.r_hash);
                    var rhash_hex   = HexEncoder.EncodeData(rhash_bytes);

                    t = new LNTransaction()
                    {
                        IsSettled        = invoice.settled.Value,
                        Memo             = invoice.memo.SanitizeXSS(),
                        Amount           = amount,//Convert.ToInt64(invoice.value, CultureInfo.InvariantCulture),
                        HashStr          = invoice.r_hash,
                        PreimageHash     = rhash_hex,
                        IsDeposit        = true,
                        TimestampSettled = settletime,
                        TimestampCreated = DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc) + TimeSpan.FromSeconds(Convert.ToInt64(invoice.creation_date, CultureInfo.InvariantCulture)),
                        PaymentRequest   = invoice.payment_request,
                        User             = null,
                    };
                    db.LightningTransactions.Add(t);
                }
                await db.SaveChangesAsync().ConfigureAwait(true);

                // Financial transaction
                double userBalance = 0.0; // This value will be returned later

                if (t.User != null)       // the user could be null if it is an anonymous payment.
                {
                    var userFunds = await db.Users
                                    .Where(u => u.Id == t.User.Id)
                                    .Select(u => u.Funds)
                                    .FirstOrDefaultAsync().ConfigureAwait(true);

                    // Make every attempt to save user balance in DB
                    int  attempts = 0;
                    bool saveFailed;
                    bool saveAborted = false;
                    do
                    {
                        attempts++;
                        saveFailed = false;

                        if (attempts < 50)
                        {
                            // This really shouldn't happen!
                            if (userFunds == null)
                            {
                                // this should not happen? - verify.  Maybe this is the case for transactions related to votes?
                                // throw new Exception("Error accessing user information related to settled LN Transaction.");
                            }
                            else
                            {
                                // Update user balance - this is a deposit.
                                userFunds.Balance += amount;// Convert.ToInt64(invoice.value, CultureInfo.InvariantCulture);
                                userBalance        = Math.Floor(userFunds.Balance);
                            }
                            try
                            {
                                db.SaveChanges(); // synchronous
                            }
                            catch (System.Data.Entity.Infrastructure.DbUpdateConcurrencyException ex)
                            {
                                saveFailed = true;
                                var entry = ex.Entries.Single();
                                entry.Reload();
                            }
                        }
                        else
                        {
                            saveAborted = true;
                        }
                    }while (saveFailed);

                    // Don't record as settled if save was aborted due to DB concurrency failure.
                    // LND database will show it was settled, but DB not.
                    // Another process can check DB sync and correct later.
                    if (saveAborted == false)
                    {
                        t.IsSettled = invoice.settled.Value;
                        await db.SaveChangesAsync().ConfigureAwait(true);
                    }
                }
                else
                {
                    t.IsSettled = invoice.settled.Value;
                    await db.SaveChangesAsync().ConfigureAwait(true);
                }

                // Send live signal to listening clients on websockets/SignalR
                //var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
                //context.Clients.All.NotifyInvoicePaid(new { invoice = invoice.payment_request, balance = userBalance, txid = t.Id });
                await NotificationService.SendPaymentNotification(
                    t.User == null? "" : t.User.AppId,
                    invoice : invoice.payment_request,
                    userBalance : userBalance,
                    txid : t.Id).ConfigureAwait(true);
            }
        }
コード例 #4
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 });
            }
        }