Compare commits

...

24 Commits

Author SHA1 Message Date
a5b732e197 Update NBitcoin, NBxplorer and bump 2017-12-03 01:56:26 +09:00
f404aaf768 bump 2017-12-02 23:22:47 +09:00
e1f8177834 Can configure externalurl in case BTCPay is behind a reverse proxy 2017-12-02 23:22:23 +09:00
cff391a7a9 Put checkout title to BTCPay 2017-12-02 14:13:11 +09:00
9cd7608a53 Fixing bug caused by BTC being too high 2017-12-02 14:07:14 +09:00
6950a06532 break line in yaml for increased readability 2017-11-27 17:01:11 +09:00
0e6c2ec556 fix search button 2017-11-13 00:27:16 +09:00
479fc50d9a Add PendingInvoice inside CreateInvoice 2017-11-12 23:51:14 +09:00
a29a8f7ed9 Do not use AddAsync 2017-11-12 23:37:21 +09:00
83cf637f9d fetch dependencies when creating request simultaneously 2017-11-12 23:23:21 +09:00
5dbb4bf6be Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2017-11-12 23:18:45 +09:00
f1f227b746 Index invoice in a parallel thread 2017-11-12 23:03:33 +09:00
b96cac16c6 Merge pull request #11 from lepipele/dev-lepi
Allowing user to invalidate paid invoice
2017-11-06 07:37:06 -08:00
f58fdafdcd Simplifying check for invoiceData null and status 2017-11-06 07:43:24 -06:00
b7b39f8284 Merge remote-tracking branch 'source/master' into dev-lepi
# Conflicts:
#	BTCPayServer/Services/Invoices/InvoiceRepository.cs
2017-11-06 07:35:17 -06:00
7a173a6692 Update NBXplorer 2017-11-06 00:54:03 -08:00
b042f98f0f Correctly handle RBF 2017-11-06 00:31:02 -08:00
0bb260bec9 Allowing user to invalidate paid invoice 2017-11-05 21:15:52 -06:00
024ab8ff69 Update NBXplorer 2017-11-04 16:02:00 -07:00
50cb61f3bd Update nbxplorer 2017-11-02 15:38:52 -07:00
f2befb7c48 Add video link to homepage 2017-11-02 14:03:49 -07:00
dd80263490 update nbxplorer 2017-11-01 02:17:39 -07:00
73c44f8726 don't crash if the user unset his extpubkey 2017-11-01 01:06:59 -07:00
ca85095273 Move code into CurrencyNameTable 2017-10-27 18:58:43 +09:00
32 changed files with 1074 additions and 146 deletions

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>

View File

@ -108,7 +108,6 @@ namespace BTCPayServer.Tests
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
watcher.PollInterval = TimeSpan.FromMilliseconds(500);
}
public BTCPayServerRuntime Runtime

View File

@ -71,7 +71,7 @@ namespace BTCPayServer.Tests.Logging
public void LogInformation(string msg)
{
if (msg != null)
_Helper.WriteLine(Name + ": " + msg);
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
}
public class Logs

View File

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Tests
{
@ -55,14 +56,17 @@ namespace BTCPayServer.Tests
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.UpdateStore(StoreId, new StoreViewModel()
{
DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]",
DerivationScheme = DerivationScheme.ToString(),
SpeedPolicy = SpeedPolicy.MediumSpeed
}, "Save");
return store;
}
public DerivationStrategyBase DerivationScheme { get; set; }
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();

View File

