public async Task <InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider) { List <string> textSearch = new List <string>(); invoice = Clone(invoice); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); invoice.Payments = new List <PaymentEntity>(); invoice.StoreId = storeId; using (var context = _ContextFactory.CreateContext()) { context.Invoices.Add(new InvoiceData() { StoreDataId = storeId, Id = invoice.Id, Created = invoice.InvoiceTime, Blob = ToBytes(invoice), OrderId = invoice.OrderId, Status = invoice.Status, ItemCode = invoice.ProductInformation.ItemCode, CustomerEmail = invoice.RefundMail }); foreach (var cryptoData in invoice.GetCryptoData().Values) { var network = networkProvider.GetNetwork(cryptoData.CryptoCode); if (network == null) { throw new InvalidOperationException("CryptoCode unsupported"); } context.AddressInvoices.Add(new AddressInvoiceData() { Address = BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(), InvoiceDataId = invoice.Id, CreatedTime = DateTimeOffset.UtcNow, }); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoice.Id, Address = cryptoData.DepositAddress, #pragma warning disable CS0618 CryptoCode = cryptoData.CryptoCode, #pragma warning restore CS0618 Assigned = DateTimeOffset.UtcNow }); textSearch.Add(cryptoData.DepositAddress); textSearch.Add(cryptoData.Calculate().TotalDue.ToString()); } context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); await context.SaveChangesAsync().ConfigureAwait(false); } textSearch.Add(invoice.Id); textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture)); textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)); textSearch.Add(invoice.OrderId); textSearch.Add(ToString(invoice.BuyerInformation)); textSearch.Add(ToString(invoice.ProductInformation)); textSearch.Add(invoice.StoreId); AddToTextSearch(invoice.Id, textSearch.ToArray()); return(invoice); }
public async Task <InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, InvoiceLogs creationLogs, BTCPayNetworkProvider networkProvider) { List <string> textSearch = new List <string>(); invoice = Clone(invoice, null); invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); #pragma warning disable CS0618 invoice.Payments = new List <PaymentEntity>(); #pragma warning restore CS0618 invoice.StoreId = storeId; using (var context = _ContextFactory.CreateContext()) { context.Invoices.Add(new Data.InvoiceData() { StoreDataId = storeId, Id = invoice.Id, Created = invoice.InvoiceTime, Blob = ToBytes(invoice, null), OrderId = invoice.OrderId, #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete ItemCode = invoice.ProductInformation.ItemCode, CustomerEmail = invoice.RefundMail }); foreach (var paymentMethod in invoice.GetPaymentMethods(networkProvider)) { if (paymentMethod.Network == null) { throw new InvalidOperationException("CryptoCode unsupported"); } var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination(); string address = GetDestination(paymentMethod, paymentMethod.Network.NBitcoinNetwork); context.AddressInvoices.Add(new AddressInvoiceData() { InvoiceDataId = invoice.Id, CreatedTime = DateTimeOffset.UtcNow, }.Set(address, paymentMethod.GetId())); context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData() { InvoiceDataId = invoice.Id, Assigned = DateTimeOffset.UtcNow }.SetAddress(paymentDestination, paymentMethod.GetId().ToString())); textSearch.Add(paymentDestination); textSearch.Add(paymentMethod.Calculate().TotalDue.ToString()); } context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id }); foreach (var log in creationLogs.ToList()) { context.InvoiceEvents.Add(new InvoiceEventData() { InvoiceDataId = invoice.Id, Message = log.Log, Timestamp = log.Timestamp, UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10)) }); } await context.SaveChangesAsync().ConfigureAwait(false); } textSearch.Add(invoice.Id); textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture)); textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)); textSearch.Add(invoice.OrderId); textSearch.Add(ToString(invoice.BuyerInformation, null)); textSearch.Add(ToString(invoice.ProductInformation, null)); textSearch.Add(invoice.StoreId); AddToTextSearch(invoice.Id, textSearch.ToArray()); return(invoice); }
public async Task <InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, string[] additionalSearchTerms = null) { var textSearch = new HashSet <string>(); invoice = Clone(invoice); invoice.Networks = _btcPayNetworkProvider; invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); #pragma warning disable CS0618 invoice.Payments = new List <PaymentEntity>(); #pragma warning restore CS0618 invoice.StoreId = storeId; using (var context = _applicationDbContextFactory.CreateContext()) { var invoiceData = new Data.InvoiceData() { StoreDataId = storeId, Id = invoice.Id, Created = invoice.InvoiceTime, Blob = ToBytes(invoice, null), OrderId = invoice.Metadata.OrderId, #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete ItemCode = invoice.Metadata.ItemCode, CustomerEmail = invoice.RefundMail, Archived = false }; await context.Invoices.AddAsync(invoiceData); foreach (var paymentMethod in invoice.GetPaymentMethods()) { if (paymentMethod.Network == null) { throw new InvalidOperationException("CryptoCode unsupported"); } var details = paymentMethod.GetPaymentMethodDetails(); if (!details.Activated) { continue; } var paymentDestination = details.GetPaymentDestination(); string address = GetDestination(paymentMethod); await context.AddressInvoices.AddAsync(new AddressInvoiceData() { InvoiceDataId = invoice.Id, CreatedTime = DateTimeOffset.UtcNow, }.Set(address, paymentMethod.GetId())); await context.HistoricalAddressInvoices.AddAsync(new HistoricalAddressInvoiceData() { InvoiceDataId = invoice.Id, Assigned = DateTimeOffset.UtcNow }.SetAddress(paymentDestination, paymentMethod.GetId().ToString())); textSearch.Add(paymentDestination); textSearch.Add(paymentMethod.Calculate().TotalDue.ToString()); } await context.PendingInvoices.AddAsync(new PendingInvoiceData() { Id = invoice.Id }); textSearch.Add(invoice.Id); textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture)); if (!invoice.IsUnsetTopUp()) { textSearch.Add(invoice.Price.ToString(CultureInfo.InvariantCulture)); } textSearch.Add(invoice.Metadata.OrderId); textSearch.Add(invoice.StoreId); textSearch.Add(invoice.Metadata.BuyerEmail); if (additionalSearchTerms != null) { textSearch.AddRange(additionalSearchTerms); } AddToTextSearch(context, invoiceData, textSearch.ToArray()); await context.SaveChangesAsync().ConfigureAwait(false); } return(invoice); }
private static string GetHttpJobId(InvoiceEntity invoice) { return($"{invoice.Id}-{invoice.Status}-HTTP"); }
private async Task <IEnumerable <(PaymentEntity Payment, TransactionResult Transaction)> > GetPaymentsWithTransaction(InvoiceEntity invoice) { var getPayments = invoice.Payments .Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token))) .ToArray(); await Task.WhenAll(getPayments).ConfigureAwait(false); var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction)); return(transactions); }
private async Task <(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice) { bool needSave = false; //Fetch unknown payments var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy); changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false); var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray(); var invoiceIds = utxos.Select(u => _InvoiceRepository.GetInvoiceIdFromScriptPubKey(u.Output.ScriptPubKey)).ToArray(); utxos = utxos .Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id) .ToArray(); List <Coin> receivedCoins = new List <Coin>(); foreach (var received in utxos) { if (received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey) { receivedCoins.Add(new Coin(received.Outpoint, received.Output)); } } var alreadyAccounted = new HashSet <OutPoint>(invoice.Payments.Select(p => p.Outpoint)); BitcoinAddress generatedAddress = null; bool dirtyAddress = false; foreach (var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint))) { var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false); invoice.Payments.Add(payment); if (coin.ScriptPubKey == invoice.DepositAddress.ScriptPubKey && generatedAddress == null) { dirtyAddress = true; } } ////// if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow) { needSave = true; await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = "expired"; } if (invoice.Status == "new" || invoice.Status == "expired") { var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum(); if (totalPaid >= invoice.GetTotalCryptoDue()) { if (invoice.Status == "new") { invoice.Status = "paid"; if (invoice.FullNotifications) { _NotificationManager.Notify(invoice); } invoice.ExceptionStatus = null; await _InvoiceRepository.UnaffectAddress(invoice.Id); needSave = true; } else if (invoice.Status == "expired") { invoice.ExceptionStatus = "paidLate"; needSave = true; } } if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver") { invoice.ExceptionStatus = "paidOver"; await _InvoiceRepository.UnaffectAddress(invoice.Id); needSave = true; } if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial") { Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress); invoice.ExceptionStatus = "paidPartial"; needSave = true; if (dirtyAddress) { var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy)); Logs.PayServer.LogInformation("Generate new " + address); await _InvoiceRepository.NewAddress(invoice.Id, address); } } } if (invoice.Status == "paid") { if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow) { var transactions = await GetPaymentsWithTransaction(invoice); if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed) { transactions = transactions.Where(t => !t.Transaction.Transaction.RBF); } else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed) { transactions = transactions.Where(t => t.Transaction.Confirmations >= 1); } else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed) { transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); } var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); if (totalConfirmed >= invoice.GetTotalCryptoDue()) { await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = "confirmed"; _NotificationManager.Notify(invoice); needSave = true; } } else { await _InvoiceRepository.UnaffectAddress(invoice.Id); invoice.Status = "invalid"; needSave = true; } } if (invoice.Status == "confirmed") { var transactions = await GetPaymentsWithTransaction(invoice); transactions = transactions.Where(t => t.Transaction.Confirmations >= 6); var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum(); if (totalConfirmed >= invoice.GetTotalCryptoDue()) { invoice.Status = "complete"; if (invoice.FullNotifications) { _NotificationManager.Notify(invoice); } needSave = true; } } return(needSave, changes); }
/// <summary> /// Add a payment to an invoice /// </summary> /// <param name="invoiceId"></param> /// <param name="date"></param> /// <param name="paymentData"></param> /// <param name="cryptoCode"></param> /// <param name="accounted"></param> /// <returns>The PaymentEntity or null if already added</returns> public async Task <PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetworkBase network, bool accounted = false) { await using var context = _applicationDbContextFactory.CreateContext(); var invoice = await context.Invoices.FindAsync(invoiceId); if (invoice == null) { return(null); } InvoiceEntity invoiceEntity = invoice.GetBlob(_btcPayNetworkProvider); PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType())); IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); PaymentEntity entity = new PaymentEntity { Version = 1, #pragma warning disable CS0618 CryptoCode = network.CryptoCode, #pragma warning restore CS0618 ReceivedTime = date.UtcDateTime, Accounted = accounted, NetworkFee = paymentMethodDetails.GetNextNetworkFee(), Network = network }; entity.SetCryptoPaymentData(paymentData); //TODO: abstract if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod && bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly && bitcoinPaymentMethod.NextNetworkFee == Money.Zero) { bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.NetworkFeeRate.GetFee(100); // assume price for 100 bytes paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod); invoiceEntity.SetPaymentMethod(paymentMethod); invoice.Blob = InvoiceRepository.ToBytes(invoiceEntity, network); } PaymentData data = new PaymentData { Id = paymentData.GetPaymentId(), Blob = InvoiceRepository.ToBytes(entity, entity.Network), InvoiceDataId = invoiceId, Accounted = accounted }; await context.Payments.AddAsync(data); InvoiceRepository.AddToTextSearch(context, invoice, paymentData.GetSearchTerms()); var alreadyExists = false; try { await context.SaveChangesAsync().ConfigureAwait(false); } catch (DbUpdateException) { alreadyExists = true; } if (alreadyExists) { return(null); } if (paymentData.PaymentConfirmed(entity, invoiceEntity.SpeedPolicy)) { _eventAggregator.Publish(new InvoiceEvent(invoiceEntity, InvoiceEvent.PaymentSettled) { Payment = entity }); } return(entity); }