Big refactorying for supporting multi currencies

This commit is contained in:
nicolas.dorier 2017-12-21 15:52:04 +09:00
parent d5ef36fe50
commit a37fdde214
23 changed files with 1201 additions and 240 deletions

View file

@ -38,33 +38,38 @@ namespace BTCPayServer.Tests
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
var cryptoData = entity.GetCryptoData("BTC");
Assert.NotNull(cryptoData); // Should use legacy data to build itself
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Coins(1.1m), cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), cryptoData.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Coins(0.7m), cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.2m), cryptoData.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Coins(0.6m), cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Zero, cryptoData.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), cryptoData.GetTotalCryptoDue());
#pragma warning restore CS0618
}
[Fact]
@ -312,26 +317,26 @@ namespace BTCPayServer.Tests
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
Assert.Single(textSearchResult);
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);
@ -348,7 +353,7 @@ namespace BTCPayServer.Tests
cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length);
Assert.Single(invoiceEntity.HistoricalAddresses);
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
Money secondPayment = Money.Zero;
@ -360,7 +365,7 @@ namespace BTCPayServer.Tests
Assert.Equal("new", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));

View file

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public class BTCPayNetwork
{
public Network NBitcoinNetwork { get; set; }
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public class BTCPayNetworkProvider
{
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
public BTCPayNetworkProvider(Network network)
{
if(network == Network.Main)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.Main,
UriScheme = "bitcoin"
});
}
if (network == Network.TestNet)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.TestNet,
UriScheme = "bitcoin"
});
}
if (network == Network.RegTest)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.RegTest,
UriScheme = "bitcoin"
});
}
}
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode, network);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
return network;
}
}
}

View file

@ -43,10 +43,10 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
BTCPayServerOptions options,
IServer server,
Network network)
BTCPayNetworkProvider networkProvider)
{
_Settings = repo;
_Network = network;
_Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork;
_Explorer = explorer;
_Options = options;
_EventAggregator = eventAggregator;

View file

@ -24,16 +24,19 @@ namespace BTCPayServer.Controllers
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository)
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
this._NetworkProvider = networkProvider;
}
[HttpPost]
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO();
var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp);
}
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
return DataWrapper.Create(entities);
}

View file

@ -17,21 +17,25 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
return NotFound();
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);

View file