@ -45,22 +45,22 @@ namespace BTCPayServer.Tests
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()), 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());
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()), Accounted = true });
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()), Accounted = true });
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()), Accounted = true });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
@ -194,6 +194,63 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanRBFPayment()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD"
}, Facade.Merchant);
var payment1 = Money.Coins(0.04m);
var payment2 = Money.Coins(0.08m);
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress.ToString(),
payment1.ToString(),
null, //comment
null, //comment_to
false, //subtractfeefromamount
true, //replaceable
}).ResultString);
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
});
var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1));
foreach (var input in tx.Inputs)
{
input.ScriptSig = Script.Empty; //Strip signatures
}
var change = tx.Outputs.First(o => o.Value != payment1);
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
change.Value -= (payment2 - payment1) * 2; //Add more fees
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
});
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -217,19 +274,23 @@ namespace BTCPayServer.Tests
}, 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();
Assert.Equal(1, textSearchResult.Length);
Eventually(() =>
{
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();
Assert.Equal(1, textSearchResult.Length);
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);

View File

@ -21,7 +21,7 @@ services:
- "tests:127.0.0.1"
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.18
image: nicolasdorier/nbxplorer:1.0.0.29
ports:
- "32838:32838"
expose:
@ -33,11 +33,15 @@ services:
NBXPLORER_RPCPASSWORD: DwubwWsoo3
NBXPLORER_NODEENDPOINT: bitcoind:39388
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- bitcoind
- postgres
eclair:
image: nicolasdorier/docker-bitcoin:0.15.0.1
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.15.0.1
@ -45,7 +49,13 @@ services:
- "43782:43782"
- "39388:39388"
environment:
BITCOIN_EXTRA_ARGS: "rpcuser=ceiwHEbqWI83\nrpcpassword=DwubwWsoo3\nregtest=1\nrpcport=43782\nport=39388\nwhitelist=0.0.0.0/0"
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
expose:
- "43782"
- "39388"
@ -53,4 +63,4 @@ services:
postgres:
image: postgres:9.6.5
ports:
- "39372:5432"
- "39372:5432"

View File

@ -69,7 +69,7 @@ namespace BTCPayServer.Authentication
{
var now = DateTime.UtcNow;
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
await ctx.PairingCodes.AddAsync(new PairingCodeData()
ctx.PairingCodes.Add(new PairingCodeData()
{
Id = pairingCodeId,
DateCreated = now,

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.26</Version>
<Version>1.0.0.32</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -21,10 +21,10 @@
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.38" />
<PackageReference Include="NBitcoin" Version="4.0.0.48" />
<PackageReference Include="NBitpayClient" Version="1.0.0.12" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.18" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />

View File

@ -57,6 +57,7 @@ namespace BTCPayServer.Configuration
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
}
public bool RequireHttps
@ -68,5 +69,10 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
}
}

View File

@ -49,11 +49,6 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
}
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db);
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
@ -68,7 +63,9 @@ namespace BTCPayServer.Configuration
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
InvoiceRepository = new InvoiceRepository(dbContext, CreateDBPath(opts, "InvoiceDB"), Network);
_Resources.Add(InvoiceRepository);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)

View File

@ -19,8 +19,8 @@ namespace BTCPayServer.Configuration
{
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "NBXplorer\r\nLightweight block explorer for tracking HD wallets",
Name = "NBXplorer"
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
Name = "BTCPay"
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
@ -30,7 +30,7 @@ namespace BTCPayServer.Configuration
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
return app;
}

View File

