Compare commits

...

12 Commits

34 changed files with 1914 additions and 341 deletions

View File

@ -121,6 +121,11 @@ namespace BTCPayServer.Tests
internal set;
}
public T GetService<T>()
{
return _Host.Services.GetRequiredService<T>();
}
public T GetController<T>(string userId = null) where T : Controller
{
var context = new DefaultHttpContext();

View File

@ -17,333 +17,377 @@ using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Authentication;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Extensions;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Tests
{
public class UnitTest1
{
public UnitTest1(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
public class UnitTest1
{
public UnitTest1(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
[Fact]
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
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), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()) });
//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());
//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());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()) });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
}
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
}
[Fact]
public void CanPayUsingBIP70()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
[Fact]
public void CanPayUsingBIP70()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.False(invoice.Refundable);
Assert.False(invoice.Refundable);
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
var request = url.GetPaymentRequest();
var payment = request.CreatePayment();
var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72);
var request = url.GetPaymentRequest();
var payment = request.CreatePayment();
Transaction tx = new Transaction();
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
var cashCow = tester.ExplorerNode;
tx = cashCow.FundRawTransaction(tx).Transaction;
tx = cashCow.SignRawTransaction(tx);
Transaction tx = new Transaction();
tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script)));
var cashCow = tester.ExplorerNode;
tx = cashCow.FundRawTransaction(tx).Transaction;
tx = cashCow.SignRawTransaction(tx);
payment.Transactions.Add(tx);
payment.Transactions.Add(tx);
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
var ack = payment.SubmitPayment();
Assert.NotNull(ack);
payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey));
var ack = payment.SubmitPayment();
Assert.NotNull(ack);
Eventually(() =>
{
tester.SimulateCallback(url.Address);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable);
});
}
}
Eventually(() =>
{
tester.SimulateCallback(url.Address);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable);
});
}
}
[Fact]
public void CanUseServerInitiatedPairingCode()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
[Fact]
public void CanUseServerInitiatedPairingCode()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.Register();
acc.CreateStore();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "bla",
PublicKey = null
}).GetAwaiter().GetResult();
var controller = tester.PayTester.GetController<StoresController>(acc.UserId);
var token = (RedirectToActionResult)controller.CreateToken(acc.StoreId, new Models.StoreViewModels.CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "bla",
PublicKey = null
}).GetAwaiter().GetResult();
var pairingCode = (string)token.RouteValues["pairingCode"];
var pairingCode = (string)token.RouteValues["pairingCode"];
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
}
}
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
}
}
[Fact]
public void CanSendIPN()
{
using (var callbackServer = new CustomServer())
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000);
tester.SimulateCallback(url.Address);
callbackServer.ProcessNextRequest((ctx) =>
{
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
});
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
}
}
[Fact]
public void CanSendIPN()
{
using(var callbackServer = new CustomServer())
{
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
Price = 5.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
Thread.Sleep(5000);
tester.SimulateCallback(url.Address);
callbackServer.ProcessNextRequest((ctx) =>
{
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
});
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
Assert.NotNull(invoice2);
}
}
}
[Fact]
public void CantPairTwiceWithSamePubkey()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.Register();
var store = acc.CreateStore();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
[Fact]
public void CantPairTwiceWithSamePubkey()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.Register();
var store = acc.CreateStore();
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
}
}
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
Assert.Equal(1, textSearchResult.Length);
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);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
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.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.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);
var firstPayment = Money.Coins(0.04m);
var firstPayment = Money.Coins(0.04m);
var txFee = Money.Zero;
var txFee = Money.Zero;
var rate = user.BitPay.GetRates();
var rate = user.BitPay.GetRates();
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
cashCow.SendToAddress(invoiceAddress, firstPayment);
var cashCow = tester.ExplorerNode;
Money secondPayment = Money.Zero;
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var iii = ctx.AddressInvoices.ToArray();
Assert.True(IsMapped(invoice, ctx));
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
secondPayment = localInvoice.BtcDue;
});
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length);
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
cashCow.SendToAddress(invoiceAddress, secondPayment);
Money secondPayment = Money.Zero;
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
});
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == invoice.BitcoinAddress.ToString());
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == localInvoice.BitcoinAddress.ToString());
Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
});
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
cashCow.SendToAddress(invoiceAddress, secondPayment);
cashCow.Generate(5); //Now should be complete
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
Assert.True(IsMapped(localInvoice, ctx));
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
});
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
});
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
cashCow.Generate(5); //Now should be complete
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
});
cashCow.Generate(1);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
}
}
cashCow.SendToAddress(invoiceAddress, invoice.BtcDue + Money.Coins(1));
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
while (true)
{
try
{
act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
}
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
cashCow.Generate(1);
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal("paidOver", (string)((JValue)localInvoice.ExceptionStatus).Value);
});
}
}
[Fact]
public void CheckRatesProvider()
{
var coinAverage = new CoinAverageRateProvider();
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
cached.CacheSpan = TimeSpan.FromSeconds(10);
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.Address == h) != null;
}
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
while(true)
{
try
{
act();
break;
}
catch(XunitException) when(!cts.Token.IsCancellationRequested)
{
cts.Token.WaitHandle.WaitOne(500);
}
}
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.22</Version>
<Version>1.0.0.25</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />

View File

@ -107,13 +107,12 @@ namespace BTCPayServer.Controllers
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
entity.TxFee = (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await _FeeProvider.GetFeeRateAsync()).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await _RateProvider.GetRateAsync(invoice.Currency);
entity.PosData = invoice.PosData;
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
await _Watcher.WatchAsync(entity.Id);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };

View File

@ -108,6 +108,34 @@ namespace BTCPayServer.Controllers
return View(result);
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if(store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
}
[HttpGet]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId)
@ -119,6 +147,7 @@ namespace BTCPayServer.Controllers
var vm = new StoreViewModel();
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.DerivationScheme = store.DerivationStrategy;
vm.StatusMessage = StatusMessage;
@ -173,6 +202,14 @@ namespace BTCPayServer.Controllers
}
}
if(store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
{
var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee;
store.SetStoreBlob(blob, _Network);
needUpdate = true;
}
if(needUpdate)
{
await _Repo.UpdateStore(store);

View File

@ -21,5 +21,10 @@ namespace BTCPayServer.Data
{
get; set;
}
public DateTimeOffset? CreatedTime
{
get; set;
}
}
}

View File

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
}
public DbSet<PendingInvoiceData> PendingInvoices
{
get; set;
@ -118,6 +123,13 @@ namespace BTCPayServer.Data
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
builder.Entity<HistoricalAddressInvoiceData>()
.HasKey(o => new
{
o.InvoiceDataId,
o.Address
});
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class HistoricalAddressInvoiceData
{
public string InvoiceDataId
{
get; set;
}
public string Address
{
get; set;
}
public DateTimeOffset Assigned
{
get; set;
}
public DateTimeOffset? UnAssigned
{
get; set;
}
}
}

View File

@ -37,6 +37,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
}
public byte[] Blob
{
get; set;

View File

@ -1,9 +1,12 @@
using BTCPayServer.Models;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using NBXplorer;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Data
@ -51,5 +54,28 @@ namespace BTCPayServer.Data
{
get; set;
}
public byte[] StoreBlob
{
get;
set;
}
public StoreBlob GetStoreBlob(Network network)
{
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
}
public void SetStoreBlob(StoreBlob storeBlob, Network network)
{
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob));
}
}
public class StoreBlob
{
public bool NetworkFeeDisabled
{
get; set;
}
}
}

View File

@ -49,10 +49,10 @@ namespace BTCPayServer
}
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
public static HtmlString ToJson(this object o)
public static string ToJson(this object o)
{
var res = JsonConvert.SerializeObject(o, Formatting.None, jsonSettings);
return new HtmlString(res);
return res;
}
}
}

View File

@ -33,6 +33,7 @@ using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Hosting
{
@ -112,6 +113,7 @@ namespace BTCPayServer.Hosting
runtime.Configure(o.GetRequiredService<BTCPayServerOptions>());
return runtime;
});
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
@ -137,7 +139,10 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<IRateProvider, BitpayRateProvider>();
services.TryAddSingleton<IRateProvider>(o =>
{
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());

View File

@ -67,7 +67,7 @@ namespace BTCPayServer.Hosting
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();

View File

@ -0,0 +1,452 @@
// <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("20171023101754_StoreBlob")]
partial class StoreBlob
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
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<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()
.HasForeignKey("InvoiceDataId");
});
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 StoreBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "StoreBlob",
table: "Stores",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StoreBlob",
table: "Stores");
}
}
}