@ -26,7 +26,7 @@ namespace BTCPayServer.Controllers
{
[HttpPost]
[Route("invoices/{invoiceId}")]
public IActionResult Invoice(string invoiceId, string command)
public IActionResult Invoice(string invoiceId, string command, string cryptoCode = null)
{
if (command == "refresh")
{
@ -41,8 +41,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId)
public async Task<IActionResult> Invoice(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
@ -51,7 +53,14 @@ namespace BTCPayServer.Controllers
if (invoice == null)
return NotFound();
var dto = invoice.EntityToDTO();
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || !invoice.Support(network))
return NotFound();
var cryptoData = invoice.GetCryptoData(network);
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
@ -64,16 +73,16 @@ namespace BTCPayServer.Controllers
ExpirationDate = invoice.ExpirationTime,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Rate = invoice.Rate,
Rate = cryptoData.Rate,
Fiat = dto.Price + " " + dto.Currency,
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
BTC = cryptoData.GetTotalCryptoDue().ToString() + $" {network.CryptoCode}",
BTCDue = cryptoData.GetCryptoDue().ToString() + $" {network.CryptoCode}",
BTCPaid = cryptoData.GetTotalPaid().ToString() + $" {network.CryptoCode}",
NetworkFee = cryptoData.GetNetworkFee().ToString() + $" {network.CryptoCode}",
NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation,
BitcoinAddress = invoice.DepositAddress,
PaymentUrl = dto.PaymentUrls.BIP72
BitcoinAddress = BitcoinAddress.Create(cryptoInfo.Address, network.NBitcoinNetwork),
PaymentUrl = cryptoInfo.PaymentUrls.BIP72
};
var payments = invoice
@ -81,11 +90,11 @@ namespace BTCPayServer.Controllers
.Select(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(network.NBitcoinNetwork);
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
m.TransactionLink = string.Format(network.BlockExplorerLink, m.TransactionId);
return m;
})
.ToArray();
@ -100,48 +109,54 @@ namespace BTCPayServer.Controllers
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId);
var model = await GetInvoiceModel(invoiceId, cryptoCode);
if (model == null)
return NotFound();
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || network == null || !invoice.Support(network))
return null;
var cryptoData = invoice.GetCryptoData(network);
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var currency = invoice.ProductInformation.Currency;
var model = new PaymentModel()
{
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BtcAddress = invoice.DepositAddress.ToString(),
BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
BtcTotalDue = invoice.GetTotalCryptoDue().ToString(),
BtcDue = invoice.GetCryptoDue().ToString(),
BtcAddress = cryptoData.DepositAddress,
BtcAmount = (cryptoData.GetTotalCryptoDue() - cryptoData.TxFee).ToString(),
BtcTotalDue = cryptoData.GetTotalCryptoDue().ToString(),
BtcDue = cryptoData.GetCryptoDue().ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
Rate = cryptoData.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.GetTxCount(),
BtcPaid = invoice.GetTotalPaid().ToString(),
TxFees = cryptoData.TxFee.ToString(),
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP72,
TxCount = cryptoData.GetTxCount(),
BtcPaid = cryptoData.GetTotalPaid().ToString(),
Status = invoice.Status
};
@ -163,9 +178,9 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId)
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
{
var model = await GetInvoiceModel(invoiceId);
var model = await GetInvoiceModel(invoiceId, cryptoCode);
if (model == null)
return NotFound();
return Json(model);

View file

@ -48,15 +48,13 @@ namespace BTCPayServer.Controllers
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
IFeeProviderFactory _FeeProviderFactory;
private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer;
EventAggregator _EventAggregator;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider _NetworkProvider;
public InvoiceController(InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
@ -65,31 +63,32 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator,
InvoiceWatcherAccessor watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
BTCPayNetworkProvider networkProvider,
IFeeProviderFactory feeProviderFactory)
{
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
{
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
var derivationStrategy = store.DerivationStrategy;
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
};
var storeBlob = store.GetStoreBlob(_Network);
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
@ -114,16 +113,45 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate;
var queries = storeBlob.GetSupportedCryptoCurrencies()
.Select(n => _NetworkProvider.GetNetwork(n))
.Where(n => n != null)
.Select(network =>
{
return new
{
network = network,
getFeeRate = _FeeProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
getRate = _RateProvider.GetRateAsync(invoice.Currency),
getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy, network))
};
});
var cryptoDatas = new Dictionary<string, CryptoData>();
foreach (var q in queries)
{
CryptoData cryptoData = new CryptoData();
cryptoData.CryptoCode = q.network.CryptoCode;
cryptoData.FeeRate = (await q.getFeeRate);
cryptoData.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : cryptoData.FeeRate.GetFee(100); // assume price for 100 bytes
cryptoData.Rate = await q.getRate;
cryptoData.DepositAddress = (await q.getAddress).ToString();
#pragma warning disable CS0618
if (q.network.CryptoCode == "BTC")
{
entity.TxFee = cryptoData.TxFee;
entity.Rate = cryptoData.Rate;
entity.DepositAddress = cryptoData.DepositAddress;
}
#pragma warning restore CS0618
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
}
entity.SetCryptoData(cryptoDatas);
entity.PosData = invoice.PosData;
entity.DepositAddress = await getAddress;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_Watcher.Watch(entity.Id);
var resp = entity.EntityToDTO();
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -155,9 +183,9 @@ namespace BTCPayServer.Controllers
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)

View file

@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
Network network,
BTCPayNetworkProvider networkProvider,
IHostingEnvironment env)
{
_Repo = repo;
@ -43,7 +43,7 @@ namespace BTCPayServer.Controllers
_TokenController = tokenController;
_Wallet = wallet;
_Env = env;
_Network = network;
_Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork;
_CallbackController = callbackController;
}
Network _Network;
@ -145,7 +145,7 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob(_Network);
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
@ -210,11 +210,11 @@ namespace BTCPayServer.Controllers
}
}
var blob = store.GetStoreBlob(_Network);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
if (store.SetStoreBlob(blob, _Network))
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}