@ -23,11 +23,11 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId, string command)
public IActionResult Invoice(string invoiceId, string command)
{
if (command == "refresh")
{
await _Watcher.WatchAsync(invoiceId, true);
_Watcher.Watch(invoiceId);
}
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
return RedirectToAction(nameof(Invoice), new
@ -92,27 +92,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
static Dictionary<string, CultureInfo> _CurrencyProviders = new Dictionary<string, CultureInfo>();
private IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
{
if (_CurrencyProviders.Count == 0)
{
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
{
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
}
catch { }
}
}
return _CurrencyProviders.TryGet(currency);
}
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("invoice")]
@ -140,6 +119,7 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
var cryptoFormat = _CurrencyNameTable.GetCurrencyProvider("BTC");
var model = new PaymentModel()
{
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
@ -153,7 +133,7 @@ namespace BTCPayServer.Controllers
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", GetCurrencyProvider(invoice.ProductInformation.Currency)),
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(invoice.ProductInformation.Currency)),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
@ -301,6 +281,16 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost]
[Route("invoices/invalidatepaid")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
return RedirectToAction(nameof(ListInvoices));
}
[TempData]
public string StatusMessage
{

View File

@ -51,11 +51,13 @@ namespace BTCPayServer.Controllers
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
IRateProvider rateProvider,
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
ExplorerClient explorerClient,
IFeeProvider feeProvider)
{
_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));
@ -107,13 +110,16 @@ namespace BTCPayServer.Controllers
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
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));
var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate;
entity.PosData = invoice.PosData;
entity.DepositAddress = await getAddress;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Watcher.WatchAsync(entity.Id);
_Watcher.Watch(entity.Id);
var resp = entity.EntityToDTO();
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}

View File

@ -190,9 +190,12 @@ namespace BTCPayServer.Controllers
needUpdate = true;
try
{
var strategy = ParseDerivationStrategy(model.DerivationScheme);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
var strategy = ParseDerivationStrategy(model.DerivationScheme);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
}
store.DerivationStrategy = model.DerivationScheme;
}
catch

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
class CustomThreadPool : IDisposable
{
CancellationTokenSource _Cancel = new CancellationTokenSource();
TaskCompletionSource<bool> _Exited;
int _ExitedCount = 0;
Thread[] _Threads;
Exception _UnhandledException;
BlockingCollection<(Action, TaskCompletionSource<object>)> _Actions = new BlockingCollection<(Action, TaskCompletionSource<object>)>(new ConcurrentQueue<(Action, TaskCompletionSource<object>)>());
public CustomThreadPool(int threadCount, string threadName)
{
if (threadCount <= 0)
throw new ArgumentOutOfRangeException(nameof(threadCount));
_Exited = new TaskCompletionSource<bool>();
_Threads = Enumerable.Range(0, threadCount).Select(_ => new Thread(RunLoop) { Name = threadName }).ToArray();
foreach (var t in _Threads)
t.Start();
}
public void Do(Action act)
{
DoAsync(act).GetAwaiter().GetResult();
}
public T Do<T>(Func<T> act)
{
return DoAsync(act).GetAwaiter().GetResult();
}
public async Task<T> DoAsync<T>(Func<T> act)
{
TaskCompletionSource<object> done = new TaskCompletionSource<object>();
_Actions.Add((() =>
{
try
{
done.TrySetResult(act());
}
catch (Exception ex) { done.TrySetException(ex); }
}
, done));
return (T)(await done.Task.ConfigureAwait(false));
}
public Task DoAsync(Action act)
{
return DoAsync<object>(() =>
{
act();
return null;
});
}
void RunLoop()
{
try
{
foreach (var act in _Actions.GetConsumingEnumerable(_Cancel.Token))
{
act.Item1();
}
}
catch (OperationCanceledException) when (_Cancel.IsCancellationRequested) { }
catch (Exception ex)
{
_Cancel.Cancel();
_UnhandledException = ex;
}
if (Interlocked.Increment(ref _ExitedCount) == _Threads.Length)
{
foreach (var action in _Actions)
{
try
{
action.Item2.TrySetCanceled();
}
catch { }
}
_Exited.TrySetResult(true);
}
}
public void Dispose()
{
_Cancel.Cancel();
_Exited.Task.GetAwaiter().GetResult();
}
}
}

View File

@ -71,5 +71,9 @@ namespace BTCPayServer.Data
get;
set;
}
public List<AddressInvoiceData> AddressInvoices
{
get; set;
}
}
}

View File

@ -25,5 +25,9 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool Accounted
{
get; set;
}
}
}

View File

