btcpayserver/BTCPayServer/Services/Invoices/InvoiceEntity.cs

1007 lines
37 KiB
C#

using NBitcoin;
using System.Linq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Models;
using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using NBitpayClient;
using BTCPayServer.Payments.Bitcoin;
namespace BTCPayServer.Services.Invoices
{
public 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;
}
}
public 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;
}
}
public enum SpeedPolicy
{
HighSpeed = 0,
MediumSpeed = 1,
LowSpeed = 2,
LowMediumSpeed = 3
}
public class InvoiceEntity
{
public const int InternalTagSupport_Version = 1;
public const int Lastest_Version = 1;
public int Version { get; set; }
public string Id
{
get; set;
}
public string StoreId
{
get; set;
}
public string OrderId
{
get; set;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
[Obsolete("Use GetPaymentMethod(network) instead")]
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 ProductInformation ProductInformation
{
get; set;
}
public BuyerInformation BuyerInformation
{
get; set;
}
public string PosData
{
get;
set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
public string[] GetInternalTags(string suffix)
{
return InternalTags == null ? Array.Empty<string>() : InternalTags
.Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture))
.Select(t => t.Substring(suffix.Length)).ToArray();
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get;
set;
}
[Obsolete("Use GetPaymentMethodFactories() instead")]
public string DerivationStrategies
{
get;
set;
}
public IEnumerable<T> GetSupportedPaymentMethod<T>(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod
{
return
GetSupportedPaymentMethod(networks)
.Where(p => paymentMethodId == null || p.PaymentId == paymentMethodId)
.OfType<T>();
}
public IEnumerable<T> GetSupportedPaymentMethod<T>(BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod
{
return GetSupportedPaymentMethod<T>(null, networks);
}
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethod(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
if (!string.IsNullOrEmpty(DerivationStrategies))
{
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
var network = networks.GetNetwork(paymentMethodId.CryptoCode);
if (network != null)
{
if (network == networks.BTC && paymentMethodId.PaymentType == PaymentTypes.BTCLike)
btcReturned = true;
yield return PaymentMethodExtensions.Deserialize(paymentMethodId, strat.Value, network);
}
}
}
if (!btcReturned && !string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
{
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
}
}
#pragma warning restore CS0618
}
internal void SetSupportedPaymentMethods(IEnumerable<ISupportedPaymentMethod> derivationStrategies)
{
JObject obj = new JObject();
foreach (var strat in derivationStrategies)
{
obj.Add(strat.PaymentId.ToString(), PaymentMethodExtensions.Serialize(strat));
#pragma warning disable CS0618
if (strat.PaymentId.IsBTCOnChain)
DerivationStrategy = ((JValue)PaymentMethodExtensions.Serialize(strat)).Value<string>();
}
DerivationStrategies = JsonConvert.SerializeObject(obj);
#pragma warning restore CS0618
}
[JsonIgnore]
public InvoiceStatus 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<PaymentEntity> Payments
{
get; set;
}
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments?.ToList() ?? new List<PaymentEntity>();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
return Payments.Where(p => p.CryptoCode == cryptoCode).ToList();
}
public List<PaymentEntity> GetPayments(BTCPayNetwork network)
{
return GetPayments(network.CryptoCode);
}
#pragma warning restore CS0618
public bool Refundable
{
get;
set;
}
public string RefundMail
{
get;
set;
}
public string RedirectURL
{
get;
set;
}
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
public Money TxFee
{
get;
set;
}
public bool FullNotifications
{
get;
set;
}
public string NotificationEmail
{
get;
set;
}
public string NotificationURL
{
get;
set;
}
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 HistoricalAddressInvoiceData[] HistoricalAddresses
{
get;
set;
}
public HashSet<string> AvailableAddressHashes
{
get;
set;
}
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public double PaymentTolerance { get; set; }
public bool IsExpired()
{
return DateTimeOffset.UtcNow > ExpirationTime;
}
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
{
ServerUrl = ServerUrl ?? "";
InvoiceResponse dto = new InvoiceResponse
{
Id = Id,
StoreId = StoreId,
OrderId = OrderId,
PosData = PosData,
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 = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable },
PaymentSubtotals = new Dictionary<string, long>(),
PaymentTotals = new Dictionary<string, long>(),
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(),
Addresses = new Dictionary<string, string>(),
PaymentCodes = new Dictionary<string, InvoicePaymentUrls>(),
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
};
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
foreach (var info in this.GetPaymentMethods(networkProvider))
{
var accounting = info.Calculate();
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
var subtotalPrice = accounting.TotalDue - accounting.NetworkFee;
var cryptoCode = info.GetId().CryptoCode;
var address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
var exrates = new Dictionary<string, decimal>
{
{ ProductInformation.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();
var scheme = info.Network.UriScheme;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
if (paymentId.PaymentType == PaymentTypes.BTCLike)
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo);
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
if (paymentId.PaymentType == PaymentTypes.LightningLike)
{
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
BOLT11 = $"lightning:{cryptoInfo.Address}"
};
}
#pragma warning disable CS0618
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
{
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 CS0618
dto.CryptoInfo.Add(cryptoInfo);
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
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
Populate(ProductInformation, dto);
dto.Buyer = new JObject();
dto.Buyer.Add(new JProperty("name", BuyerInformation.BuyerName));
dto.Buyer.Add(new JProperty("address1", BuyerInformation.BuyerAddress1));
dto.Buyer.Add(new JProperty("address2", BuyerInformation.BuyerAddress2));
dto.Buyer.Add(new JProperty("locality", BuyerInformation.BuyerCity));
dto.Buyer.Add(new JProperty("region", BuyerInformation.BuyerState));
dto.Buyer.Add(new JProperty("postalCode", BuyerInformation.BuyerZip));
dto.Buyer.Add(new JProperty("country", BuyerInformation.BuyerCountry));
dto.Buyer.Add(new JProperty("phone", BuyerInformation.BuyerPhone));
dto.Buyer.Add(new JProperty("email", string.IsNullOrWhiteSpace(BuyerInformation.BuyerEmail) ? RefundMail : BuyerInformation.BuyerEmail));
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
return dto;
}
private void Populate<TFrom, TDest>(TFrom from, TDest dest)
{
var str = JsonConvert.SerializeObject(from);
JsonConvert.PopulateObject(str, dest);
}
internal bool Support(PaymentMethodId paymentMethodId)
{
var rates = GetPaymentMethods(null);
return rates.TryGet(paymentMethodId) != null;
}
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider)
{
GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data);
return data;
}
public PaymentMethod GetPaymentMethod(BTCPayNetwork network, PaymentTypes paymentType, BTCPayNetworkProvider networkProvider)
{
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
}
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
{
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
if (PaymentMethod != null)
{
foreach (var prop in PaymentMethod.Properties())
{
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
r.CryptoCode = paymentMethodId.CryptoCode;
r.PaymentType = paymentMethodId.PaymentType.ToString();
r.ParentEntity = this;
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
if (r.Network != null || networkProvider == null)
paymentMethods.Add(r);
}
}
#pragma warning restore CS0618
return paymentMethods;
}
Network Dummy = Network.Main;
public void SetPaymentMethod(PaymentMethod paymentMethod)
{
var dict = GetPaymentMethods(null);
dict.AddOrReplace(paymentMethod);
SetPaymentMethods(dict);
}
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
{
var obj = new JObject();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
foreach (var v in paymentMethods)
{
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
clone.CryptoCode = 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
}
public InvoiceState GetInvoiceState()
{
return new InvoiceState(Status, ExceptionStatus);
}
}
public enum InvoiceStatus
{
New,
Paid,
Expired,
Invalid,
Complete,
Confirmed
}
public enum InvoiceExceptionStatus
{
None,
PaidLate,
PaidPartial,
Marked,
Invalid,
PaidOver
}
public class InvoiceState
{
static Dictionary<string, InvoiceStatus> _StringToInvoiceStatus;
static Dictionary<InvoiceStatus, string> _InvoiceStatusToString;
static Dictionary<string, InvoiceExceptionStatus> _StringToExceptionStatus;
static Dictionary<InvoiceExceptionStatus, string> _ExceptionStatusToString;
static InvoiceState()
{
_StringToInvoiceStatus = new Dictionary<string, InvoiceStatus>();
_StringToInvoiceStatus.Add("paid", InvoiceStatus.Paid);
_StringToInvoiceStatus.Add("expired", InvoiceStatus.Expired);
_StringToInvoiceStatus.Add("invalid", InvoiceStatus.Invalid);
_StringToInvoiceStatus.Add("complete", InvoiceStatus.Complete);
_StringToInvoiceStatus.Add("new", InvoiceStatus.New);
_StringToInvoiceStatus.Add("confirmed", InvoiceStatus.Confirmed);
_InvoiceStatusToString = _StringToInvoiceStatus.ToDictionary(kv => kv.Value, kv => kv.Key);
_StringToExceptionStatus = new Dictionary<string, InvoiceExceptionStatus>();
_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(InvoiceStatus status, InvoiceExceptionStatus exceptionStatus)
{
Status = status;
ExceptionStatus = exceptionStatus;
}
public InvoiceStatus Status { get; }
public InvoiceExceptionStatus ExceptionStatus { get; }
public static string ToString(InvoiceStatus status)
{
return _InvoiceStatusToString[status];
}
public static string ToString(InvoiceExceptionStatus exceptionStatus)
{
return _ExceptionStatusToString[exceptionStatus];
}
public bool CanMarkComplete()
{
return (Status == InvoiceStatus.Paid) ||
#pragma warning disable CA1305 // Specify IFormatProvider
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
(Status != InvoiceStatus.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked) ||
(Status == InvoiceStatus.Invalid);
#pragma warning restore CA1305 // Specify IFormatProvider
}
public bool CanMarkInvalid()
{
return (Status == InvoiceStatus.Paid) ||
(Status == InvoiceStatus.New) ||
#pragma warning disable CA1305 // Specify IFormatProvider
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatus.New || Status == InvoiceStatus.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
(Status != InvoiceStatus.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
#pragma warning restore CA1305 // Specify IFormatProvider;
}
public override string ToString()
{
return ToString(Status) + (ExceptionStatus == InvoiceExceptionStatus.None ? string.Empty : $" ({ToString(ExceptionStatus)})");
}
}
public class PaymentMethodAccounting
{
/// <summary>Total amount of this invoice</summary>
public Money TotalDue { get; set; }
/// <summary>Amount of crypto remaining to pay this invoice</summary>
public Money Due { get; set; }
/// <summary>Same as Due, can be negative</summary>
public Money DueUncapped { get; set; }
/// <summary>If DueUncapped is negative, that means user overpaid invoice</summary>
public Money OverpaidHelper
{
get { return DueUncapped > Money.Zero ? Money.Zero : -DueUncapped; }
}
/// <summary>
/// Total amount of the invoice paid after conversion to this crypto currency
/// </summary>
public Money Paid { get; set; }
/// <summary>
/// Total amount of the invoice paid in this currency
/// </summary>
public Money CryptoPaid { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
public int TxRequired { get; set; }
/// <summary>
/// Number of transactions using this payment method
/// </summary>
public int TxCount { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
public Money NetworkFee { get; set; }
/// <summary>
/// Minimum required to be paid in order to accept invoice as paid
/// </summary>
public Money MinimumTotalDue { get; set; }
}
public class PaymentMethod
{
[JsonIgnore]
public InvoiceEntity ParentEntity { get; set; }
[JsonIgnore]
public BTCPayNetwork Network { get; set; }
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().CryptoCode instead")]
public string CryptoCode { get; set; }
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().PaymentType instead")]
public string PaymentType { get; set; }
public PaymentMethodId GetId()
{
#pragma warning disable CS0618 // Type or member is obsolete
return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(PaymentType));
#pragma warning restore CS0618 // Type or member is obsolete
}
public void SetId(PaymentMethodId id)
{
#pragma warning disable CS0618 // Type or member is obsolete
CryptoCode = 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 Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
FeeRate = FeeRate,
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
NextNetworkFee = NextNetworkFee
};
}
else
{
var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails);
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
{
btcLike.NextNetworkFee = NextNetworkFee;
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
btcLike.FeeRate = FeeRate;
}
return details;
}
throw new NotSupportedException(PaymentType);
#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;
}
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
PaymentMethodDetails = jobj;
#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(Func<PaymentEntity, bool> paymentPredicate = null)
{
paymentPredicate = paymentPredicate ?? new Func<PaymentEntity, bool>((p) => true);
var paymentMethods = ParentEntity.GetPaymentMethods(null);
var totalDue = ParentEntity.ProductInformation.Price / Rate;
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = 8;
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0;
var payments =
ParentEntity.GetPayments()
.Where(p => p.Accounted && paymentPredicate(p))
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
paid += _.GetValue(paymentMethods, GetId());
if (!paidEnough)
{
totalDue += txFee;
}
paidEnough |= Extensions.RoundUp(paid, precision) >= Extensions.RoundUp(totalDue, precision);
if (GetId() == _.GetPaymentMethodId())
{
cryptoPaid += _.GetCryptoPaymentData().GetValue();
txRequired++;
}
return _;
})
.ToArray();
var accounting = new PaymentMethodAccounting();
accounting.TxCount = txRequired;
if (!paidEnough)
{
txRequired++;
totalDue += GetTxFee();
}
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision));
accounting.TxRequired = txRequired;
accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision));
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
var minimumTotalDueSatoshi = Math.Max(1.0m, accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m)));
accounting.MinimumTotalDue = Money.Satoshis(minimumTotalDueSatoshi);
return accounting;
}
private decimal GetTxFee()
{
var method = GetPaymentMethodDetails();
if (method == null)
return 0.0m;
return method.GetNextNetworkFee();
}
}
public class PaymentEntity
{
public int Version { get; set; }
public DateTimeOffset ReceivedTime
{
get; set;
}
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;
}
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")]
public string CryptoCode
{
get;
set;
}
[Obsolete("Use GetCryptoPaymentData() instead")]
public string CryptoPaymentData { get; set; }
[Obsolete("Use GetpaymentMethodId().PaymentType instead")]
public string CryptoPaymentDataType { get; set; }
public CryptoPaymentData GetCryptoPaymentData()
{
#pragma warning disable CS0618
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
// For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
paymentData.Outpoint = Outpoint;
paymentData.Output = Output;
paymentData.RBF = true;
paymentData.ConfirmationCount = 0;
paymentData.Legacy = true;
return paymentData;
}
if (GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
{
var paymentData = JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikePaymentData>(CryptoPaymentData);
// legacy
paymentData.Output = Output;
paymentData.Outpoint = Outpoint;
return paymentData;
}
if (GetPaymentMethodId().PaymentType == PaymentTypes.LightningLike)
{
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
}
throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
#pragma warning restore CS0618
}
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 = JsonConvert.SerializeObject(cryptoPaymentData);
#pragma warning restore CS0618
return this;
}
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
{
value = value ?? this.GetCryptoPaymentData().GetValue();
var to = paymentMethodId;
var from = this.GetPaymentMethodId();
if (to == from)
return decimal.Round(value.Value, 8);
var fromRate = paymentMethods[from].Rate;
var toRate = paymentMethods[to].Rate;
var fiatValue = fromRate * decimal.Round(value.Value, 8);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return otherCurrencyValue;
}
public PaymentMethodId GetPaymentMethodId()
{
#pragma warning disable CS0618 // Type or member is obsolete
return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(CryptoPaymentDataType));
#pragma warning restore CS0618 // Type or member is obsolete
}
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
public interface CryptoPaymentData
{
/// <summary>
/// Returns an identifier which uniquely identify the payment
/// </summary>
/// <returns>The payment id</returns>
string GetPaymentId();
/// <summary>
/// Returns terms which will be indexed and searchable in the search bar of invoice
/// </summary>
/// <returns>The search terms</returns>
string[] GetSearchTerms();
/// <summary>
/// Get value of what as been paid
/// </summary>
/// <returns>The amount paid</returns>
decimal GetValue();
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
PaymentTypes GetPaymentType();
string GetDestination(BTCPayNetwork network);
}
}