Further abstract payment data by encapsulating bitcoin related logic into BitcoinLikePaymentData

This commit is contained in:
nicolas.dorier 2018-02-18 02:19:35 +09:00
parent b898cc030c
commit a1ee09cd85
6 changed files with 125 additions and 69 deletions

View file

@ -66,7 +66,7 @@ namespace BTCPayServer
public BTCPayDefaultSettings DefaultSettings { get; set; } public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; } public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 7; public int MaxTrackedConfirmation { get; internal set; } = 6;
public override string ToString() public override string ToString()
{ {

View file

@ -80,14 +80,34 @@ namespace BTCPayServer.Controllers
var payments = invoice var payments = invoice
.GetPayments() .GetPayments()
.Where(p => p.GetCryptoPaymentData() is BitcoinLikePaymentData)
.Select(async payment => .Select(async payment =>
{ {
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment(); var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode()); var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode(); m.CryptoCode = payment.GetCryptoCode();
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork); m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString(); int confirmationCount = 0;
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
}
else
{
confirmationCount = paymentData.ConfirmationCount;
}
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime; m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId); m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted; m.Replaced = !payment.Accounted;

View file

@ -165,10 +165,11 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode); var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null) if (invoice != null)
{ {
var payment = invoice.GetPayments().FirstOrDefault(p => p.Outpoint == txCoin.Outpoint); var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
if (payment == null) var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist)
{ {
payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, txCoin, network.CryptoCode); var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy); await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
} }
else else
@ -205,17 +206,27 @@ namespace BTCPayServer.HostedServices
} }
} }
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
{
return invoice.GetPayments()
.Select(p => p.GetCryptoPaymentData() as BitcoinLikePaymentData)
.Where(p => p != null);
}
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId) async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>(); List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network) var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(t => t.Outpoint.Hash) .Select(p => p.Outpoint.Hash)
.ToArray()); .ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value)); var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(wallet.Network)) foreach (var payment in invoice.GetPayments(wallet.Network))
{ {
if (!transactions.TryGetValue(payment.Outpoint.Hash, out TransactionResult tx)) var paymentData = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
if (paymentData == null)
continue;
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
continue; continue;
var txId = tx.Transaction.GetHash(); var txId = tx.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId); var txConflict = conflicts.GetConflict(txId);
@ -228,27 +239,12 @@ namespace BTCPayServer.HostedServices
payment.Accounted = accounted; payment.Accounted = accounted;
} }
var bitcoinLike = payment.GetCryptoPaymentData() as BitcoinLikePaymentData; if (paymentData.ConfirmationCount != tx.Confirmations)
// Legacy
if (bitcoinLike == null)
{ {
#pragma warning disable CS0618 // Type or member is obsolete if(wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
payment.CryptoPaymentDataType = "BTCLike";
#pragma warning restore CS0618 // Type or member is obsolete
bitcoinLike = new BitcoinLikePaymentData();
bitcoinLike.ConfirmationCount = tx.Confirmations;
bitcoinLike.RBF = tx.Transaction.RBF;
payment.SetCryptoPaymentData(bitcoinLike);
updated = true;
}
if (bitcoinLike.ConfirmationCount != tx.Confirmations)
{
if(wallet.Network.MaxTrackedConfirmation >= bitcoinLike.ConfirmationCount)
{ {
bitcoinLike.ConfirmationCount = tx.Confirmations; paymentData.ConfirmationCount = tx.Confirmations;
payment.SetCryptoPaymentData(bitcoinLike); payment.SetCryptoPaymentData(paymentData);
updated = true; updated = true;
} }
} }
@ -328,7 +324,7 @@ namespace BTCPayServer.HostedServices
foreach (var invoiceId in invoices) foreach (var invoiceId in invoices)
{ {
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true); var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(network).Select(p => p.Outpoint)); var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
var strategy = invoice.GetDerivationStrategy(network); var strategy = invoice.GetDerivationStrategy(network);
if (strategy == null) if (strategy == null)
continue; continue;
@ -337,7 +333,9 @@ namespace BTCPayServer.HostedServices
.ToArray(); .ToArray();
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint))) foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{ {
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, coin.Coin, network.CryptoCode).ConfigureAwait(false); var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint); alreadyAccounted.Add(coin.Coin.Outpoint);
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy); invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++; totalPayment++;

View file