@ -11,11 +11,26 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using NBitcoin;
using System.Threading.Tasks;
using NBXplorer;
using NBXplorer.Models;
using System.Linq;
using System.Threading;
namespace BTCPayServer
{
public static class Extensions
{
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this ExplorerClient client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
var transactions = hashes
.Select(async o => await client.GetTransactionAsync(o, cts))
.ToArray();
await Task.WhenAll(transactions).ConfigureAwait(false);
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/"))

View File

@ -30,13 +30,16 @@ namespace BTCPayServer.Hosting
TokenRepository _TokenRepository;
RequestDelegate _Next;
CallbackController _CallbackController;
BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
BTCPayServerOptions options,
CallbackController callbackController)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController;
_Options = options ?? throw new ArgumentNullException(nameof(options));
}
@ -50,6 +53,16 @@ namespace BTCPayServer.Hosting
_Registered = true;
}
// Make sure that code executing after this point think that the external url has been hit.
if(_Options.ExternalUrl != null)
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if(_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);

View File

@ -0,0 +1,479 @@
// <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("20171105235734_PaymentAccounted")]
partial class PaymentAccounted
{
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<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()
.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,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class PaymentAccounted : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "Accounted",
table: "Payments",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Accounted",
table: "Payments");
}
}
}

View File

@ -132,6 +132,8 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");

View File

@ -9,6 +9,7 @@ using NBitpayClient;
using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
namespace BTCPayServer.Services.Invoices
{
@ -132,6 +133,7 @@ namespace BTCPayServer.Services.Invoices
int txCount = 1;
var payments =
Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
@ -260,6 +262,12 @@ namespace BTCPayServer.Services.Invoices
set;
}
public HashSet<string> AvailableAddressHashes
{
get;
set;
}
public bool IsExpired()
{
return DateTimeOffset.UtcNow > ExpirationTime;
@ -300,7 +308,7 @@ namespace BTCPayServer.Services.Invoices
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
var paid = Payments.Select(p => p.Output.Value).Sum();
var paid = Payments.Where(p => p.Accounted).Select(p => p.Output.Value).Sum();
dto.BTCPaid = paid.ToString();
dto.BTCDue = GetCryptoDue().ToString();
@ -321,6 +329,17 @@ namespace BTCPayServer.Services.Invoices
}
}
public class AccountedPaymentEntity
{
public int Confirmations
{
get;
set;
}
public PaymentEntity Payment { get; set; }
public Transaction Transaction { get; set; }
}
public class PaymentEntity
{
public DateTimeOffset ReceivedTime
@ -335,5 +354,9 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
public bool Accounted
{
get; set;
}
}
}

View File

