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 KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 7;
public int MaxTrackedConfirmation { get; internal set; } = 6;
public override string ToString()
{

View file

@ -80,14 +80,34 @@ namespace BTCPayServer.Controllers
var payments = invoice
.GetPayments()
.Where(p => p.GetCryptoPaymentData() is BitcoinLikePaymentData)
.Select(async payment =>
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
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.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;

View file

@ -165,10 +165,11 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
{
var payment = invoice.GetPayments().FirstOrDefault(p => p.Outpoint == txCoin.Outpoint);
if (payment == null)
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
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);
}
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)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(invoice.GetPayments(wallet.Network)
.Select(t => t.Outpoint.Hash)
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(p => p.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
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;
var txId = tx.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
@ -228,27 +239,12 @@ namespace BTCPayServer.HostedServices
payment.Accounted = accounted;
}
var bitcoinLike = payment.GetCryptoPaymentData() as BitcoinLikePaymentData;
// Legacy
if (bitcoinLike == null)
if (paymentData.ConfirmationCount != tx.Confirmations)
{
#pragma warning disable CS0618 // Type or member is obsolete
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;
payment.SetCryptoPaymentData(bitcoinLike);
if(wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
{
paymentData.ConfirmationCount = tx.Confirmations;
payment.SetCryptoPaymentData(paymentData);
updated = true;
}
}
@ -328,7 +324,7 @@ namespace BTCPayServer.HostedServices
foreach (var invoiceId in invoices)
{
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);
if (strategy == null)
continue;
@ -337,7 +333,9 @@ namespace BTCPayServer.HostedServices
.ToArray();
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);
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++;

View file

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

View file

@ -537,7 +537,7 @@ namespace BTCPayServer.Services.Invoices
public BitcoinAddress GetDepositAddress()
{
if(string.IsNullOrEmpty(DepositAddress))
if (string.IsNullOrEmpty(DepositAddress))
{
return null;
}
@ -604,12 +604,14 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
public OutPoint Outpoint
{
get; set;
}
[Obsolete("Use GetValue() or GetScriptPubKey() instead")]
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")]
public TxOut Output
{
get; set;
@ -627,6 +629,7 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[Obsolete("Use GetCryptoCode() instead")]
public string CryptoCode
{
@ -644,23 +647,38 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
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")
{
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
}
public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData)
{
#pragma warning disable CS0618
if (cryptoPaymentData is BitcoinLikePaymentData)
if (cryptoPaymentData is BitcoinLikePaymentData paymentData)
{
CryptoPaymentDataType = "BTCLike";
// Legacy
Outpoint = paymentData.Outpoint;
Output = paymentData.Output;
///
}
else
throw new NotSupportedException(cryptoPaymentData.ToString());
@ -701,41 +719,60 @@ namespace BTCPayServer.Services.Invoices
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 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 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 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)
{
return ConfirmationCount >= 6;
return ConfirmationCount >= network.MaxTrackedConfirmation;
}
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());
}
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())
{
PaymentEntity entity = new PaymentEntity
{
Outpoint = receivedCoin.Outpoint,
#pragma warning disable CS0618
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode,
#pragma warning restore CS0618
ReceivedTime = date.UtcDateTime,
Accounted = false
};
entity.SetCryptoPaymentData(new BitcoinLikePaymentData());
entity.SetCryptoPaymentData(paymentData);
PaymentData data = new PaymentData
{
Id = receivedCoin.Outpoint.ToString(),
Id = paymentData.GetPaymentId(),
Blob = ToBytes(entity, null),
InvoiceDataId = invoiceId,
Accounted = false
@ -469,7 +469,7 @@ namespace BTCPayServer.Services.Invoices
context.Payments.Add(data);
await context.SaveChangesAsync().ConfigureAwait(false);
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString());
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
return entity;
}
}
@ -482,8 +482,9 @@ namespace BTCPayServer.Services.Invoices
{
foreach (var payment in payments)
{
var paymentData = payment.GetCryptoPaymentData();
var data = new PaymentData();
data.Id = payment.Outpoint.ToString();
data.Id = paymentData.GetPaymentId();
data.Accounted = payment.Accounted;
data.Blob = ToBytes(payment, null);
context.Attach(data);