View File

@ -0,0 +1,477 @@
// <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("20171024163354_RenewUsedAddresses")]
partial class RenewUsedAddresses
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
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<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<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()
.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,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class RenewUsedAddresses : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "CreatedTime",
table: "AddressInvoices",
nullable: true);
migrationBuilder.CreateTable(
name: "HistoricalAddressInvoices",
columns: table => new
{
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
Address = table.Column<string>(type: "TEXT", nullable: false),
Assigned = table.Column<DateTimeOffset>(nullable: false),
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HistoricalAddressInvoices", x => new { x.InvoiceDataId, x.Address });
table.ForeignKey(
name: "FK_HistoricalAddressInvoices_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "HistoricalAddressInvoices");
migrationBuilder.DropColumn(
name: "CreatedTime",
table: "AddressInvoices");
}
}
}

View File

@ -25,6 +25,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
@ -34,6 +36,21 @@ namespace BTCPayServer.Migrations
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
@ -173,6 +190,8 @@ namespace BTCPayServer.Migrations
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
@ -365,6 +384,14 @@ namespace BTCPayServer.Migrations
.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")

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models
{
public class ConfirmModel
{
public string Title
{
get; set;
}
public string Description
{
get; set;
}
public string Action
{
get; set;
}
}
}

View File

@ -21,6 +21,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Url]
[Display(Name = "Store Website")]
[MaxLength(500)]
public string StoreWebsite
{
get;
@ -39,6 +40,12 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Add network fee to invoice (vary with mining fees)")]
public bool NetworkFee
{
get; set;
}
public List<(string KeyPath, string Address)> AddressSamples
{
get; set;

View File

@ -1,23 +1,26 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14139/",
"sslPort": 0
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14139/",
"sslPort": 0
}
},
"profiles": {
"Default": {
"commandName": "Project"
},
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_EXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_NETWORK": "regtest",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://localhost:14142/"
}
}
},
"profiles": {
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_EXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_NETWORK": "regtest",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://localhost:14142/"
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Text;
namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env)
{
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
Build = "Debug";
#else
Build = "Release";
#endif
Environment = env;
}
public IHostingEnvironment Environment
{
get; set;
}
public string Version
{
get; set;
}
public string Build
{
get; set;
}
public override string ToString()
{
StringBuilder txt = new StringBuilder();
txt.Append($"@Copyright BTCPayServer v{Version}");
if(!Environment.IsProduction() || Build.Equals("Release", StringComparison.OrdinalIgnoreCase))
{
txt.Append($" Environment: {Environment.EnvironmentName} Build: {Build}");
}
return txt.ToString();
}
}
}

View File

@ -8,6 +8,7 @@ using BTCPayServer.Models;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Invoices
{
@ -253,6 +254,11 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public HistoricalAddressInvoiceData[] HistoricalAddresses
{
get;
set;
}
public bool IsExpired()
{
@ -273,7 +279,7 @@ namespace BTCPayServer.Services.Invoices
ExpirationTime = ExpirationTime,
BTCPrice = Money.Coins((decimal)(1.0 / Rate)).ToString(),
Status = Status,
Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id,
Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id,
Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable }
};

View File