@ -19,7 +19,7 @@ using BTCPayServer.Models.InvoicingModels;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceRepository
public class InvoiceRepository : IDisposable
{
@ -45,24 +45,17 @@ namespace BTCPayServer.Services.Invoices
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, DBreezeEngine engine, Network network)
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
{
_Engine = engine;
_Engine = new DBreezeEngine(dbreezePath);
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
_Network = network;
_ContextFactory = contextFactory;
}
public async Task AddPendingInvoice(string invoiceId)
{
using (var ctx = _ContextFactory.CreateContext())
{
ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
await ctx.SaveChangesAsync();
}
}
public async Task<bool> RemovePendingInvoice(string invoiceId)
{
using (var ctx = _ContextFactory.CreateContext())
@ -117,7 +110,7 @@ namespace BTCPayServer.Services.Invoices
invoice.StoreId = storeId;
using (var context = _ContextFactory.CreateContext())
{
await context.AddAsync(new InvoiceData()
context.Invoices.Add(new InvoiceData()
{
StoreDataId = storeId,
Id = invoice.Id,
@ -127,21 +120,20 @@ namespace BTCPayServer.Services.Invoices
Status = invoice.Status,
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
});
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
await context.SaveChangesAsync().ConfigureAwait(false);
}
@ -231,11 +223,14 @@ namespace BTCPayServer.Services.Invoices
void AddToTextSearch(string invoiceId, params string[] terms)
{
using (var tx = _Engine.GetTransaction())
_IndexerThread.DoAsync(() =>
{
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.Commit();
}
using (var tx = _Engine.GetTransaction())
{
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.Commit();
}
});
}
public async Task UpdateInvoiceStatus(string invoiceId, string status, string exceptionStatus)
@ -251,7 +246,18 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool includeHistoricalAddresses = false)
public async Task UpdatePaidInvoiceToInvalid(string invoiceId)
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData?.Status != "paid")
return;
invoiceData.Status = "invalid";
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
public async Task<InvoiceEntity> GetInvoice(string storeId, string id, bool inludeAddressData = false)
{
using (var context = _ContextFactory.CreateContext())
{
@ -260,8 +266,8 @@ namespace BTCPayServer.Services.Invoices
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if (includeHistoricalAddresses)
query = query.Include(o => o.HistoricalAddressInvoices);
if (inludeAddressData)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
query = query.Where(i => i.Id == id);
if (storeId != null)
@ -278,7 +284,12 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
entity.Payments = invoice.Payments.Select(p => ToObject<PaymentEntity>(p.Blob)).ToList();
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
paymentEntity.Accounted = p.Accounted;
return paymentEntity;
}).ToList();
entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail;
@ -287,6 +298,10 @@ namespace BTCPayServer.Services.Invoices
{
entity.HistoricalAddresses = invoice.HistoricalAddressInvoices.ToArray();
}
if (invoice.AddressInvoices != null)
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.Address).ToHashSet();
}
return entity;
}
@ -364,12 +379,12 @@ namespace BTCPayServer.Services.Invoices
int i = 0;
foreach (var output in outputs)
{
await context.RefundAddresses.AddAsync(new RefundAddressesData()
context.RefundAddresses.Add(new RefundAddressesData()
{
Id = invoiceId + "-" + i,
InvoiceDataId = invoiceId,
Blob = ToBytes(output)
}).ConfigureAwait(false);
});
i++;
}
await context.SaveChangesAsync().ConfigureAwait(false);
@ -397,13 +412,32 @@ namespace BTCPayServer.Services.Invoices
InvoiceDataId = invoiceId
};
await context.Payments.AddAsync(data).ConfigureAwait(false);
context.Payments.Add(data);
await context.SaveChangesAsync().ConfigureAwait(false);
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString());
return entity;
}
}
public async Task UpdatePayments(List<AccountedPaymentEntity> payments)
{
if (payments.Count == 0)
return;
using (var context = _ContextFactory.CreateContext())
{
foreach (var payment in payments)
{
var data = new PaymentData();
data.Id = payment.Payment.Outpoint.ToString();
data.Accounted = payment.Payment.Accounted;
context.Attach(data);
context.Entry(data).Property(o => o.Accounted).IsModified = true;
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
private T ToObject<T>(byte[] value)
{
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
@ -423,6 +457,14 @@ namespace BTCPayServer.Services.Invoices
{
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
}
public void Dispose()
{
if (_Engine != null)
_Engine.Dispose();
if (_IndexerThread != null)
_IndexerThread.Dispose();
}
}
public class InvoiceQuery

View File

@ -65,7 +65,7 @@ namespace BTCPayServer.Services.Invoices
{
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId).ConfigureAwait(false);
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
if (invoice == null)
break;
var stateBefore = invoice.Status;
@ -113,28 +113,18 @@ 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 => _InvoiceRepository.GetInvoiceIdFromScriptPubKey(u.Output.ScriptPubKey)).ToArray();
utxos =
utxos
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
.ToArray();
List<Coin> receivedCoins = new List<Coin>();
foreach (var received in utxos)
if (received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
if (invoice.AvailableAddressHashes.Contains(received.Output.ScriptPubKey.Hash.ToString()))
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;
}
dirtyAddress = true;
}
//////
@ -147,7 +137,7 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = invoice.Payments.Select(p => p.Output.Value).Sum();
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.Output.Value).Sum();
if (totalPaid >= invoice.GetTotalCryptoDue())
{
if (invoice.Status == "new")
@ -196,15 +186,15 @@ namespace BTCPayServer.Services.Invoices
var transactions = await GetPaymentsWithTransaction(invoice);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => !t.Transaction.Transaction.RBF);
transactions = transactions.Where(t => !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Transaction.Confirmations >= 1);
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
@ -227,7 +217,7 @@ namespace BTCPayServer.Services.Invoices
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Transaction.Confirmations >= 6);
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
{
@ -237,18 +227,62 @@ namespace BTCPayServer.Services.Invoices
needSave = true;
}
}
return (needSave, changes);
}
private async Task<IEnumerable<(PaymentEntity Payment, TransactionResult Transaction)>> GetPaymentsWithTransaction(InvoiceEntity invoice)
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(InvoiceEntity invoice)
{
var getPayments = invoice.Payments
.Select(async o => (Payment: o, Transaction: await _ExplorerClient.GetTransactionAsync(o.Outpoint.Hash, _Cts.Token)))
.ToArray();
await Task.WhenAll(getPayments).ConfigureAwait(false);
var transactions = getPayments.Select(c => (Payment: c.Result.Payment, Transaction: c.Result.Transaction));
return transactions;
var transactions = await _ExplorerClient.GetTransactions(invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
foreach (var payment in invoice.Payments)
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
{
result.Remove(payment.Outpoint);
continue;
}
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
payments.Add(accountedPayment);
foreach (var txin in tx.Transaction.Inputs)
{
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
{
//We get a double spend
var existing = spentTxIn[txin.PrevOut];
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
{
spentTxIn[txin.PrevOut] = accountedPayment;
result.Remove(existing.Payment.Outpoint);
}
}
}
}
List<PaymentEntity> updated = new List<PaymentEntity>();
var accountedPayments = payments.Where(p =>
{
var accounted = result.Contains(p.Payment.Outpoint);
if (p.Payment.Accounted != accounted)
{
p.Payment.Accounted = accounted;
updated.Add(p.Payment);
}
return accounted;
}).ToArray();
await _InvoiceRepository.UpdatePayments(payments);
return accountedPayments;
}
TimeSpan _PollInterval;
@ -268,12 +302,10 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task WatchAsync(string invoiceId, bool singleShot = false)
public void Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
if (!singleShot)
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
_WatchRequests.Add(invoiceId);
}