View file

@ -17,6 +17,17 @@ namespace BTCPayServer.Data
get; set;
}
[Obsolete("Use GetCryptoCode instead")]
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
}
#pragma warning restore CS0618
public DateTimeOffset Assigned
{
get; set;

View file

@ -62,15 +62,17 @@ namespace BTCPayServer.Data
set;
}
public StoreBlob GetStoreBlob(Network network)
static Network Dummy = Network.Main;
public StoreBlob GetStoreBlob()
{
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
}
public bool SetStoreBlob(StoreBlob storeBlob, Network network)
public bool SetStoreBlob(StoreBlob storeBlob)
{
var original = new Serializer(network).ToString(GetStoreBlob(network));
var newBlob = new Serializer(network).ToString(storeBlob);
var original = new Serializer(Dummy).ToString(GetStoreBlob());
var newBlob = new Serializer(Dummy).ToString(storeBlob);
if (original == newBlob)
return false;
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
@ -95,5 +97,19 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetSupportedCryptoCurrencies() instead")]
public string[] SupportedCryptoCurrencies { get; set; }
public string[] GetSupportedCryptoCurrencies()
{
#pragma warning disable CS0618
if(SupportedCryptoCurrencies == null)
{
return new string[] { "BTC" };
}
return SupportedCryptoCurrencies;
#pragma warning restore CS0618
}
}
}

View file

@ -107,7 +107,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -125,14 +124,20 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
return new BTCPayNetworkProvider(opts.Network);
});
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClient>())
{
Fallback = new FeeRate(100, 1),
BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>()
BlockTarget = 20
});
services.TryAddSingleton<NBXplorerWaiterAccessor>();

View file

@ -0,0 +1,481 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20171221054550_AltcoinSupport")]
partial class AltcoinSupport
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class AltcoinSupport : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CryptoCode",
table: "HistoricalAddressInvoices",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CryptoCode",
table: "HistoricalAddressInvoices");
}
}
}

View file

@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -44,6 +44,8 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
@ -382,7 +384,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany()
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});

View file

