mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-20 10:40:29 +01:00
1007 lines
37 KiB
C#
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);
|
|
}
|
|
}
|