using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Client.Models; using BTCPayServer.Data; using BTCPayServer.JsonConverters; using BTCPayServer.Models; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; using NBitcoin; using NBitcoin.DataEncoders; using NBitpayClient; using NBXplorer; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace BTCPayServer.Services.Invoices { public class InvoiceCryptoInfo : NBitpayClient.InvoiceCryptoInfo { [JsonProperty("paymentUrls")] public new InvoicePaymentUrls PaymentUrls { get; set; } public class InvoicePaymentUrls : NBitpayClient.InvoicePaymentUrls { [JsonExtensionData] public Dictionary AdditionalData { get; set; } } } public class InvoiceMetadata : IHasAdditionalData { public static readonly JsonSerializer MetadataSerializer; static InvoiceMetadata() { var seria = new JsonSerializer(); seria.DefaultValueHandling = DefaultValueHandling.Ignore; seria.FloatParseHandling = FloatParseHandling.Decimal; seria.ContractResolver = new CamelCasePropertyNamesContractResolver(); MetadataSerializer = seria; } [JsonIgnore] public string OrderId { get => this.GetAdditionalData("orderId"); set => this.SetAdditionalData("orderId", value); } [JsonIgnore] public string OrderUrl { get => this.GetAdditionalData("orderUrl"); set => this.SetAdditionalData("orderUrl", value); } [JsonIgnore] public string PaymentRequestId { get => this.GetAdditionalData("paymentRequestId"); set => this.SetAdditionalData("paymentRequestId", value); } [JsonIgnore] public string BuyerName { get => this.GetAdditionalData("buyerName"); set => this.SetAdditionalData("buyerName", value); } [JsonIgnore] public string BuyerEmail { get => this.GetAdditionalData("buyerEmail"); set => this.SetAdditionalData("buyerEmail", value); } [JsonIgnore] public string BuyerCountry { get => this.GetAdditionalData("buyerCountry"); set => this.SetAdditionalData("buyerCountry", value); } [JsonIgnore] public string BuyerZip { get => this.GetAdditionalData("buyerZip"); set => this.SetAdditionalData("buyerZip", value); } [JsonIgnore] public string BuyerState { get => this.GetAdditionalData("buyerState"); set => this.SetAdditionalData("buyerState", value); } [JsonIgnore] public string BuyerCity { get => this.GetAdditionalData("buyerCity"); set => this.SetAdditionalData("buyerCity", value); } [JsonIgnore] public string BuyerAddress2 { get => this.GetAdditionalData("buyerAddress2"); set => this.SetAdditionalData("buyerAddress2", value); } [JsonIgnore] public string BuyerAddress1 { get => this.GetAdditionalData("buyerAddress1"); set => this.SetAdditionalData("buyerAddress1", value); } [JsonIgnore] public string BuyerPhone { get => this.GetAdditionalData("buyerPhone"); set => this.SetAdditionalData("buyerPhone", value); } [JsonIgnore] public string ItemDesc { get => this.GetAdditionalData("itemDesc"); set => this.SetAdditionalData("itemDesc", value); } [JsonIgnore] public string ItemCode { get => this.GetAdditionalData("itemCode"); set => this.SetAdditionalData("itemCode", value); } [JsonIgnore] public bool? Physical { get => this.GetAdditionalData("physical"); set => this.SetAdditionalData("physical", value); } [JsonIgnore] public decimal? TaxIncluded { get => this.GetAdditionalData("taxIncluded"); set => this.SetAdditionalData("taxIncluded", value); } /// /// posData is a field that may be treated differently for presentation and in some legacy API /// Before, it was a string field which could contain some JSON data inside. /// For making it easier to query on the DB, and for logic using PosData in the code, we decided to /// parse it as a JObject. /// /// This property will return the posData as a JObject, even if it's a Json string inside. /// [JsonIgnore] public JObject PosData { get { if (AdditionalData == null || !(AdditionalData.TryGetValue("posData", out var jt) is true)) return default; if (jt.Type == JTokenType.Null) return default; if (jt.Type == JTokenType.String) try { return JObject.Parse(jt.Value()); } catch { return null; } if (jt.Type == JTokenType.Object) return (JObject)jt; return null; } set { this.SetAdditionalData("posData", value); } } /// /// See comments on /// [JsonIgnore] public string PosDataLegacy { get { return this.GetAdditionalData("posData"); } set { if (value != null) { try { PosData = JObject.Parse(value); return; } catch { } } this.SetAdditionalData("posData", value); } } [JsonExtensionData] public IDictionary AdditionalData { get; set; } public static InvoiceMetadata FromJObject(JObject jObject) { return jObject.ToObject(MetadataSerializer); } public JObject ToJObject() { return JObject.FromObject(this, MetadataSerializer); } } public class InvoiceEntity : IHasAdditionalData { class BuyerInformation { [JsonProperty(PropertyName = "buyerName")] public string BuyerName { get; set; } [JsonProperty(PropertyName = "buyerEmail")] public string BuyerEmail { get; set; } [JsonProperty(PropertyName = "buyerCountry")] public string BuyerCountry { get; set; } [JsonProperty(PropertyName = "buyerZip")] public string BuyerZip { get; set; } [JsonProperty(PropertyName = "buyerState")] public string BuyerState { get; set; } [JsonProperty(PropertyName = "buyerCity")] public string BuyerCity { get; set; } [JsonProperty(PropertyName = "buyerAddress2")] public string BuyerAddress2 { get; set; } [JsonProperty(PropertyName = "buyerAddress1")] public string BuyerAddress1 { get; set; } [JsonProperty(PropertyName = "buyerPhone")] public string BuyerPhone { get; set; } } class ProductInformation { [JsonProperty(PropertyName = "itemDesc")] public string ItemDesc { get; set; } [JsonProperty(PropertyName = "itemCode")] public string ItemCode { get; set; } [JsonProperty(PropertyName = "physical")] public bool Physical { get; set; } [JsonProperty(PropertyName = "price")] public decimal Price { get; set; } [JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)] public decimal TaxIncluded { get; set; } [JsonProperty(PropertyName = "currency")] public string Currency { get; set; } } [JsonIgnore] public BTCPayNetworkProvider Networks { get; set; } public const int InternalTagSupport_Version = 1; public const int GreenfieldInvoices_Version = 2; public const int Lastest_Version = 2; public int Version { get; set; } public string Id { get; set; } public string StoreId { get; set; } public SpeedPolicy SpeedPolicy { get; set; } public string DefaultLanguage { get; set; } [Obsolete("Use GetPaymentMethod(network) instead")] [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Rate { get; set; } public DateTimeOffset InvoiceTime { get; set; } public DateTimeOffset ExpirationTime { get; set; } [Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")] public string DepositAddress { get; set; } public InvoiceMetadata Metadata { get; set; } [JsonConverter(typeof(NumericStringJsonConverter))] public decimal Price { get; set; } public string Currency { get; set; } public string DefaultPaymentMethod { get; set; } #nullable enable public PaymentMethodId? GetDefaultPaymentMethod() { PaymentMethodId.TryParse(DefaultPaymentMethod, out var id); return id; } #nullable restore [JsonExtensionData] public IDictionary AdditionalData { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public HashSet InternalTags { get; set; } = new HashSet(); public string[] GetInternalTags(string prefix) { return InternalTags == null ? Array.Empty() : InternalTags .Where(t => t.StartsWith(prefix, StringComparison.InvariantCulture)) .Select(t => t.Substring(prefix.Length)).ToArray(); } [Obsolete("Use GetPaymentMethodFactories() instead")] [JsonIgnore] public JObject DerivationStrategies { get { if (AdditionalData is null || AdditionalData.TryGetValue("derivationStrategies", out var v) is not true) { if (AdditionalData is null || AdditionalData.TryGetValue("derivationStrategy", out v) is not true || Networks.BTC is null) return null; // This code is very unlikely called. "derivationStrategy" is an old property that was present in 2018. // And this property is only read for unexpired invoices with lazy payments (Feature unavailable then) var settings = DerivationSchemeSettings.Parse(v.ToString(), Networks.BTC); settings.AccountOriginal = v.ToString(); settings.Source = "ManualDerivationScheme"; return JObject.Parse(settings.ToJson()); } if (v.Type == JTokenType.String) return JObject.Parse(v.Value()); if (v.Type == JTokenType.Object) return (JObject)v; return null; } set { this.SetAdditionalData("derivationStrategies", value); this.SetAdditionalData("derivationStrategy", null); } } public IEnumerable GetSupportedPaymentMethod(PaymentMethodId paymentMethodId) where T : ISupportedPaymentMethod { return GetSupportedPaymentMethod() .Where(p => paymentMethodId == null || p.PaymentId == paymentMethodId) .OfType(); } public IEnumerable GetSupportedPaymentMethod() where T : ISupportedPaymentMethod { return GetSupportedPaymentMethod(null); } public IEnumerable GetSupportedPaymentMethod() { #pragma warning disable CS0618 if (DerivationStrategies != null) { foreach (var strat in DerivationStrategies.Properties()) { if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId)) { continue; } var network = Networks.GetNetwork(paymentMethodId.CryptoCode); if (network != null) { yield return paymentMethodId.PaymentType.DeserializeSupportedPaymentMethod(network, strat.Value); } } } #pragma warning restore CS0618 } internal void SetSupportedPaymentMethods(IEnumerable derivationStrategies) { JObject obj = new JObject(); foreach (var strat in derivationStrategies) { obj.Add(strat.PaymentId.ToString(), PaymentMethodExtensions.Serialize(strat)); #pragma warning disable CS0618 } DerivationStrategies = obj; #pragma warning restore CS0618 } [JsonIgnore] public Dictionary Rates { get; private set; } public void UpdateTotals() { Rates = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var p in GetPaymentMethods()) { Rates.TryAdd(p.Currency, p.Rate); } PaidAmount = new Amounts() { Currency = Currency }; foreach (var payment in GetPayments(false)) { payment.Rate = Rates[payment.Currency]; payment.InvoiceEntity = this; payment.UpdateAmounts(); if (payment.Accounted) { PaidAmount.Gross += payment.InvoicePaidAmount.Gross; PaidAmount.Net += payment.InvoicePaidAmount.Net; } } NetDue = Price - PaidAmount.Net; MinimumNetDue = Price * (1.0m - ((decimal)PaymentTolerance / 100.0m)) - PaidAmount.Net; PaidFee = PaidAmount.Gross - PaidAmount.Net; if (NetDue < 0.0m) { // If any payment method exactly pay the invoice, the overpayment is caused by // rounding limitation of the underlying payment method. // Document this overpayment as dust, and set the net due to 0 if (GetPaymentMethods().Any(p => p.Calculate().DueUncapped == 0.0m)) { Dust = -NetDue; NetDue = 0.0m; } } } /// /// Overpaid amount caused by payment method /// Example: If you need to pay 124.4 sats, the on-chain payment need to be technically rounded to 125 sats, the extra 0.6 sats shouldn't be considered an over payment. /// [JsonIgnore] public decimal Dust { get; set; } /// /// The due to consider the invoice paid (can be negative if over payment) /// [JsonIgnore] public decimal NetDue { get; set; } /// /// Minimum due to consider the invoice paid (can be negative if overpaid) /// [JsonIgnore] public decimal MinimumNetDue { get; set; } public bool IsUnderPaid => MinimumNetDue > 0; [JsonIgnore] public bool IsOverPaid => NetDue < 0; /// /// Total of network fee paid by accounted payments /// [JsonIgnore] public decimal PaidFee { get; set; } [JsonIgnore] public InvoiceStatusLegacy Status { get; set; } [JsonProperty(PropertyName = "status")] [Obsolete("Use Status instead")] public string StatusString => InvoiceState.ToString(Status); [JsonIgnore] public InvoiceExceptionStatus ExceptionStatus { get; set; } [JsonProperty(PropertyName = "exceptionStatus")] [Obsolete("Use ExceptionStatus instead")] public string ExceptionStatusString => InvoiceState.ToString(ExceptionStatus); [Obsolete("Use GetPayments instead")] public List Payments { get; set; } #pragma warning disable CS0618 public List GetPayments(bool accountedOnly) { return Payments?.Where(entity => entity.GetPaymentMethodId() != null && (!accountedOnly || entity.Accounted)).ToList() ?? new List(); } public List GetPayments(string cryptoCode, bool accountedOnly) { return GetPayments(accountedOnly).Where(p => p.Currency == cryptoCode).ToList(); } public List GetPayments(BTCPayNetworkBase network, bool accountedOnly) { return GetPayments(network.CryptoCode, accountedOnly); } #pragma warning restore CS0618 // public bool Refundable { get; set; } public bool? RequiresRefundEmail { get; set; } = null; public string RefundMail { get; set; } public string StoreSupportUrl { get; set; } [JsonProperty("redirectURL")] public string RedirectURLTemplate { get; set; } [JsonIgnore] public Uri RedirectURL => FillPlaceholdersUri(RedirectURLTemplate); private Uri FillPlaceholdersUri(string v) { var uriStr = (v ?? string.Empty).Replace("{OrderId}", System.Web.HttpUtility.UrlEncode(Metadata.OrderId) ?? "", StringComparison.OrdinalIgnoreCase) .Replace("{InvoiceId}", System.Web.HttpUtility.UrlEncode(Id) ?? "", StringComparison.OrdinalIgnoreCase); if (Uri.TryCreate(uriStr, UriKind.Absolute, out var uri) && (uri.Scheme == "http" || uri.Scheme == "https")) return uri; return null; } public bool RedirectAutomatically { get; set; } [Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")] public Money TxFee { get; set; } public bool FullNotifications { get; set; } public string NotificationEmail { get; set; } [JsonProperty("notificationURL")] public string NotificationURLTemplate { get; set; } [JsonIgnore] public Uri NotificationURL => FillPlaceholdersUri(NotificationURLTemplate); public string ServerUrl { get; set; } [Obsolete("Use Set/GetPaymentMethod() instead")] [JsonProperty(PropertyName = "cryptoData")] public JObject PaymentMethod { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)] public DateTimeOffset MonitoringExpiration { get; set; } public HashSet AvailableAddressHashes { get; set; } public bool ExtendedNotifications { get; set; } public List Events { get; internal set; } public double PaymentTolerance { get; set; } public bool Archived { get; set; } [JsonConverter(typeof(StringEnumConverter))] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public InvoiceType Type { get; set; } public List Refunds { get; set; } [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; } [JsonConverter(typeof(StringEnumConverter))] public CheckoutType? CheckoutType { get; set; } public bool LazyPaymentMethods { get; set; } public bool IsExpired() { return DateTimeOffset.UtcNow > ExpirationTime; } public InvoiceResponse EntityToDTO() { ServerUrl = ServerUrl ?? ""; InvoiceResponse dto = new InvoiceResponse { Id = Id, StoreId = StoreId, OrderId = Metadata.OrderId, PosData = Metadata.PosDataLegacy, CurrentTime = DateTimeOffset.UtcNow, InvoiceTime = InvoiceTime, ExpirationTime = ExpirationTime, #pragma warning disable CS0618 // Type or member is obsolete Status = StatusString, ExceptionStatus = ExceptionStatus == InvoiceExceptionStatus.None ? new JValue(false) : new JValue(ExceptionStatusString), #pragma warning restore CS0618 // Type or member is obsolete Currency = Currency, PaymentSubtotals = new Dictionary(), PaymentTotals = new Dictionary(), SupportedTransactionCurrencies = new Dictionary(), Addresses = new Dictionary(), PaymentCodes = new Dictionary(), ExchangeRates = new Dictionary>() }; dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id; dto.CryptoInfo = new List(); dto.MinerFees = new Dictionary(); foreach (var info in this.GetPaymentMethods()) { var accounting = info.Calculate(); var cryptoInfo = new InvoiceCryptoInfo(); var subtotalPrice = accounting.TotalDue - accounting.NetworkFee; var cryptoCode = info.GetId().CryptoCode; var details = info.GetPaymentMethodDetails(); var address = details?.GetPaymentDestination(); var exrates = new Dictionary { { Currency, cryptoInfo.Rate } }; cryptoInfo.CryptoCode = cryptoCode; cryptoInfo.PaymentType = info.GetId().PaymentType.ToString(); cryptoInfo.Rate = info.Rate; cryptoInfo.Price = subtotalPrice.ToString(); cryptoInfo.Due = accounting.Due.ToString(); cryptoInfo.Paid = accounting.Paid.ToString(); cryptoInfo.TotalDue = accounting.TotalDue.ToString(); cryptoInfo.NetworkFee = accounting.NetworkFee.ToString(); cryptoInfo.TxCount = accounting.TxCount; cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString(); cryptoInfo.Address = address; cryptoInfo.ExRates = exrates; var paymentId = info.GetId(); cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}"; cryptoInfo.Payments = GetPayments(info.Network, true).Select(entity => { var data = entity.GetCryptoPaymentData(); return new InvoicePaymentInfo() { Id = data.GetPaymentId(), Fee = entity.NetworkFee, Value = data.GetValue(), Completed = data.PaymentCompleted(entity), Confirmed = data.PaymentConfirmed(entity, SpeedPolicy), Destination = data.GetDestination(), PaymentType = data.GetPaymentType().ToString(), ReceivedDate = entity.ReceivedTime.DateTime }; }).ToList(); if (details?.Activated is true) { paymentId.PaymentType.PopulateCryptoInfo(this, info, cryptoInfo, ServerUrl); if (paymentId.PaymentType == PaymentTypes.BTCLike) { var minerInfo = new MinerFeeInfo(); minerInfo.TotalFee = accounting.ToSmallestUnit(accounting.NetworkFee); minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)details).FeeRate .GetFee(1).Satoshi; dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo); #pragma warning disable 618 if (info.Currency == "BTC") { dto.BTCPrice = cryptoInfo.Price; dto.Rate = cryptoInfo.Rate; dto.ExRates = cryptoInfo.ExRates; dto.BitcoinAddress = cryptoInfo.Address; dto.BTCPaid = cryptoInfo.Paid; dto.BTCDue = cryptoInfo.Due; dto.PaymentUrls = cryptoInfo.PaymentUrls; } #pragma warning restore 618 } } dto.CryptoInfo.Add(cryptoInfo); dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls); dto.PaymentSubtotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(subtotalPrice)); dto.PaymentTotals.Add(paymentId.ToString(), accounting.ToSmallestUnit(accounting.TotalDue)); dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency() { Enabled = true }); dto.Addresses.Add(paymentId.ToString(), address); dto.ExchangeRates.TryAdd(cryptoCode, exrates); } //dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice dto.ItemCode = Metadata.ItemCode; dto.ItemDesc = Metadata.ItemDesc; dto.TaxIncluded = Metadata.TaxIncluded ?? 0m; dto.Price = Price; dto.Currency = Currency; dto.CheckoutType = CheckoutType; dto.Buyer = new JObject(); dto.Buyer.Add(new JProperty("name", Metadata.BuyerName)); dto.Buyer.Add(new JProperty("address1", Metadata.BuyerAddress1)); dto.Buyer.Add(new JProperty("address2", Metadata.BuyerAddress2)); dto.Buyer.Add(new JProperty("locality", Metadata.BuyerCity)); dto.Buyer.Add(new JProperty("region", Metadata.BuyerState)); dto.Buyer.Add(new JProperty("postalCode", Metadata.BuyerZip)); dto.Buyer.Add(new JProperty("country", Metadata.BuyerCountry)); dto.Buyer.Add(new JProperty("phone", Metadata.BuyerPhone)); dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(Metadata.BuyerEmail) ? RefundMail : Metadata.BuyerEmail)); dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for dto.Guid = Guid.NewGuid().ToString(); return dto; } internal bool Support(PaymentMethodId paymentMethodId) { var rates = GetPaymentMethods(); return rates.TryGet(paymentMethodId) != null; } public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId) { GetPaymentMethods().TryGetValue(paymentMethodId, out var data); return data; } public PaymentMethod GetPaymentMethod(BTCPayNetworkBase network, PaymentType paymentType) { return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType)); } public PaymentMethodDictionary GetPaymentMethods() { PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); var serializer = new Serializer(null); #pragma warning disable CS0618 if (PaymentMethod != null) { foreach (var prop in PaymentMethod.Properties()) { var r = serializer.ToObject(prop.Value.ToString()); if (!PaymentMethodId.TryParse(prop.Name, out var paymentMethodId)) { continue; } r.Currency = paymentMethodId.CryptoCode; r.PaymentType = paymentMethodId.PaymentType.ToString(); r.ParentEntity = this; if (Networks != null) { r.Network = Networks.GetNetwork(r.Currency); if (r.Network is null) continue; } paymentMethods.Add(r); } } #pragma warning restore CS0618 return paymentMethods; } public void SetPaymentMethod(PaymentMethod paymentMethod) { var dict = GetPaymentMethods(); dict.AddOrReplace(paymentMethod); SetPaymentMethods(dict); } public void SetPaymentMethods(PaymentMethodDictionary paymentMethods) { var obj = new JObject(); var serializer = new Serializer(null); #pragma warning disable CS0618 foreach (var v in paymentMethods) { var clone = serializer.ToObject(serializer.ToString(v)); clone.Currency = null; clone.PaymentType = null; obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone)))); } PaymentMethod = obj; foreach (var cryptoData in paymentMethods) { cryptoData.ParentEntity = this; } #pragma warning restore CS0618 UpdateTotals(); } public InvoiceState GetInvoiceState() { return new InvoiceState(Status, ExceptionStatus); } /// /// Invoice version < 1 were saving metadata directly under the InvoiceEntity /// object. But in version > 2, the metadata is saved under the InvoiceEntity.Metadata object /// This method is extracting metadata from the InvoiceEntity of version < 1 invoices and put them in InvoiceEntity.Metadata. /// internal void MigrateLegacyInvoice() { T TryParseMetadata(string field) where T : class { if (AdditionalData.TryGetValue(field, out var token) && token is JObject obj) { return obj.ToObject(); } return null; } if (TryParseMetadata("buyerInformation") is BuyerInformation buyerInformation && TryParseMetadata("productInformation") is ProductInformation productInformation) { var wellknown = new InvoiceMetadata() { BuyerAddress1 = buyerInformation.BuyerAddress1, BuyerAddress2 = buyerInformation.BuyerAddress2, BuyerCity = buyerInformation.BuyerCity, BuyerCountry = buyerInformation.BuyerCountry, BuyerEmail = buyerInformation.BuyerEmail, BuyerName = buyerInformation.BuyerName, BuyerPhone = buyerInformation.BuyerPhone, BuyerState = buyerInformation.BuyerState, BuyerZip = buyerInformation.BuyerZip, ItemCode = productInformation.ItemCode, ItemDesc = productInformation.ItemDesc, Physical = productInformation.Physical, TaxIncluded = productInformation.TaxIncluded }; if (AdditionalData.TryGetValue("posData", out var token) && token is JValue val && val.Type == JTokenType.String) { wellknown.PosDataLegacy = val.Value(); } if (AdditionalData.TryGetValue("orderId", out var token2) && token2 is JValue val2 && val2.Type == JTokenType.String) { wellknown.OrderId = val2.Value(); } Metadata = wellknown; Currency = productInformation.Currency?.Trim().ToUpperInvariant(); Price = productInformation.Price; } else { throw new InvalidOperationException("Not a legacy invoice"); } } public bool IsUnsetTopUp() { return Type == InvoiceType.TopUp && Price == 0.0m; } public Amounts PaidAmount { get; set; } } public enum InvoiceStatusLegacy { New, Paid, Expired, Invalid, Complete, Confirmed } public static class InvoiceStatusLegacyExtensions { public static InvoiceStatus ToModernStatus(this InvoiceStatusLegacy legacy) { switch (legacy) { case InvoiceStatusLegacy.Complete: case InvoiceStatusLegacy.Confirmed: return InvoiceStatus.Settled; case InvoiceStatusLegacy.Expired: return InvoiceStatus.Expired; case InvoiceStatusLegacy.Invalid: return InvoiceStatus.Invalid; case InvoiceStatusLegacy.Paid: return InvoiceStatus.Processing; case InvoiceStatusLegacy.New: return InvoiceStatus.New; default: throw new NotSupportedException(); } } } public class InvoiceState { static readonly Dictionary _StringToInvoiceStatus; static readonly Dictionary _InvoiceStatusToString; static readonly Dictionary _StringToExceptionStatus; static readonly Dictionary _ExceptionStatusToString; static InvoiceState() { _StringToInvoiceStatus = new Dictionary(); _StringToInvoiceStatus.Add("paid", InvoiceStatusLegacy.Paid); _StringToInvoiceStatus.Add("expired", InvoiceStatusLegacy.Expired); _StringToInvoiceStatus.Add("invalid", InvoiceStatusLegacy.Invalid); _StringToInvoiceStatus.Add("complete", InvoiceStatusLegacy.Complete); _StringToInvoiceStatus.Add("new", InvoiceStatusLegacy.New); _StringToInvoiceStatus.Add("confirmed", InvoiceStatusLegacy.Confirmed); _InvoiceStatusToString = _StringToInvoiceStatus.ToDictionary(kv => kv.Value, kv => kv.Key); _StringToExceptionStatus = new Dictionary(); _StringToExceptionStatus.Add(string.Empty, InvoiceExceptionStatus.None); _StringToExceptionStatus.Add("paidPartial", InvoiceExceptionStatus.PaidPartial); _StringToExceptionStatus.Add("paidLate", InvoiceExceptionStatus.PaidLate); _StringToExceptionStatus.Add("paidOver", InvoiceExceptionStatus.PaidOver); _StringToExceptionStatus.Add("marked", InvoiceExceptionStatus.Marked); _ExceptionStatusToString = _StringToExceptionStatus.ToDictionary(kv => kv.Value, kv => kv.Key); _StringToExceptionStatus.Add("false", InvoiceExceptionStatus.None); } public InvoiceState(string status, string exceptionStatus) { Status = _StringToInvoiceStatus[status]; ExceptionStatus = _StringToExceptionStatus[exceptionStatus ?? string.Empty]; } public InvoiceState(InvoiceStatusLegacy status, InvoiceExceptionStatus exceptionStatus) { Status = status; ExceptionStatus = exceptionStatus; } public InvoiceStatusLegacy Status { get; } public InvoiceExceptionStatus ExceptionStatus { get; } public static string ToString(InvoiceStatusLegacy status) { return _InvoiceStatusToString[status]; } public static string ToString(InvoiceExceptionStatus exceptionStatus) { return _ExceptionStatusToString[exceptionStatus]; } public bool CanMarkComplete() { return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired or InvoiceStatusLegacy.Invalid || (Status != InvoiceStatusLegacy.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked); } public bool CanMarkInvalid() { return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired || (Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked); } public bool CanRefund() { return Status == InvoiceStatusLegacy.Confirmed || Status == InvoiceStatusLegacy.Complete || (Status == InvoiceStatusLegacy.Expired && (ExceptionStatus == InvoiceExceptionStatus.PaidLate || ExceptionStatus == InvoiceExceptionStatus.PaidOver || ExceptionStatus == InvoiceExceptionStatus.PaidPartial)) || Status == InvoiceStatusLegacy.Invalid; } public bool IsSettled() { return Status == InvoiceStatusLegacy.Confirmed || Status == InvoiceStatusLegacy.Complete || (Status == InvoiceStatusLegacy.Expired && ExceptionStatus is InvoiceExceptionStatus.PaidLate or InvoiceExceptionStatus.PaidOver); } public override int GetHashCode() { return HashCode.Combine(Status, ExceptionStatus); } public static bool operator ==(InvoiceState a, InvoiceState b) { if (a is null && b is null) return true; if (a is null) return false; return a.Equals(b); } public static bool operator !=(InvoiceState a, InvoiceState b) { return !(a == b); } public bool Equals(InvoiceState o) { if (o is null) return false; return o.Status == Status && o.ExceptionStatus == ExceptionStatus; } public override bool Equals(object obj) { if (obj is InvoiceState o) { return this.Equals(o); } return false; } public override string ToString() { return Status.ToModernStatus() + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})"); } } public class PaymentMethodAccounting { public int Divisibility { get; set; } /// Total amount of this invoice public decimal TotalDue { get; set; } /// Amount of crypto remaining to pay this invoice public decimal Due { get; set; } /// Same as Due, can be negative public decimal DueUncapped { get; set; } /// If DueUncapped is negative, that means user overpaid invoice public decimal OverpaidHelper { get { return DueUncapped > 0.0m ? 0.0m : -DueUncapped; } } /// /// Total amount of the invoice paid after conversion to this crypto currency /// public decimal Paid { get; set; } /// /// Total amount of the invoice paid in this currency /// public decimal CryptoPaid { get; set; } /// /// Number of transactions required to pay /// public int TxRequired { get; set; } /// /// Number of transactions using this payment method /// public int TxCount { get; set; } /// /// Total amount of network fee to pay to the invoice /// public decimal NetworkFee { get; set; } /// /// Total amount of network fee to pay to the invoice /// public decimal NetworkFeeAlreadyPaid { get; set; } /// /// Minimum required to be paid in order to accept invoice as paid /// public decimal MinimumTotalDue { get; set; } public decimal ToSmallestUnit(decimal v) { for (int i = 0; i < Divisibility; i++) { v *= 10.0m; } return v; } public string ShowMoney(decimal v) => MoneyExtensions.ShowMoney(v, Divisibility); } public interface IPaymentMethod { PaymentMethodId GetId(); decimal Rate { get; set; } IPaymentMethodDetails GetPaymentMethodDetails(); } public class PaymentMethod : IPaymentMethod { [JsonIgnore] public InvoiceEntity ParentEntity { get; set; } [JsonIgnore] public BTCPayNetworkBase Network { get; set; } [JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)] public string Currency { get; set; } [JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)] [Obsolete("Use GetId().PaymentType instead")] public string PaymentType { get; set; } /// /// We only use this to pass a singleton asking to the payment handler to prefer payments through TOR, we don't really /// need to save this information /// [JsonIgnore] public bool PreferOnion { get; set; } public PaymentMethodId GetId() { #pragma warning disable CS0618 // Type or member is obsolete return new PaymentMethodId(Currency, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : PaymentTypes.Parse(PaymentType)); #pragma warning restore CS0618 // Type or member is obsolete } public void SetId(PaymentMethodId id) { #pragma warning disable CS0618 // Type or member is obsolete Currency = id.CryptoCode; PaymentType = id.PaymentType.ToString(); #pragma warning restore CS0618 // Type or member is obsolete } [JsonProperty(PropertyName = "rate")] public decimal Rate { get; set; } [Obsolete("Use GetPaymentMethodDetails() instead")] [JsonProperty(PropertyName = "paymentMethod")] public JObject PaymentMethodDetails { get; set; } public IPaymentMethodDetails GetPaymentMethodDetails() { #pragma warning disable CS0618 // Type or member is obsolete // Legacy, old code does not have PaymentMethods if (string.IsNullOrEmpty(PaymentType) || PaymentMethodDetails == null) { return new BitcoinLikeOnChainPaymentMethod { FeeRate = FeeRate, DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress, NextNetworkFee = NextNetworkFee }; } // A bug in previous version of BTCPay Server wasn't properly serializing those fields if (PaymentMethodDetails["PaymentHash"] is JObject) PaymentMethodDetails["PaymentHash"] = null; if (PaymentMethodDetails["Preimage"] is JObject) PaymentMethodDetails["Preimage"] = null; IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString()); switch (details) { case BitcoinLikeOnChainPaymentMethod btcLike: btcLike.NextNetworkFee = NextNetworkFee; btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress; btcLike.FeeRate = FeeRate; break; case LightningLikePaymentMethodDetails lnLike: // use set properties and fall back to values from payment data var payments = ParentEntity.GetPayments(true).Where(paymentEntity => paymentEntity.GetPaymentMethodId() == GetId()); var payment = payments.Select(p => p.GetCryptoPaymentData() as LightningLikePaymentData).FirstOrDefault(); var paymentHash = payment?.PaymentHash != null && payment.PaymentHash != default ? payment.PaymentHash : null; var preimage = payment?.Preimage != null && payment.Preimage != default ? payment.Preimage : null; lnLike.PaymentHash = lnLike.PaymentHash != null && lnLike.PaymentHash != default ? lnLike.PaymentHash : paymentHash; lnLike.Preimage = lnLike.Preimage != null && lnLike.Preimage != default ? lnLike.Preimage : preimage; break; } return details; #pragma warning restore CS0618 // Type or member is obsolete } public PaymentMethod SetPaymentMethodDetails(IPaymentMethodDetails paymentMethod) { #pragma warning disable CS0618 // Type or member is obsolete // Legacy, need to fill the old fields if (PaymentType == null) PaymentType = paymentMethod.GetPaymentType().ToString(); else if (PaymentType != paymentMethod.GetPaymentType().ToString()) throw new InvalidOperationException("Invalid payment method affected"); if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod) { NextNetworkFee = bitcoinPaymentMethod.NextNetworkFee; FeeRate = bitcoinPaymentMethod.FeeRate; DepositAddress = bitcoinPaymentMethod.DepositAddress; } PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod)); #pragma warning restore CS0618 // Type or member is obsolete return this; } [JsonProperty(PropertyName = "feeRate")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")] public FeeRate FeeRate { get; set; } [JsonProperty(PropertyName = "txFee")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).NextNetworkFee")] public Money NextNetworkFee { get; set; } [JsonProperty(PropertyName = "depositAddress")] [Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")] public string DepositAddress { get; set; } public PaymentMethodAccounting Calculate() { var i = ParentEntity; int precision = Network?.Divisibility ?? 8; var accounting = new PaymentMethodAccounting(); var thisPaymentMethodPayments = i.GetPayments(true).Where(p => GetId() == p.GetPaymentMethodId()).ToList(); accounting.TxCount = thisPaymentMethodPayments.Count; accounting.TxRequired = accounting.TxCount; var grossDue = i.Price + i.PaidFee; if (i.MinimumNetDue > 0.0m) { accounting.TxRequired++; grossDue += Rate * (GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m); } accounting.Divisibility = precision; accounting.TotalDue = Coins(grossDue / Rate, precision); accounting.Paid = Coins(i.PaidAmount.Gross / Rate, precision); accounting.CryptoPaid = Coins(thisPaymentMethodPayments.Sum(p => p.PaidAmount.Gross), precision); // This one deal with the fact where it might looks like a slight over payment due to the dust of another payment method. // So if we detect the NetDue is zero, just cap dueUncapped to 0 var dueUncapped = i.NetDue == 0.0m ? 0.0m : grossDue - i.PaidAmount.Gross; accounting.DueUncapped = Coins(dueUncapped / Rate, precision); accounting.Due = Max(accounting.DueUncapped, 0.0m); accounting.NetworkFee = Coins((grossDue - i.Price) / Rate, precision); accounting.NetworkFeeAlreadyPaid = Coins(i.PaidFee / Rate, precision); accounting.MinimumTotalDue = Max(Smallest(precision), Coins((grossDue * (1.0m - ((decimal)i.PaymentTolerance / 100.0m))) / Rate, precision)); return accounting; } private decimal Smallest(int precision) { decimal a = 1.0m; for (int i = 0; i < precision; i++) { a /= 10.0m; } return a; } decimal Max(decimal a, decimal b) => a > b ? a : b; const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m; internal static decimal Coins(decimal v, int precision) { v = Extensions.RoundUp(v, precision); // Clamp the value to not crash on degenerate invoices if (v > MaxCoinValue) v = MaxCoinValue; return v; } } public class PaymentEntity { [NotMapped] [JsonIgnore] public BTCPayNetworkBase Network { get; set; } public int Version { get; set; } [Obsolete("Use ReceivedTime instead")] [JsonProperty("receivedTime", DefaultValueHandling = DefaultValueHandling.Ignore)] // Old invoices were storing the received time in second public DateTimeOffset? ReceivedTimeSeconds { get; set; } [Obsolete("Use ReceivedTime instead")] [JsonProperty("receivedTimeMs", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonConverter(typeof(DateTimeMilliJsonConverter))] // Our RBF detection logic depends on properly ordering payments based on // received time, so we needed a received time in milli to ensure that // even if payments are separated by less than a second, they would still be ordered correctly public DateTimeOffset? ReceivedTimeMilli { get; set; } [JsonIgnore] public DateTimeOffset ReceivedTime { get { #pragma warning disable 618 return (ReceivedTimeMilli ?? ReceivedTimeSeconds).GetValueOrDefault(); #pragma warning restore 618 } set { #pragma warning disable 618 ReceivedTimeMilli = value; #pragma warning restore 618 } } public decimal NetworkFee { get; set; } [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")] public OutPoint Outpoint { get; set; } [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")] public TxOut Output { get; set; } public bool Accounted { get; set; } string _Currency; [JsonProperty("cryptoCode")] public string Currency { get { return _Currency ?? "BTC"; } set { _Currency = value; } } [Obsolete("Use GetCryptoPaymentData() instead")] public string CryptoPaymentData { get; set; } [Obsolete("Use GetpaymentMethodId().PaymentType instead")] public string CryptoPaymentDataType { get; set; } [JsonIgnore] public decimal Rate { get; set; } [JsonIgnore] /// public string InvoiceCurrency => InvoiceEntity.Currency; /// The amount paid by this payment in the /// [JsonIgnore] public Amounts PaidAmount { get; set; } /// /// The amount paid by this payment in the /// [JsonIgnore] public Amounts InvoicePaidAmount { get; set; } [JsonIgnore] public InvoiceEntity InvoiceEntity { get; set; } public void UpdateAmounts() { var pd = GetCryptoPaymentData(); if (pd is null) return; var value = pd.GetValue(); PaidAmount = new Amounts() { Currency = Currency, Gross = value, Net = value - NetworkFee }; InvoicePaidAmount = new Amounts() { Currency = InvoiceCurrency, Gross = PaidAmount.Gross * Rate, Net = PaidAmount.Net * Rate }; } public CryptoPaymentData GetCryptoPaymentData() { CryptoPaymentData paymentData = null; #pragma warning disable CS0618 // Type or member is obsolete if (string.IsNullOrEmpty(CryptoPaymentData)) { // For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety var bitcoin = new BitcoinLikePaymentData(); bitcoin.Network = Network; bitcoin.Outpoint = Outpoint; bitcoin.Output = Output; bitcoin.RBF = true; bitcoin.ConfirmationCount = 0; bitcoin.Legacy = true; bitcoin.Output = Output; bitcoin.Outpoint = Outpoint; paymentData = bitcoin; } else { var paymentMethodId = GetPaymentMethodId(); if (paymentMethodId is null) { return null; } paymentData = paymentMethodId.PaymentType.DeserializePaymentData(Network, CryptoPaymentData); if (paymentData is null) { return null; } paymentData.Network = Network; if (paymentData is BitcoinLikePaymentData bitcoin) { bitcoin.Output = Output; bitcoin.Outpoint = Outpoint; } } return paymentData; } public PaymentEntity SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData) { #pragma warning disable CS0618 if (cryptoPaymentData is Payments.Bitcoin.BitcoinLikePaymentData paymentData) { // Legacy Outpoint = paymentData.Outpoint; Output = paymentData.Output; /// } CryptoPaymentDataType = cryptoPaymentData.GetPaymentType().ToString(); CryptoPaymentData = GetPaymentMethodId().PaymentType.SerializePaymentData(Network, cryptoPaymentData); #pragma warning restore CS0618 return this; } public PaymentMethodId GetPaymentMethodId() { #pragma warning disable CS0618 // Type or member is obsolete PaymentType paymentType; if (string.IsNullOrEmpty(CryptoPaymentDataType)) { paymentType = BitcoinPaymentType.Instance; } else if (!PaymentTypes.TryParse(CryptoPaymentDataType, out paymentType)) { return null; } return new PaymentMethodId(Currency ?? "BTC", paymentType); #pragma warning restore CS0618 // Type or member is obsolete } } /// /// A record of a payment /// public interface CryptoPaymentData { [JsonIgnore] BTCPayNetworkBase Network { get; set; } /// /// Returns an identifier which uniquely identify the payment /// /// The payment id string GetPaymentId(); /// /// Returns terms which will be indexed and searchable in the search bar of invoice /// /// The search terms string[] GetSearchTerms(); /// /// Get value of what as been paid /// /// The amount paid decimal GetValue(); bool PaymentCompleted(PaymentEntity entity); bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy); PaymentType GetPaymentType(); string GetDestination(); string GetPaymentProof(); } }