@ -36,11 +36,56 @@ namespace BTCPayServer.Models
}
}
public class InvoiceCryptoInfo
{
[JsonProperty("cryptoCode")]
public string CryptoCode { get; set; }
[JsonProperty("rate")]
public decimal Rate { get; set; }
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
public Dictionary<string, double> ExRates
{
get; set;
}
//"btcPaid":"0.000000"
[JsonProperty("paid")]
public string Paid
{
get; set;
}
//"btcPrice":"0.001157"
[JsonProperty("price")]
public string Price
{
get; set;
}
//"btcDue":"0.001160"
[JsonProperty("due")]
public string Due
{
get; set;
}
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
}
public string Address { get; set; }
public string Url { get; set; }
}
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
[Obsolete("Use CryptoInfo.Url instead")]
public string Url
{
get; set;
@ -59,6 +104,7 @@ namespace BTCPayServer.Models
}
//"btcPrice":"0.001157"
[JsonProperty("btcPrice")]
[Obsolete("Use CryptoInfo.Price instead")]
public string BTCPrice
{
get; set;
@ -66,11 +112,15 @@ namespace BTCPayServer.Models
//"btcDue":"0.001160"
[JsonProperty("btcDue")]
[Obsolete("Use CryptoInfo.Due instead")]
public string BTCDue
{
get; set;
}
[JsonProperty("cryptoInfo")]
public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
//"price":5
[JsonProperty("price")]
public double Price
@ -87,6 +137,7 @@ namespace BTCPayServer.Models
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
[Obsolete("Use CryptoInfo.ExRates instead")]
public Dictionary<string, double> ExRates
{
get; set;
@ -156,6 +207,7 @@ namespace BTCPayServer.Models
//"btcPaid":"0.000000"
[JsonProperty("btcPaid")]
[Obsolete("Use CryptoInfo.Paid instead")]
public string BTCPaid
{
get; set;
@ -163,7 +215,8 @@ namespace BTCPayServer.Models
//"rate":4320.02
[JsonProperty("rate")]
public double Rate
[Obsolete("Use CryptoInfo.Rate instead")]
public decimal Rate
{
get; set;
}
@ -178,6 +231,7 @@ namespace BTCPayServer.Models
//"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"}
[JsonProperty("paymentUrls")]
[Obsolete("Use CryptoInfo.PaymentsUrls instead")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
@ -197,6 +251,7 @@ namespace BTCPayServer.Models
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
[JsonProperty("bitcoinAddress")]
[Obsolete("Use CryptoInfo.Address instead")]
public string BitcoinAddress
{
get; set;

View file

@ -92,7 +92,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public double Rate
public decimal Rate
{
get;
internal set;

View file

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IFeeProviderFactory
{
IFeeProvider CreateFeeProvider(BTCPayNetwork network);
}
}

View file

@ -8,29 +8,49 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Fees
{
public class NBXplorerFeeProvider : IFeeProvider
public class NBXplorerFeeProviderFactory : IFeeProviderFactory
{
public NBXplorerFeeProviderFactory(ExplorerClient explorerClient)
{
if (explorerClient == null)
throw new ArgumentNullException(nameof(explorerClient));
_ExplorerClient = explorerClient;
}
private readonly ExplorerClient _ExplorerClient;
public ExplorerClient ExplorerClient
{
get; set;
get
{
return _ExplorerClient;
}
}
public FeeRate Fallback
public FeeRate Fallback { get; set; }
public int BlockTarget { get; set; }
public IFeeProvider CreateFeeProvider(BTCPayNetwork network)
{
get; set;
return new NBXplorerFeeProvider(this);
}
public int BlockTarget
}
public class NBXplorerFeeProvider : IFeeProvider
{
public NBXplorerFeeProvider(NBXplorerFeeProviderFactory factory)
{
get; set;
if (factory == null)
throw new ArgumentNullException(nameof(factory));
_Factory = factory;
}
private readonly NBXplorerFeeProviderFactory _Factory;
public async Task<FeeRate> GetFeeRateAsync()
{
try
{
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
return (await _Factory.ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
}
catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
{
return Fallback;
return _Factory.Fallback;
}
}
}

View file

@ -10,6 +10,7 @@ using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
using NBXplorer;
namespace BTCPayServer.Services.Invoices
{
@ -82,7 +83,7 @@ namespace BTCPayServer.Services.Invoices
}
[JsonProperty(PropertyName = "price")]
public double Price
public decimal Price
{
get; set;
}
@ -111,65 +112,17 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public int GetTxCount()
{
return Calculate().TxCount;
}
public string OrderId
{
get; set;
}
public Money GetTotalCryptoDue()
{
return Calculate().TotalDue;
}
private (Money TotalDue, Money Paid, int TxCount) Calculate()
{
var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee;
var paid = Money.Zero;
int txCount = 1;
var payments =
Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
paid += _.Output.Value;
return _;
})
.TakeWhile(_ =>
{
var paidEnough = totalDue <= paid;
if (!paidEnough)
{
txCount++;
totalDue += TxFee;
}
return !paidEnough;
})
.ToArray();
return (totalDue, paid, txCount);
}
public Money GetTotalPaid()
{
return Calculate().Paid;
}
public Money GetCryptoDue()
{
var o = Calculate();
var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
public double Rate
[Obsolete("Use GetCryptoData(network).Rate instead")]
public decimal Rate
{
get; set;
}
@ -181,7 +134,9 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
public BitcoinAddress DepositAddress
[Obsolete("Use GetCryptoData(network).DepositAddress instead")]
public string DepositAddress
{
get; set;
}
@ -231,6 +186,8 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use GetCryptoData(network).TxFee instead")]
public Money TxFee
{
get;
@ -251,6 +208,10 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use Set/GetCryptoData() instead")]
public JObject CryptoData { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{
@ -275,7 +236,7 @@ namespace BTCPayServer.Services.Invoices
}
public InvoiceResponse EntityToDTO()
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
{
ServerUrl = ServerUrl ?? "";
InvoiceResponse dto = new InvoiceResponse
@ -286,34 +247,62 @@ namespace BTCPayServer.Services.Invoices
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime,
BTCPrice = Money.Coins((decimal)(ProductInformation.Price / Rate)).ToString(),
Status = Status,
Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id,
Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable },
Rate = Rate
Flags = new Flags() { Refundable = Refundable }
};
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
foreach (var info in this.GetCryptoData().Values)
{
var cryptoInfo = new InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.CryptoCode;
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
cryptoInfo.Due = info.GetCryptoDue().ToString();
var paid = Payments.Where(p => p.Accounted && p.GetCryptoCode() == info.CryptoCode).Select(p => p.GetValue()).Sum();
cryptoInfo.Paid = paid.ToString();
cryptoInfo.Address = info.DepositAddress;
cryptoInfo.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
};
var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC";
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
{
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
#pragma warning disable CS0618
if (info.CryptoCode == "BTC")
{
dto.Url = cryptoInfo.Url;
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);
}
Populate(ProductInformation, dto);
Populate(BuyerInformation, dto);
dto.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, Rate }
};
dto.PaymentUrls = new InvoicePaymentUrls()
{
BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP72b = $"bitcoin:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}"),
BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}",
};
dto.BitcoinAddress = DepositAddress.ToString();
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
var paid = Payments.Where(p => p.Accounted).Select(p => p.Output.Value).Sum();
dto.BTCPaid = paid.ToString();
dto.BTCDue = GetCryptoDue().ToString();
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
return dto;
}
@ -324,11 +313,135 @@ namespace BTCPayServer.Services.Invoices
JsonConvert.PopulateObject(str, dest);
}
internal bool Support(BTCPayNetwork network)
{
var rates = GetCryptoData();
return rates.TryGetValue(network.CryptoCode, out var data);
}
public CryptoData GetCryptoData(string cryptoCode)
{
GetCryptoData().TryGetValue(cryptoCode, out var data);
return data;
}
public CryptoData GetCryptoData(BTCPayNetwork network)
{
GetCryptoData().TryGetValue(network.CryptoCode, out var data);
return data;
}
public Dictionary<string, CryptoData> GetCryptoData()
{
Dictionary<string, CryptoData> rates = new Dictionary<string, CryptoData>();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
// Legacy
if (Rate != 0.0m)
{
rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress });
}
if (CryptoData != null)
{
foreach (var prop in CryptoData.Properties())
{
var r = serializer.ToObject<CryptoData>(prop.Value.ToString());
r.CryptoCode = prop.Name;
r.ParentEntity = this;
rates.TryAdd(r.CryptoCode, r);
}
}
#pragma warning restore CS0618
return rates;
}
Network Dummy = Network.Main;
public void SetCryptoData(Dictionary<string, CryptoData> cryptoData)
{
var obj = new JObject();
var serializer = new Serializer(Dummy);
foreach (var kv in cryptoData)
{
var clone = serializer.ToObject<CryptoData>(serializer.ToString(kv.Value));
clone.CryptoCode = null;
obj.Add(new JProperty(kv.Key, JObject.Parse(serializer.ToString(clone))));
}
#pragma warning disable CS0618
CryptoData = obj;
#pragma warning restore CS0618
}
}
public class CryptoData
{
[JsonIgnore]
public InvoiceEntity ParentEntity { get; set; }
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string CryptoCode { get; set; }
[JsonProperty(PropertyName = "rate")]
public decimal Rate { get; set; }
[JsonProperty(PropertyName = "feeRate")]
public FeeRate FeeRate { get; set; }
[JsonProperty(PropertyName = "txFee")]
public Money TxFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; }
public Money GetNetworkFee()
{
var item = Calculate();
return TxFee * item.TxCount;
}
public int GetTxCount()
{
return Calculate().TxCount;
}
public Money GetTotalCryptoDue()
{
return Calculate().TotalDue;
}
private (Money TotalDue, Money Paid, int TxCount) Calculate()
{
var cryptoData = ParentEntity.GetCryptoData();
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate) + TxFee;
var paid = Money.Zero;
int txCount = 1;
var payments =
ParentEntity.Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
paid += _.GetValue(cryptoData, CryptoCode);
return _;
})
.TakeWhile(_ =>
{
var paidEnough = totalDue <= paid;
if (!paidEnough && _.GetCryptoCode() == CryptoCode)
{
txCount++;
totalDue += TxFee;
}
return !paidEnough;
})
.ToArray();
return (totalDue, paid, txCount);
}
public Money GetTotalPaid()
{
return Calculate().Paid;
}
public Money GetCryptoDue()
{
var o = Calculate();
var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v;
}
}
public class AccountedPaymentEntity
@ -352,13 +465,59 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use GetValue() or GetScriptPubKey() instead")]
public TxOut Output
{
get; set;
}
public Script GetScriptPubKey()
{
#pragma warning disable CS0618
return Output.ScriptPubKey;
#pragma warning restore CS0618
}
public bool Accounted
{
get; set;
}
[Obsolete("Use GetCryptoCode() instead")]
public string CryptoCode
{
get;
set;
}
public Money GetValue()
{
#pragma warning disable CS0618
return Output.Value;
#pragma warning restore CS0618
}
public Money GetValue(Dictionary<string, CryptoData> cryptoData, string cryptoCode)
{
#pragma warning disable CS0618
var to = cryptoCode;
var from = GetCryptoCode();
if (to == from)
return Output.Value;
var fromRate = cryptoData[from].Rate;
var toRate = cryptoData[to].Rate;
var fiatValue = fromRate * Output.Value.ToDecimal(MoneyUnit.BTC);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return Money.Coins(otherCurrencyValue);
#pragma warning restore CS0618
}
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
}