View File

@ -66,7 +66,7 @@ namespace BTCPayServer.Services.Rates
{
var rateJson = new RateJson();
rateJson.Code = rate.Name;
rateJson.Rate = rate.Value["rate"].Value<decimal>();
rateJson.Rate = decimal.Parse(rate.Value["rate"].Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson);
}

View File

@ -1,9 +1,11 @@
using System;
using NBitcoin;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Text;
using System.Globalization;
namespace BTCPayServer.Services.Rates
{
@ -37,6 +39,41 @@ namespace BTCPayServer.Services.Rates
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
}
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
public IFormatProvider GetCurrencyProvider(string currency)
{
lock (_CurrencyProviders)
{
if (_CurrencyProviders.Count == 0)
{
foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures).Where(c => !c.IsNeutralCulture))
{
try
{
_CurrencyProviders.TryAdd(new RegionInfo(culture.LCID).ISOCurrencySymbol, culture);
}
catch { }
}
AddCurrency(_CurrencyProviders, "BTC", 8, "BTC");
}
return _CurrencyProviders.TryGet(currency);
}
}
private void AddCurrency(Dictionary<string, IFormatProvider> currencyProviders, string code, int divisibility, string symbol)
{
var culture = new CultureInfo("en-US");
var number = new NumberFormatInfo();
number.CurrencyDecimalDigits = divisibility;
number.CurrencySymbol = symbol;
number.CurrencyDecimalSeparator = culture.NumberFormat.CurrencyDecimalSeparator;
number.CurrencyGroupSeparator = culture.NumberFormat.CurrencyGroupSeparator;
number.CurrencyGroupSizes = culture.NumberFormat.CurrencyGroupSizes;
number.CurrencyNegativePattern = 8;
number.CurrencyPositivePattern = 3;
number.NegativeSign = culture.NumberFormat.NegativeSign;
currencyProviders.TryAdd(code, number);
}
Dictionary<string, CurrencyData> _Currencies;