@ -54,8 +54,6 @@ namespace BTCPayServer.Services.Invoices
_ContextFactory = contextFactory;
}
public async Task AddPendingInvoice(string invoiceId)
{
using(var ctx = _ContextFactory.CreateContext())
@ -79,6 +77,14 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey)
{
using(var db = _ContextFactory.CreateContext())
{
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString());
return result?.InvoiceDataId;
}
}
public async Task<string[]> GetPendingInvoices()
{
@ -122,11 +128,26 @@ namespace BTCPayServer.Services.Invoices
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
}).ConfigureAwait(false);
context.AddressInvoices.Add(new AddressInvoiceData()
{
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
});
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(),
@ -139,6 +160,63 @@ namespace BTCPayServer.Services.Invoices
return invoice;
}
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress)
{
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)
{
MarkUnassigned(invoiceId, old, context);
}
context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow });
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoiceId,
Address = bitcoinAddress.ToString(),
Assigned = DateTimeOffset.UtcNow
});
await context.SaveChangesAsync();
AddToTextSearch(invoice.Id, bitcoinAddress.ToString());
return true;
}
}
private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context)
{
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;
}
public async Task UnaffectAddress(string invoiceId)
{
using(var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if(invoiceData == null)
return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
if(invoiceEntity.DepositAddress == null)
return;
MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context);
try
{
await context.SaveChangesAsync();
}
catch(DbUpdateException) { } //Possibly, it was unassigned before
}
}
private string[] SearchInvoice(string searchTerms)
{
@ -173,7 +251,7 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> GetInvoice(string storeId, string id)
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool includeHistoricalAddresses = false)
{
using(var context = _ContextFactory.CreateContext())
{
@ -181,8 +259,10 @@ namespace BTCPayServer.Services.Invoices
context
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses)
.Where(i => i.Id == id);
.Include(o => o.RefundAddresses);
if(includeHistoricalAddresses)
query = query.Include(o => o.HistoricalAddressInvoices);
query = query.Where(i => i.Id == id);
if(storeId != null)
query = query.Where(i => i.StoreDataId == storeId);
@ -203,6 +283,10 @@ namespace BTCPayServer.Services.Invoices
entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail;
entity.Refundable = invoice.RefundAddresses.Count != 0;
if(invoice.HistoricalAddressInvoices != null)
{
entity.HistoricalAddresses = invoice.HistoricalAddressInvoices.ToArray();
}
return entity;
}

View File

@ -45,7 +45,7 @@ namespace BTCPayServer.Services.Invoices
public async Task NotifyReceived(Script scriptPubKey)
{
var invoice = await _Wallet.GetInvoiceId(scriptPubKey);
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey);
if(invoice != null)
_WatchRequests.Add(invoice);
}
@ -81,7 +81,7 @@ namespace BTCPayServer.Services.Invoices
}
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
if(invoice.Status == "complete" ||
if(invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
{
if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
@ -113,7 +113,7 @@ namespace BTCPayServer.Services.Invoices
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
var invoiceIds = utxos.Select(u => _InvoiceRepository.GetInvoiceIdFromScriptPubKey(u.Output.ScriptPubKey)).ToArray();
utxos =
utxos
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
@ -125,16 +125,23 @@ namespace BTCPayServer.Services.Invoices
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
BitcoinAddress generatedAddress = null;
bool dirtyAddress = false;
foreach(var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
invoice.Payments.Add(payment);
if(coin.ScriptPubKey == invoice.DepositAddress.ScriptPubKey && generatedAddress == null)
{
dirtyAddress = true;
}
}
//////
if(invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "expired";
}
@ -151,6 +158,7 @@ namespace BTCPayServer.Services.Invoices
_NotificationManager.Notify(invoice);
}
invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
}
else if(invoice.Status == "expired")
@ -163,13 +171,21 @@ namespace BTCPayServer.Services.Invoices
if(totalPaid > invoice.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")
{
Logs.PayServer.LogInformation("Paid to " + invoice.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);
}
}
}
@ -194,6 +210,7 @@ namespace BTCPayServer.Services.Invoices
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if(totalConfirmed >= invoice.GetTotalCryptoDue())
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "confirmed";
_NotificationManager.Notify(invoice);
needSave = true;
@ -201,6 +218,7 @@ namespace BTCPayServer.Services.Invoices
}
else
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedRateProvider : IRateProvider
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
public CachedRateProvider(IRateProvider inner, IMemoryCache memoryCache)
{
if(inner == null)
throw new ArgumentNullException(nameof(inner));
if(memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this._MemoryCache = memoryCache;
}
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public Task<decimal> GetRateAsync(string currency)
{
return _MemoryCache.GetOrCreateAsync("CURR_" + currency, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);
});
}
private bool TryGetFromCache(string key, out object obj)
{
obj = _MemoryCache.Get(key);
return obj != null;
}
public Task<ICollection<Rate>> GetRatesAsync()
{
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
}
}

View File