View file

@ -45,17 +45,20 @@ namespace BTCPayServer.Services.Invoices
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
}
async Task Notify(InvoiceEntity invoice)
@ -110,19 +113,15 @@ namespace BTCPayServer.Services.Invoices
}
}
private static async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Url = dto.Url,
BTCDue = dto.BTCDue,
BTCPaid = dto.BTCPaid,
BTCPrice = dto.BTCPrice,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
@ -130,10 +129,23 @@ namespace BTCPayServer.Services.Invoices
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Rate = dto.Rate,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
if(btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);

View file

@ -45,7 +45,7 @@ namespace BTCPayServer.Services.Invoices
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
@ -102,8 +102,9 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
invoice.Payments = new List<PaymentEntity>();
@ -121,52 +122,77 @@ namespace BTCPayServer.Services.Invoices
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
});
context.AddressInvoices.Add(new AddressInvoiceData()
foreach (var cryptoData in invoice.GetCryptoData().Values)
{
Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(),
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
});
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Address = invoice.DepositAddress.ToString(),
Assigned = DateTimeOffset.UtcNow
});
var network = networkProvider.GetNetwork(cryptoData.CryptoCode);
if (network == null)
throw new InvalidOperationException("CryptoCode unsupported");
context.AddressInvoices.Add(new AddressInvoiceData()
{
Address = BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(),
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
});
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Address = cryptoData.DepositAddress,
#pragma warning disable CS0618
CryptoCode = cryptoData.CryptoCode,
#pragma warning restore CS0618
Assigned = DateTimeOffset.UtcNow
});
textSearch.Add(cryptoData.DepositAddress);
textSearch.Add(cryptoData.GetTotalCryptoDue().ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
await context.SaveChangesAsync().ConfigureAwait(false);
}
AddToTextSearch(invoice.Id,
invoice.Id,
invoice.DepositAddress.ToString(),
invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture),
invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture),
invoice.GetTotalCryptoDue().ToString(),
invoice.OrderId,
ToString(invoice.BuyerInformation),
ToString(invoice.ProductInformation),
invoice.StoreId
);
textSearch.Add(invoice.Id);
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.OrderId);
textSearch.Add(ToString(invoice.BuyerInformation));
textSearch.Add(ToString(invoice.ProductInformation));
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress)
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network)
{
using (var context = _ContextFactory.CreateContext())
{
var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId);
if (invoice == null)
return false;
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob);
var old = invoiceEntity.DepositAddress;
invoiceEntity.DepositAddress = bitcoinAddress;
invoice.Blob = ToBytes(invoiceEntity);
if (old != null)
var cryptoData = invoiceEntity.GetCryptoData();
var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault();
if (currencyData == null)
return false;
if (currencyData.DepositAddress != null)
{
MarkUnassigned(invoiceId, old, context);
MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode);
}
currencyData.DepositAddress = bitcoinAddress.ToString();
#pragma warning disable CS0618
if (network.CryptoCode == "BTC")
{
invoiceEntity.DepositAddress = currencyData.DepositAddress;
}
#pragma warning disable CS0618
invoiceEntity.SetCryptoData(cryptoData);
invoice.Blob = ToBytes(invoiceEntity);
context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow });
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
@ -181,14 +207,19 @@ namespace BTCPayServer.Services.Invoices
}
}
private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context)
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
{
var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId;
historical.Address = old.ToString();
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
foreach (var address in entity.GetCryptoData())
{
if (cryptoCode != null && cryptoCode != address.Value.CryptoCode)
continue;
var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId;
historical.Address = address.Value.DepositAddress;
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
}
}
public async Task UnaffectAddress(string invoiceId)
@ -199,9 +230,7 @@ namespace BTCPayServer.Services.Invoices
if (invoiceData == null)
return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
if (invoiceEntity.DepositAddress == null)
return;
MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context);
MarkUnassigned(invoiceId, invoiceEntity, context, null);
try
{
await context.SaveChangesAsync();
@ -223,7 +252,7 @@ namespace BTCPayServer.Services.Invoices
void AddToTextSearch(string invoiceId, params string[] terms)
{
_IndexerThread.DoAsync(() =>
_IndexerThread.DoAsync(() =>
{
using (var tx = _Engine.GetTransaction())
{
@ -284,7 +313,7 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
entity.Payments = invoice.Payments.Select(p =>
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
paymentEntity.Accounted = p.Accounted;

View file

@ -29,9 +29,10 @@ namespace BTCPayServer.Services.Invoices
DerivationStrategyFactory _DerivationFactory;
EventAggregator _EventAggregator;
BTCPayWallet _Wallet;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(ExplorerClient explorerClient,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWallet wallet,
@ -44,6 +45,7 @@ namespace BTCPayServer.Services.Invoices
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
accessor.Instance = this;
}
CompositeDisposable leases = new CompositeDisposable();
@ -126,6 +128,7 @@ namespace BTCPayServer.Services.Invoices
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
List<Coin> receivedCoins = new List<Coin>();
foreach (var received in utxos)
@ -142,7 +145,9 @@ namespace BTCPayServer.Services.Invoices
dirtyAddress = true;
}
//////
var network = _NetworkProvider.GetNetwork("BTC");
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
@ -154,8 +159,8 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.Output.Value).Sum();
if (totalPaid >= invoice.GetTotalCryptoDue())
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= cryptoData.GetTotalCryptoDue())
{
if (invoice.Status == "new")
{
@ -172,23 +177,23 @@ namespace BTCPayServer.Services.Invoices
}
}
if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
if (totalPaid > cryptoData.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
}
if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
if (totalPaid < cryptoData.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress);
Logs.PayServer.LogInformation("Paid to " + cryptoData.DepositAddress);
invoice.ExceptionStatus = "paidPartial";
needSave = true;
if (dirtyAddress)
{
var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy));
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
@ -210,13 +215,13 @@ namespace BTCPayServer.Services.Invoices
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum();
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(chainTotalConfirmed < invoice.GetTotalCryptoDue()))
(chainTotalConfirmed < cryptoData.GetTotalCryptoDue()))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid")));
@ -225,8 +230,8 @@ namespace BTCPayServer.Services.Invoices
}
else
{
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= cryptoData.GetTotalCryptoDue())
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed")));
@ -240,8 +245,8 @@ namespace BTCPayServer.Services.Invoices
{
var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= cryptoData.GetTotalCryptoDue())
{
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete")));
invoice.Status = "complete";