View File

@ -76,8 +76,8 @@ namespace BTCPayServer.Services.Stores
ApplicationUserId = ownerId,
Role = "Owner"
};
await ctx.AddAsync(store).ConfigureAwait(false);
await ctx.AddAsync(userStore).ConfigureAwait(false);
ctx.Add(store);
ctx.Add(userStore);
await ctx.SaveChangesAsync().ConfigureAwait(false);
return store;
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public class TransactionComparer : IEqualityComparer<Transaction>
{
private static TransactionComparer _Instance = new TransactionComparer();
public static TransactionComparer Instance
{
get
{
return _Instance;
}
}
public bool Equals(Transaction x, Transaction y)
{
return x.GetHash() == y.GetHash();
}
public int GetHashCode(Transaction obj)
{
return obj.GetHash().GetHashCode();
}
}
}

View File

@ -54,8 +54,15 @@
<div class="call-to-action bg-dark">
<div class="container text-center">
<h2>You don't want to depend on us? Self-Host it yourself!</h2>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Read The Doc!</a>
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/npFMOu6tTpA" frameborder="0" allowfullscreen></iframe>
</div>
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/6rd8ZpLrz-4" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
<!-- base href="/" -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>BitPay Invoice</title>
<title>BTCPay Invoice</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />

View File

@ -22,7 +22,7 @@
<input asp-for="SearchTerm" class="form-control" />
<input type="hidden" asp-for="Count" />
<span asp-validation-for="SearchTerm" class="text-danger"></span>
<button type="button" class="btn btn-default" title="Search invoice">
<button type="submit" class="btn btn-default" title="Search invoice">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</form>
@ -43,12 +43,28 @@
</tr>
</thead>
<tbody>
@foreach(var invoice in Model.Invoices)
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.InvoiceId</td>
<td>@invoice.Status</td>
@if (invoice.Status == "paid")
{
<td>
<div class="btn-group">
<a class="dropdown-toggle dropdown-toggle-split" style="cursor: pointer;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status <span class="sr-only">Toggle Dropdown</span>
</a>
<div class="dropdown-menu pull-right">
<button class="dropdown-item small" data-toggle="modal" data-target="#myModal" onclick="$('#invoiceId').val('@invoice.InvoiceId')">Make Invalid</button>
</div>
</div>
</td>
}
else
{
<td>@invoice.Status</td>
}
<td>@invoice.AmountCurrency</td>
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> - <a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a></td>
</tr>
@ -56,23 +72,48 @@
</tbody>
</table>
<span>
@if(Model.Skip != 0)
@if (Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})"><<</a><span> - </span>
{
searchTerm = Model.SearchTerm,
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})"><<</a><span> - </span>
}
<a href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">>></a>
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">>></a>
</span>
</div>
</div>
</section>
<!-- Modal -->
<div id="myModal" class="modal fade" role="dialog">
<form method="post" action="/invoices/invalidatepaid">
<input id="invoiceId" name="invoiceId" type="hidden" />
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Set Invoice status to Invalid</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to invalidate this transaction? This action is NOT undoable!</p>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Yes, make invoice Invalid</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</form>
</div>