@ -0,0 +1,116 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageException : Exception
{
public CoinAverageException(string message) : base(message)
{
}
}
public class CoinAverageRateProvider : IRateProvider
{
public class RatesJson
{
public class RateJson
{
public string Code
{
get; set;
}
public decimal Rate
{
get; set;
}
}
[JsonProperty("rates")]
public JObject RatesInternal
{
get; set;
}
[JsonIgnore]
public List<RateJson> Rates
{
get; set;
}
[JsonIgnore]
public Dictionary<string, decimal> RatesByCurrency
{
get; set;
}
public decimal GetRate(string currency)
{
if(!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD))
throw new RateUnavailableException(currency);
if(!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD))
throw new RateUnavailableException(currency);
return currUSD / btcUSD;
}
public void CalculateDictionary()
{
RatesByCurrency = new Dictionary<string, decimal>();
Rates = new List<RateJson>();
foreach(var rate in RatesInternal.OfType<JProperty>())
{
var rateJson = new RateJson();
rateJson.Code = rate.Name;
rateJson.Rate = rate.Value["rate"].Value<decimal>();
RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson);
}
}
}
static HttpClient _Client = new HttpClient();
public string Market
{
get; set;
} = "global";
public async Task<decimal> GetRateAsync(string currency)
{
RatesJson rates = await GetRatesCore();
return rates.GetRate(currency);
}
private async Task<RatesJson> GetRatesCore()
{
var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market);
using(resp)
{
if((int)resp.StatusCode == 401)
throw new CoinAverageException("Unauthorized access to the API");
if((int)resp.StatusCode == 429)
throw new CoinAverageException("Exceed API limits");
if((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync());
rates.CalculateDictionary();
return rates;
}
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
RatesJson rates = await GetRatesCore();
return rates.Rates.Select(o => new Rate()
{
Currency = o.Code,
Value = rates.GetRate(o.Code)
}).ToList();
}
}
}

View File

@ -83,6 +83,18 @@ namespace BTCPayServer.Services.Stores
}
}
public async Task RemoveStore(string storeId, string userId)
{
using(var ctx = _ContextFactory.CreateContext())
{
var storeUser = await ctx.UserStore.FirstOrDefaultAsync(o => o.StoreDataId == storeId && o.ApplicationUserId == userId);
if(storeUser == null)
return;
ctx.UserStore.Remove(storeUser);
await ctx.SaveChangesAsync();
}
}
public async Task UpdateStore(StoreData store)
{
using(var ctx = _ContextFactory.CreateContext())

View File

@ -39,28 +39,6 @@ namespace BTCPayServer.Services.Wallets
await _Client.TrackAsync(derivationStrategy);
}
public async Task<string> GetInvoiceId(Script scriptPubKey)
{
using(var db = _DBFactory.CreateContext())
{
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString());
return result?.InvoiceDataId;
}
}
public async Task MapAsync(Script address, string invoiceId)
{
using(var db = _DBFactory.CreateContext())
{
db.AddressInvoices.Add(new AddressInvoiceData()
{
Address = address.Hash.ToString(),
InvoiceDataId = invoiceId
});
await db.SaveChangesAsync();
}
}
private byte[] ToBytes<T>(T obj)
{
return ZipUtils.Zip(_Serializer.ToString(obj));

View File

@ -1,4 +1,5 @@
@model PaymentModel
@inject System.Text.Encodings.Web.JavaScriptEncoder jsEncoder;
@{
Layout = null;
ViewData["Title"] = "Payment";
@ -27,7 +28,7 @@
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<script type="text/javascript">
var srvModel = @Model.ToJson();
var srvModel = JSON.parse('@jsEncoder.Encode(Model.ToJson())');
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>

View File

@ -0,0 +1,23 @@
@model ConfirmModel
@{
Layout = "_Layout.cshtml";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@Model.Title</h2>
<hr class="primary">
<p>@Model.Description</p>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<form method="post">
<button type="submit" class="btn btn-info btn-danger" title="Continue">@Model.Action</button>
</form>
</div>
</div>
</div>
</section>

View File

@ -1,6 +1,7 @@
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment env
<!DOCTYPE html>
<html lang="en">
@ -75,7 +76,9 @@
</div>
</nav>
@RenderBody()
<footer class="bg-dark">
<div class="container" style="text-align:right;font-size:10px; ">@env.ToString()</div>
</footer>
<!-- Bootstrap core JavaScript -->
<script src="~/vendor/jquery/jquery.min.js"></script>
<script src="~/vendor/popper/popper.min.js"></script>

View File

@ -42,7 +42,7 @@
<a href="@store.WebSite">@store.WebSite</a>
}</td>
<td>@store.Balance</td>
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a></td>
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
</tr>
}
</tbody>

View File

@ -26,6 +26,10 @@
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="SpeedPolicy"></label>
<select asp-for="SpeedPolicy" class="form-control">