@ -22,7 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels
public class Payment public class Payment
{ {
public string CryptoCode { get; set; } public string CryptoCode { get; set; }
public int Confirmations public string Confirmations
{ {
get; set; get; set;
} }

View file

@ -537,7 +537,7 @@ namespace BTCPayServer.Services.Invoices
public BitcoinAddress GetDepositAddress() public BitcoinAddress GetDepositAddress()
{ {
if(string.IsNullOrEmpty(DepositAddress)) if (string.IsNullOrEmpty(DepositAddress))
{ {
return null; return null;
} }
@ -604,12 +604,14 @@ namespace BTCPayServer.Services.Invoices
{ {
get; set; get; set;
} }
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
public OutPoint Outpoint public OutPoint Outpoint
{ {
get; set; get; set;
} }
[Obsolete("Use GetValue() or GetScriptPubKey() instead")] [Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")]
public TxOut Output public TxOut Output
{ {
get; set; get; set;
@ -627,6 +629,7 @@ namespace BTCPayServer.Services.Invoices
get; set; get; set;
} }
[Obsolete("Use GetCryptoCode() instead")] [Obsolete("Use GetCryptoCode() instead")]
public string CryptoCode public string CryptoCode
{ {
@ -644,23 +647,38 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618 #pragma warning disable CS0618
if (string.IsNullOrEmpty(CryptoPaymentDataType)) if (string.IsNullOrEmpty(CryptoPaymentDataType))
{ {
return NullPaymentData.Instance; // In case this is a payment done before this update, consider it unconfirmed with RBF for safety
var paymentData = new BitcoinLikePaymentData();
paymentData.Outpoint = Outpoint;
paymentData.Output = Output;
paymentData.RBF = true;
paymentData.ConfirmationCount = 0;
paymentData.Legacy = true;
return paymentData;
} }
if (CryptoPaymentDataType == "BTCLike") if (CryptoPaymentDataType == "BTCLike")
{ {
return JsonConvert.DeserializeObject<BitcoinLikePaymentData>(CryptoPaymentData); var paymentData = JsonConvert.DeserializeObject<BitcoinLikePaymentData>(CryptoPaymentData);
// legacy
paymentData.Output = Output;
paymentData.Outpoint = Outpoint;
return paymentData;
} }
else
return NullPaymentData.Instance; throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData) public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData)
{ {
#pragma warning disable CS0618 #pragma warning disable CS0618
if (cryptoPaymentData is BitcoinLikePaymentData) if (cryptoPaymentData is BitcoinLikePaymentData paymentData)
{ {
CryptoPaymentDataType = "BTCLike"; CryptoPaymentDataType = "BTCLike";
// Legacy
Outpoint = paymentData.Outpoint;
Output = paymentData.Output;
///
} }
else else
throw new NotSupportedException(cryptoPaymentData.ToString()); throw new NotSupportedException(cryptoPaymentData.ToString());
@ -701,41 +719,60 @@ namespace BTCPayServer.Services.Invoices
public interface CryptoPaymentData 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();
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network); bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network); bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
} }
public class NullPaymentData : CryptoPaymentData
{
private static readonly NullPaymentData _Instance = new NullPaymentData();
public static NullPaymentData Instance
{
get
{
return _Instance;
}
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return false;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
return false;
}
}
public class BitcoinLikePaymentData : CryptoPaymentData public class BitcoinLikePaymentData : CryptoPaymentData
{ {
public BitcoinLikePaymentData()
{
}
public BitcoinLikePaymentData(Coin coin, bool rbf)
{
Outpoint = coin.Outpoint;
Output = coin.TxOut;
ConfirmationCount = 0;
RBF = rbf;
}
[JsonIgnore]
public OutPoint Outpoint { get; set; }
[JsonIgnore]
public TxOut Output { get; set; }
public int ConfirmationCount { get; set; } public int ConfirmationCount { get; set; }
public bool RBF { get; set; } public bool RBF { get; set; }
/// <summary>
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
/// </summary>
public bool Legacy { get; set; }
public string GetPaymentId()
{
return Outpoint.ToString();
}
public string[] GetSearchTerms()
{
return new[] { Outpoint.Hash.ToString() };
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network) public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{ {
return ConfirmationCount >= 6; return ConfirmationCount >= network.MaxTrackedConfirmation;
} }
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network) public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)

View file

@ -443,24 +443,24 @@ namespace BTCPayServer.Services.Invoices
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray()); AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
} }
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, Coin receivedCoin, string cryptoCode) public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode)
{ {
using (var context = _ContextFactory.CreateContext()) using (var context = _ContextFactory.CreateContext())
{ {
PaymentEntity entity = new PaymentEntity PaymentEntity entity = new PaymentEntity
{ {
Outpoint = receivedCoin.Outpoint,
#pragma warning disable CS0618 #pragma warning disable CS0618
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode, CryptoCode = cryptoCode,
#pragma warning restore CS0618 #pragma warning restore CS0618
ReceivedTime = date.UtcDateTime, ReceivedTime = date.UtcDateTime,
Accounted = false Accounted = false
}; };
entity.SetCryptoPaymentData(new BitcoinLikePaymentData()); entity.SetCryptoPaymentData(paymentData);
PaymentData data = new PaymentData PaymentData data = new PaymentData
{ {
Id = receivedCoin.Outpoint.ToString(), Id = paymentData.GetPaymentId(),
Blob = ToBytes(entity, null), Blob = ToBytes(entity, null),
InvoiceDataId = invoiceId, InvoiceDataId = invoiceId,
Accounted = false Accounted = false
@ -469,7 +469,7 @@ namespace BTCPayServer.Services.Invoices
context.Payments.Add(data); context.Payments.Add(data);
await context.SaveChangesAsync().ConfigureAwait(false); await context.SaveChangesAsync().ConfigureAwait(false);
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString()); AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
return entity; return entity;
} }
} }
@ -482,8 +482,9 @@ namespace BTCPayServer.Services.Invoices
{ {
foreach (var payment in payments) foreach (var payment in payments)
{ {
var paymentData = payment.GetCryptoPaymentData();
var data = new PaymentData(); var data = new PaymentData();
data.Id = payment.Outpoint.ToString(); data.Id = paymentData.GetPaymentId();
data.Accounted = payment.Accounted; data.Accounted = payment.Accounted;
data.Blob = ToBytes(payment, null); data.Blob = ToBytes(payment, null);
context.Attach(data); context.Attach(data);