Compare commits

...

6 Commits

37 changed files with 1928 additions and 296 deletions

View File

@ -64,16 +64,20 @@ namespace BTCPayServer.Tests
}
IWebHost _Host;
public int Port
{
get; set;
}
public void Start()
{
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
HDPrivateKey = new ExtKey();
var port = Utils.FreeTcpPort();
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"port={port}");
config.AppendLine($"port={Port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
@ -81,7 +85,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
ServerUri = new Uri("http://127.0.0.1:" + port + "/");
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
@ -94,6 +98,7 @@ namespace BTCPayServer.Tests
{
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
@ -110,6 +115,11 @@ namespace BTCPayServer.Tests
{
get; set;
}
public string HostName
{
get;
internal set;
}
public T GetController<T>(string userId = null) where T : Controller
{
@ -129,7 +139,8 @@ namespace BTCPayServer.Tests
httpAccessor.HttpContext = context;
var controller = (T)ActivatorUtilities.CreateInstance(provider, typeof(T));
controller.Url = new UrlHelperMock();
controller.Url = new UrlHelperMock(new Uri($"http://{HostName}:{Port}/"));
controller.ControllerContext = new ControllerContext()
{
HttpContext = context

View File

@ -8,16 +8,21 @@ namespace BTCPayServer.Tests.Mocks
{
public class UrlHelperMock : IUrlHelper
{
Uri _BaseUrl;
public UrlHelperMock(Uri baseUrl)
{
_BaseUrl = baseUrl;
}
public ActionContext ActionContext => throw new NotImplementedException();
public string Action(UrlActionContext actionContext)
{
return "http://127.0.0.1/mock";
return $"{_BaseUrl}mock";
}
public string Content(string contentPath)
{
return "http://127.0.0.1/mock";
return $"{_BaseUrl}{contentPath}";
}
public bool IsLocalUrl(string url)
@ -27,12 +32,12 @@ namespace BTCPayServer.Tests.Mocks
public string Link(string routeName, object values)
{
return "http://127.0.0.1/mock";
return _BaseUrl.AbsoluteUri;
}
public string RouteUrl(UrlRouteContext routeContext)
{
return "http://127.0.0.1/mock";
return _BaseUrl.AbsoluteUri;
}
}
}

View File

@ -0,0 +1,7 @@
{
"profiles": {
"BTCPayServer.Tests": {
"commandName": "Project"
}
}
}

View File

@ -1,17 +1,21 @@
using BTCPayServer.Controllers;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.RPC;
using NBitpayClient;
using NBXplorer;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace BTCPayServer.Tests
{
@ -28,6 +32,11 @@ namespace BTCPayServer.Tests
_Directory = scope;
}
public bool Dockerized
{
get; set;
}
public void Start()
{
if(Directory.Exists(_Directory))
@ -35,6 +44,8 @@ namespace BTCPayServer.Tests
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
FakeCallback = bool.Parse(GetEnvironment("TESTS_FAKECALLBACK", "true"));
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
@ -42,6 +53,8 @@ namespace BTCPayServer.Tests
NBXplorerUri = ExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
}
@ -51,11 +64,16 @@ namespace BTCPayServer.Tests
return String.IsNullOrEmpty(var) ? defaultValue : var;
}
public TestAccount CreateAccount()
public TestAccount NewAccount()
{
return new TestAccount(this);
}
public bool FakeCallback
{
get;
set;
}
public RPCClient ExplorerNode
{
get; set;
@ -66,13 +84,143 @@ namespace BTCPayServer.Tests
get; set;
}
HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest
{
Uri serverUri;
public MockHttpRequest(Uri serverUri)
{
this.serverUri = serverUri;
}
public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Scheme
{
get => serverUri.Scheme;
set => throw new NotImplementedException();
}
public override bool IsHttps
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override HostString Host
{
get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException();
}
public override PathString PathBase
{
get => "";
set => throw new NotImplementedException();
}
public override PathString Path
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override QueryString QueryString
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IQueryCollection Query
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Protocol
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override long? ContentLength
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ContentType
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Stream Body
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
/// <summary>
/// Simulating callback from NBXplorer. NBXplorer can't reach the host during tests as it is not running on localhost.
/// </summary>
/// <param name="address"></param>
public void SimulateCallback(BitcoinAddress address = null)
{
if(!FakeCallback) //The callback of NBXplorer should work
return;
var req = new MockHttpRequest(PayTester.ServerUri);
var controller = PayTester.GetController<CallbackController>();
if(address != null)
{
var match = new TransactionMatch();
match.Outputs.Add(new KeyPathInformation() { ScriptPubKey = address.ScriptPubKey });
var content = new StringContent(new NBXplorer.Serializer(Network).ToString(match), new UTF8Encoding(false), "application/json");
var uri = controller.GetCallbackUriAsync(req).GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
message.RequestUri = uri;
message.Content = content;
_Http.SendAsync(message).GetAwaiter().GetResult();
}
else
{
var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
message.RequestUri = uri;
_Http.SendAsync(message).GetAwaiter().GetResult();
}
}
public BTCPayServerTester PayTester
{
get; set;
}
public Network Network
{
get;

View File

@ -26,10 +26,45 @@ namespace BTCPayServer.Tests
{
GrantAccessAsync().GetAwaiter().GetResult();
}
public void Register()
{
RegisterAsync().GetAwaiter().GetResult();
}
public BitcoinExtKey ExtKey
{
get; set;
}
public async Task GrantAccessAsync()
{
var extKey = new ExtKey().GetWif(parent.Network);
await RegisterAsync();
var store = await CreateStoreAsync();
var pairingCode = BitPay.RequestClientAuthorization("test", Facade.Merchant);
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore()
{
return CreateStoreAsync().GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync()
{
ExtKey = new ExtKey().GetWif(parent.Network);
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
await store.UpdateStore(StoreId, new StoreViewModel()
{
DerivationScheme = ExtKey.Neuter().ToString() + "-[legacy]",
SpeedPolicy = SpeedPolicy.MediumSpeed
}, "Save");
return store;
}
private async Task RegisterAsync()
{
var account = parent.PayTester.GetController<AccountController>();
await account.Register(new RegisterViewModel()
{
@ -38,18 +73,6 @@ namespace BTCPayServer.Tests
Password = "Kitten0@",
});
UserId = account.RegisteredUserId;
var store = parent.PayTester.GetController<StoresController>(account.RegisteredUserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
await store.UpdateStore(StoreId, new StoreViewModel()
{
DerivationScheme = extKey.Neuter().ToString() + "-[legacy]",
SpeedPolicy = SpeedPolicy.MediumSpeed
}, "Save");
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public Bitpay BitPay

View File

@ -13,6 +13,8 @@ using BTCPayServer.Servcices.Invoices;
using Newtonsoft.Json;
using System.IO;
using Newtonsoft.Json.Linq;
using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Tests
{
@ -63,7 +65,7 @@ namespace BTCPayServer.Tests
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.CreateAccount();
var user = tester.NewAccount();
user.GrantAccess();
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
@ -97,6 +99,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(url.Address);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.True(localInvoice.Refundable);
@ -104,6 +107,31 @@ namespace BTCPayServer.Tests
}
}
[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 pairingCode = (string)token.RouteValues["pairingCode"];
acc.BitPay.AuthorizeClient(new PairingCode(pairingCode)).GetAwaiter().GetResult();
Assert.True(acc.BitPay.TestAccess(Facade.Merchant));
}
}
[Fact]
public void CanSendIPN()
{
@ -112,7 +140,7 @@ namespace BTCPayServer.Tests
using(var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.CreateAccount();
var acc = tester.NewAccount();
acc.GrantAccess();
var invoice = acc.BitPay.CreateInvoice(new Invoice()
{
@ -126,6 +154,8 @@ namespace BTCPayServer.Tests
});
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();
@ -143,7 +173,7 @@ namespace BTCPayServer.Tests
using(var tester = ServerTester.Create())
{
tester.Start();
var user = tester.CreateAccount();
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
@ -201,6 +231,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidPartial", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
@ -213,6 +244,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
@ -224,6 +256,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
@ -232,6 +265,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
});
@ -253,6 +287,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paidOver", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
@ -263,6 +298,7 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);

View File

@ -10,11 +10,18 @@ services:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_FAKECALLBACK: 'true'
TESTS_PORT: 80
TESTS_HOSTNAME: tests
expose:
- "80"
links:
- nbxplorer
extra_hosts:
- "tests:127.0.0.1"
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.16
image: nicolasdorier/nbxplorer:1.0.0.18
ports:
- "32838:32838"
expose:

View File

@ -8,7 +8,7 @@ namespace BTCPayServer.Authentication
{
public class BitTokenEntity
{
public string Name
public string Facade
{
get; set;
}
@ -16,15 +16,7 @@ namespace BTCPayServer.Authentication
{
get; set;
}
public DateTimeOffset DateCreated
{
get; set;
}
public bool Active
{
get; set;
}
public string PairedId
public string StoreId
{
get; set;
}
@ -46,11 +38,9 @@ namespace BTCPayServer.Authentication
{
return new BitTokenEntity()
{
Active = Active,
DateCreated = DateCreated,
Label = Label,
Name = Name,
PairedId = PairedId,
Facade = Facade,
StoreId = StoreId,
PairingTime = PairingTime,
SIN = SIN,
Value = Value

View File

@ -26,17 +26,17 @@ namespace BTCPayServer.Authentication
get;
set;
}
public DateTimeOffset PairingTime
public DateTimeOffset CreatedTime
{
get;
set;
}
public DateTimeOffset PairingExpiration
public DateTimeOffset Expiration
{
get;
set;
}
public string Token
public string TokenValue
{
get;
set;
@ -44,7 +44,7 @@ namespace BTCPayServer.Authentication
public bool IsExpired()
{
return DateTimeOffset.UtcNow > PairingExpiration;
return DateTimeOffset.UtcNow > Expiration;
}
}
}

View File

@ -1,4 +1,5 @@
using DBreeze;
using BTCPayServer.Data;
using DBreeze;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
@ -6,175 +7,177 @@ using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using System.Linq;
namespace BTCPayServer.Authentication
{
public class TokenRepository
{
public TokenRepository(DBreezeEngine engine)
ApplicationDbContextFactory _Factory;
public TokenRepository(ApplicationDbContextFactory dbFactory)
{
_Engine = engine;
if(dbFactory == null)
throw new ArgumentNullException(nameof(dbFactory));
_Factory = dbFactory;
}
private readonly DBreezeEngine _Engine;
public DBreezeEngine Engine
public async Task<BitTokenEntity[]> GetTokens(string sin)
{
get
using(var ctx = _Factory.CreateContext())
{
return _Engine;
return (await ctx.PairedSINData
.Where(p => p.SIN == sin)
.ToListAsync())
.Select(p => CreateTokenEntity(p))
.ToArray();
}
}
public Task<BitTokenEntity[]> GetTokens(string sin)
private BitTokenEntity CreateTokenEntity(PairedSINData data)
{
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
using(var tx = _Engine.GetTransaction())
return new BitTokenEntity()
{
tx.ValuesLazyLoadingIsOn = false;
foreach(var row in tx.SelectForward<string, byte[]>($"T_{sin}"))
{
var token = ToObject<BitTokenEntity>(row.Value);
tokens.Add(token);
}
}
return Task.FromResult(tokens.ToArray());
}
public Task<BitTokenEntity> CreateToken(string sin, string tokenName)
{
var token = new BitTokenEntity
{
Name = tokenName,
Value = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
DateCreated = DateTimeOffset.UtcNow
Label = data.Label,
Facade = data.Facade,
Value = data.Id,
SIN = data.SIN,
PairingTime = data.PairingTime,
StoreId = data.StoreDataId
};
using(var tx = _Engine.GetTransaction())
{
tx.Insert<string, byte[]>($"T_{sin}", token.Name, ToBytes(token));
tx.Commit();
}
return Task.FromResult(token);
}
public Task<bool> PairWithAsync(string pairingCode, string pairedId)
public async Task<string> CreatePairingCodeAsync()
{
if(pairedId == null)
throw new ArgumentNullException(nameof(pairedId));
using(var tx = _Engine.GetTransaction())
string pairingCodeId = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
using(var ctx = _Factory.CreateContext())
{
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
if(row == null || !row.Exists)
return Task.FromResult(false);
tx.RemoveKey<string>("PairingCodes", pairingCode);
try
var now = DateTime.UtcNow;
var expiration = DateTime.UtcNow + TimeSpan.FromMinutes(15);
await ctx.PairingCodes.AddAsync(new PairingCodeData()
{
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
if(pairingEntity.IsExpired())
return Task.FromResult(false);
row = tx.Select<string, byte[]>($"T_{pairingEntity.SIN}", pairingEntity.Facade);
if(row == null || !row.Exists)
return Task.FromResult(false);
var token = ToObject<BitTokenEntity>(row.Value);
if(token.Active)
return Task.FromResult(false);
token.Active = true;
token.PairedId = pairedId;
token.SIN = pairingEntity.SIN;
token.Label = pairingEntity.Label;
token.PairingTime = DateTimeOffset.UtcNow;
tx.Insert($"TbP_{pairedId}", token.Value, ToBytes(token));
tx.Insert($"T_{pairingEntity.SIN}", pairingEntity.Facade, ToBytes(token));
}
finally
{
tx.Commit();
}
Id = pairingCodeId,
DateCreated = now,
Expiration = expiration,
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
});
await ctx.SaveChangesAsync();
}
return Task.FromResult(true);
return pairingCodeId;
}
public Task<BitTokenEntity[]> GetTokensByPairedIdAsync(string pairedId)
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
{
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
using(var tx = _Engine.GetTransaction())
using(var ctx = _Factory.CreateContext())
{
tx.ValuesLazyLoadingIsOn = false;
foreach(var row in tx.SelectForward<string, byte[]>($"TbP_{pairedId}"))
{
tokens.Add(ToObject<BitTokenEntity>(row.Value));
}
}
return Task.FromResult(tokens.ToArray());
}
public Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{
using(var tx = _Engine.GetTransaction())
{
var row = tx.Select<string, byte[]>("PairingCodes", pairingCode);
if(row == null || !row.Exists)
return Task.FromResult<PairingCodeEntity>(null);
var pairingEntity = ToObject<PairingCodeEntity>(row.Value);
if(pairingEntity.IsExpired())
return Task.FromResult<PairingCodeEntity>(null);
return Task.FromResult(pairingEntity);
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
pairingCode.Label = pairingCodeEntity.Label;
pairingCode.Facade = pairingCodeEntity.Facade;
await ctx.SaveChangesAsync();
return CreatePairingCodeEntity(pairingCode);
}
}
public Task<PairingCodeEntity> AddPairingCodeAsync(PairingCodeEntity pairingCodeEntity)
public async Task<bool> PairWithStoreAsync(string pairingCodeId, string storeId)
{
pairingCodeEntity = Clone(pairingCodeEntity);
pairingCodeEntity.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(6));
using(var tx = _Engine.GetTransaction())
using(var ctx = _Factory.CreateContext())
{
tx.Insert("PairingCodes", pairingCodeEntity.Id, ToBytes(pairingCodeEntity));
tx.Commit();
}
return Task.FromResult(pairingCodeEntity);
}
private byte[] ToBytes<T>(T obj)
{
return ZipUtils.Zip(JsonConvert.SerializeObject(obj));
}
private T ToObject<T>(byte[] value)
{
return JsonConvert.DeserializeObject<T>(ZipUtils.Unzip(value));
}
private T Clone<T>(T obj)
{
return ToObject<T>(ToBytes(obj));
}
public async Task<bool> DeleteToken(string sin, string tokenName, string storeId)
{
var token = await GetToken(sin, tokenName);
if(token == null || (token.PairedId != null && token.PairedId != storeId))
return false;
using(var tx = _Engine.GetTransaction())
{
tx.RemoveKey<string>($"T_{sin}", tokenName);
if(token.PairedId != null)
tx.RemoveKey<string>($"TbP_" + token.PairedId, token.Value);
tx.Commit();
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
pairingCode.StoreDataId = storeId;
await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
}
return true;
}
private Task<BitTokenEntity> GetToken(string sin, string tokenName)
public async Task<bool> PairWithSINAsync(string pairingCodeId, string sin)
{
using(var tx = _Engine.GetTransaction())
using(var ctx = _Factory.CreateContext())
{
tx.ValuesLazyLoadingIsOn = true;
var row = tx.Select<string, byte[]>($"T_{sin}", tokenName);
if(row == null || !row.Exists)
return Task.FromResult<BitTokenEntity>(null);
var token = ToObject<BitTokenEntity>(row.Value);
if(!token.Active)
return Task.FromResult<BitTokenEntity>(null);
return Task.FromResult(token);
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
return false;
pairingCode.SIN = sin;
await ActivateIfComplete(ctx, pairingCode);
await ctx.SaveChangesAsync();
}
return true;
}
private async Task ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
{
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
{
ctx.PairingCodes.Remove(pairingCode);
await ctx.PairedSINData.AddAsync(new PairedSINData()
{
Id = pairingCode.TokenValue,
PairingTime = DateTime.UtcNow,
Facade = pairingCode.Facade,
Label = pairingCode.Label,
StoreDataId = pairingCode.StoreDataId,
SIN = pairingCode.SIN
});
}
}
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
{
using(var ctx = _Factory.CreateContext())
{
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
.Select(c => CreateTokenEntity(c))
.ToArray();
}
}
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
{
using(var ctx = _Factory.CreateContext())
{
return CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
}
}
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
{
return new PairingCodeEntity()
{
Facade = data.Facade,
Id = data.Id,
Label = data.Label,
Expiration = data.Expiration,
CreatedTime = data.DateCreated,
TokenValue = data.TokenValue,
SIN = data.SIN
};
}
public async Task<bool> DeleteToken(string tokenId)
{
using(var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
if(token == null)
return false;
ctx.PairedSINData.Remove(token);
await ctx.SaveChangesAsync();
return true;
}
}
public async Task<BitTokenEntity> GetToken(string tokenId)
{
using(var ctx = _Factory.CreateContext())
{
var token = await ctx.PairedSINData.FindAsync(tokenId);
return CreateTokenEntity(token);
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.5</Version>
<Version>1.0.0.8</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -22,9 +22,9 @@
<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="NBitpayClient" Version="1.0.0.9" />
<PackageReference Include="NBitpayClient" Version="1.0.0.10" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.16" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.17" />
<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

@ -51,7 +51,6 @@ namespace BTCPayServer.Configuration
}
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db);
TokenRepository = new TokenRepository(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db);
@ -70,10 +69,6 @@ namespace BTCPayServer.Configuration
}
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
db = new DBreezeEngine(CreateDBPath(opts, "AddressMapping"));
_Resources.Add(db);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)
@ -103,10 +98,6 @@ namespace BTCPayServer.Configuration
get;
private set;
}
public TokenRepository TokenRepository
{
get; set;
}
public InvoiceRepository InvoiceRepository
{
get;

View File

@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("tokens")]
public async Task<GetTokensResponse> GetTokens()
public async Task<GetTokensResponse> Tokens()
{
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
return new GetTokensResponse(tokens);
@ -29,33 +29,51 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("tokens")]
public async Task<DataWrapper<List<PairingCodeResponse>>> GetPairingCode([FromBody] PairingCodeRequest token)
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
var now = DateTimeOffset.UtcNow;
var pairingEntity = new PairingCodeEntity()
PairingCodeEntity pairingEntity = null;
if(string.IsNullOrEmpty(request.PairingCode))
{
Facade = token.Facade,
Label = token.Label,
SIN = token.Id,
PairingTime = now,
PairingExpiration = now + TimeSpan.FromMinutes(15)
};
var grantedToken = await _TokenRepository.CreateToken(token.Id, token.Facade);
pairingEntity.Token = grantedToken.Name;
pairingEntity = await _TokenRepository.AddPairingCodeAsync(pairingEntity);
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required");
if(string.IsNullOrEmpty(request.Facade))
throw new BitpayHttpException(400, "'facade' property is required");
var pairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.PairWithSINAsync(pairingCode, request.Id);
pairingEntity = await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = pairingCode,
Facade = request.Facade,
Label = request.Label
});
}
else
{
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
if(string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
pairingEntity.SIN = sin;
if(!await _TokenRepository.PairWithSINAsync(request.PairingCode, sin))
throw new BitpayHttpException(400, "Unknown pairing code");
}
var pairingCodes = new List<PairingCodeResponse>
{
new PairingCodeResponse()
{
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.PairingExpiration,
DateCreated = pairingEntity.PairingTime,
Facade = grantedToken.Name,
Token = grantedToken.Value,
Label = pairingEntity.Label
}
};
new PairingCodeResponse()
{
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,
Facade = pairingEntity.Facade,
Token = pairingEntity.TokenValue,
Label = pairingEntity.Label
}
};
return DataWrapper.Create(pairingCodes);
}
}

View File

@ -0,0 +1,116 @@
using BTCPayServer.Logging;
using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using Microsoft.Extensions.Logging;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class CallbackController : Controller
{
public class CallbackSettings
{
public string Token
{
get; set;
}
}
SettingsRepository _Settings;
Network _Network;
InvoiceWatcher _Watcher;
ExplorerClient _Explorer;
public CallbackController(SettingsRepository repo,
ExplorerClient explorer,
InvoiceWatcher watcher,
Network network)
{
_Settings = repo;
_Network = network;
_Watcher = watcher;
_Explorer = explorer;
}
[Route("callbacks/transactions")]
[HttpPost]
public async Task NewTransaction(string token)
{
await AssertToken(token);
Logs.PayServer.LogInformation("New transaction callback");
//We don't want to register all the json converter at MVC level, so we parse here
var serializer = new NBXplorer.Serializer(_Network);
var content = await new StreamReader(Request.Body, new UTF8Encoding(false), false, 1024, true).ReadToEndAsync();
var match = serializer.ToObject<TransactionMatch>(content);
foreach(var output in match.Outputs)
{
await _Watcher.NotifyReceived(output.ScriptPubKey);
}
}
[Route("callbacks/blocks")]
[HttpPost]
public async Task NewBlock(string token)
{
await AssertToken(token);
Logs.PayServer.LogInformation("New block callback");
await _Watcher.NotifyBlock();
}
private async Task AssertToken(string token)
{
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
if(await GetToken() != token)
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
}
public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token);
}
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
{
var uri = await GetCallbackUriAsync(request);
await _Explorer.SubscribeToWalletAsync(uri, derivationScheme);
}
private async Task<string> GetToken()
{
var callback = await _Settings.GetSettingAsync<CallbackSettings>();
if(callback == null)
{
callback = new CallbackSettings() { Token = Guid.NewGuid().ToString() };
await _Settings.UpdateSetting(callback);
}
var token = callback.Token;
return token;
}
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
}
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
{
var uri = await GetCallbackBlockUriAsync(request);
await _Explorer.SubscribeToBlocksAsync(uri);
return uri;
}
}
}

View File

@ -83,26 +83,26 @@ namespace BTCPayServer.Controllers
if(facade == null)
throw new ArgumentNullException(nameof(facade));
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).Where(t => t.Active).ToArray();
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().SIN)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if(expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {facade} for SIN {this.GetBitIdentity().SIN}");
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Name).Concat(new[] { "user" }).FirstOrDefault()}` facade");
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if(token.Name == Facade.Merchant.ToString())
if(token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if(token.Name == Facade.PointOfSale.ToString())
if(token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
@ -111,7 +111,7 @@ namespace BTCPayServer.Controllers
private async Task<StoreData> FindStore(BitTokenEntity bitToken)
{
var store = await _StoreRepository.FindStore(bitToken.PairedId);
var store = await _StoreRepository.FindStore(bitToken.StoreId);
if(store == null)
throw new BitpayHttpException(401, "Unknown store");
return store;

View File

@ -33,7 +33,10 @@ using BTCPayServer.Servcices.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers
{
@ -96,7 +99,7 @@ namespace BTCPayServer.Controllers
entity.TxFee = (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(derivationStrategy);
entity.DepositAddress = await _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
@ -105,6 +108,11 @@ namespace BTCPayServer.Controllers
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)
{
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));

View File

@ -26,6 +26,7 @@ namespace BTCPayServer.Controllers
public StoresController(
StoreRepository repo,
TokenRepository tokenRepo,
CallbackController callbackController,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
@ -39,8 +40,10 @@ namespace BTCPayServer.Controllers
_Wallet = wallet;
_Env = env;
_Network = network;
_CallbackController = callbackController;
}
Network _Network;
CallbackController _CallbackController;
BTCPayWallet _Wallet;
AccessTokenController _TokenController;
StoreRepository _Repo;
@ -86,7 +89,7 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(s.DerivationStrategy)).ToArray();
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray();
for(int i = 0; i < stores.Length; i++)
{
@ -156,7 +159,9 @@ namespace BTCPayServer.Controllers
needUpdate = true;
try
{
await _Wallet.TrackAsync(model.DerivationScheme);
var strategy = ParseDerivationStrategy(model.DerivationScheme);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
store.DerivationStrategy = model.DerivationScheme;
}
catch
@ -192,16 +197,21 @@ namespace BTCPayServer.Controllers
}
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
{
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
}
[HttpGet]
[Route("{storeId}/Tokens")]
public async Task<IActionResult> ListTokens(string storeId)
{
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByPairedIdAsync(storeId);
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(storeId);
model.StatusMessage = StatusMessage;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Name,
Facade = t.Facade,
Label = t.Label,
SIN = t.SIN,
Id = t.Value
@ -219,16 +229,34 @@ namespace BTCPayServer.Controllers
return View(model);
}
var pairingCode = await _TokenController.GetPairingCode(new PairingCodeRequest()
var tokenRequest = new TokenRequest()
{
Facade = model.Facade,
Label = model.Label,
Id = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
});
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey))
};
string pairingCode = null;
if(model.PublicKey == null)
{
tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync();
await _TokenRepository.UpdatePairingCode(new PairingCodeEntity()
{
Id = tokenRequest.PairingCode,
Facade = model.Facade,
Label = model.Label,
});
await _TokenRepository.PairWithStoreAsync(tokenRequest.PairingCode, storeId);
pairingCode = tokenRequest.PairingCode;
}
else
{
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
}
return RedirectToAction(nameof(RequestPairing), new
{
pairingCode = pairingCode.Data[0].PairingCode,
pairingCode = pairingCode,
selectedStore = storeId
});
}
@ -239,22 +267,21 @@ namespace BTCPayServer.Controllers
{
var model = new CreateTokenViewModel();
model.Facade = "merchant";
if(_Env.IsDevelopment())
{
model.PublicKey = new Key().PubKey.ToHex();
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
[Route("{storeId}/Tokens/Delete")]
public async Task<IActionResult> DeleteToken(string storeId, string name, string sin)
public async Task<IActionResult> DeleteToken(string storeId, string tokenId)
{
if(await _TokenRepository.DeleteToken(sin, name, storeId))
StatusMessage = "Token revoked";
else
var token = await _TokenRepository.GetToken(tokenId);
if(token == null ||
token.StoreId != storeId ||
!await _TokenRepository.DeleteToken(tokenId))
StatusMessage = "Failure to revoke this token";
else
StatusMessage = "Token revoked";
return RedirectToAction(nameof(ListTokens));
}
@ -277,7 +304,7 @@ namespace BTCPayServer.Controllers
Id = pairing.Id,
Facade = pairing.Facade,
Label = pairing.Label,
SIN = pairing.SIN,
SIN = pairing.SIN ?? "Server-Initiated Pairing",
SelectedStore = selectedStore ?? stores.FirstOrDefault()?.Id,
Stores = stores.Select(s => new PairingModel.StoreViewModel()
{
@ -294,11 +321,14 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{
var store = await _Repo.FindStore(selectedStore, GetUserId());
if(store == null)
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if(store == null || pairing == null)
return NotFound();
if(pairingCode != null && await _TokenRepository.PairWithAsync(pairingCode, store.Id))
if(pairingCode != null && await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id))
{
StatusMessage = "Pairing is successfull";
if(pairing.SIN == null)
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id

View File

@ -26,6 +26,10 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<PendingInvoiceData> PendingInvoices
{
get; set;
}
public DbSet<RefundAddressesData> RefundAddresses
{
get; set;
@ -56,6 +60,17 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<PairingCodeData> PairingCodes
{
get; set;
}
public DbSet<PairedSINData> PairedSINData
{
get; set;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
@ -94,6 +109,15 @@ namespace BTCPayServer.Data
builder.Entity<AddressInvoiceData>()
.HasKey(o => o.Address);
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
builder.Entity<PairedSINData>(b =>
{
b.HasIndex(o => o.SIN);
b.HasIndex(o => o.StoreDataId);
});
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PairedSINData
{
public string Id
{
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public string Label
{
get;
set;
}
public DateTimeOffset PairingTime
{
get;
set;
}
public string SIN
{
get; set;
}
}
}

View File

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PairingCodeData
{
public string Id
{
get; set;
}
public string Facade
{
get; set;
}
public string StoreDataId
{
get; set;
}
public DateTimeOffset Expiration
{
get;
set;
}
public string Label
{
get;
set;
}
public string SIN
{
get;
set;
}
public DateTime DateCreated
{
get;
set;
}
public string TokenValue
{
get;
set;
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class PendingInvoiceData
{
public string Id
{
get; set;
}
}
}

View File

@ -38,10 +38,10 @@ namespace BTCPayServer
}
public static BitIdentity GetBitIdentity(this Controller controller)
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
{
if(!(controller.User.Identity is BitIdentity))
throw new UnauthorizedAccessException("no-bitid");
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
return (BitIdentity)controller.User.Identity;
}
}

View File

@ -32,6 +32,7 @@ using BTCPayServer.Models;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
namespace BTCPayServer.Hosting
{
@ -107,7 +108,7 @@ namespace BTCPayServer.Hosting
runtime.Configure(o.GetRequiredService<BTCPayServerOptions>());
return runtime;
});
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().TokenRepository);
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().InvoiceRepository);
services.TryAddSingleton<Network>(o => o.GetRequiredService<BTCPayServerOptions>().Network);
services.TryAddSingleton<ApplicationDbContextFactory>(o => o.GetRequiredService<BTCPayServerRuntime>().DBFactory);
@ -139,6 +140,7 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();
services.AddTransient<CallbackController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();

View File

@ -21,6 +21,7 @@ using BTCPayServer.Configuration;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
namespace BTCPayServer.Hosting
{
@ -28,14 +29,27 @@ namespace BTCPayServer.Hosting
{
TokenRepository _TokenRepository;
RequestDelegate _Next;
public BTCPayMiddleware(RequestDelegate next, TokenRepository tokenRepo)
CallbackController _CallbackController;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
CallbackController callbackController)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController;
}
bool _Registered;
public async Task Invoke(HttpContext httpContext)
{
if(!_Registered)
{
var callback = await _CallbackController.RegisterCallbackBlockUriAsync(httpContext.Request);
Logs.PayServer.LogInformation($"Registering block callback to " + callback);
_Registered = true;
}
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
@ -92,8 +106,9 @@ namespace BTCPayServer.Hosting
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;
using(var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, true))
using(var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true))
{
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
writer.Write(result);
await writer.FlushAsync();

View File

@ -33,6 +33,7 @@ using Hangfire.Dashboard;
using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Hosting
{
@ -59,17 +60,16 @@ namespace BTCPayServer.Hosting
{
get; set;
}
public void ConfigureServices(IServiceCollection services)
{
// Big hack, tests fails because Hangfire fail at initializing at the second test run
services.ConfigureBTCPayServer(Configuration);
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Big hack, tests fails because Hangfire fail at initializing at the second test run
AddHangfireFix(services);
services.AddBTCPayServer();
services.AddMvc();

View File

@ -0,0 +1,444 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Servcices.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("20171010082424_Tokens")]
partial class Tokens
{
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<string>("Name");
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>("Name");
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.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[]>("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,67 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class Tokens : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PairedSINData",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
PairingTime = table.Column<DateTimeOffset>(nullable: false),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PairedSINData", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PairingCodes",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
DateCreated = table.Column<DateTime>(nullable: false),
Expiration = table.Column<DateTimeOffset>(nullable: false),
Facade = table.Column<string>(type: "TEXT", nullable: true),
Label = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
SIN = table.Column<string>(type: "TEXT", nullable: true),
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PairingCodes", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_PairedSINData_SIN",
table: "PairedSINData",
column: "SIN");
migrationBuilder.CreateIndex(
name: "IX_PairedSINData_StoreDataId",
table: "PairedSINData",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PairedSINData");
migrationBuilder.DropTable(
name: "PairingCodes");
}
}
}

View File

@ -0,0 +1,450 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Servcices.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("20171012020112_PendingInvoices")]
partial class PendingInvoices
{
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[]>("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,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class PendingInvoices : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if(SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Name",
table: "PairingCodes");
migrationBuilder.DropColumn(
name: "Name",
table: "PairedSINData");
}
migrationBuilder.CreateTable(
name: "PendingInvoices",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PendingInvoices", x => x.Id);
});
}
private bool SupportDropColumn(string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PendingInvoices");
migrationBuilder.AddColumn<string>(
name: "Name",
table: "PairingCodes",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Name",
table: "PairedSINData",
nullable: true);
}
}
}

View File

@ -62,6 +62,54 @@ namespace BTCPayServer.Migrations
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")
@ -78,6 +126,16 @@ namespace BTCPayServer.Migrations
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")

View File

@ -39,13 +39,13 @@ namespace BTCPayServer.Models
{
JObject item = new JObject();
jarray.Add(item);
JProperty jProp = new JProperty(token.Name);
JProperty jProp = new JProperty(token.Facade);
item.Add(jProp);
jProp.Value = token.Value;
}
context.HttpContext.Response.Headers.Add("Content-Type", new Microsoft.Extensions.Primitives.StringValues("application/json"));
var str = JsonConvert.SerializeObject(jobj);
using(var writer = new StreamWriter(context.HttpContext.Response.Body, Encoding.UTF8, 1024 * 10, true))
using(var writer = new StreamWriter(context.HttpContext.Response.Body, new UTF8Encoding(false), 1024 * 10, true))
{
await writer.WriteLineAsync(str);
}

View File

@ -6,7 +6,7 @@ using NBitcoin;
namespace BTCPayServer.Models
{
public class PairingCodeRequest
public class TokenRequest
{
[JsonProperty(PropertyName = "id")]
public string Id
@ -34,6 +34,12 @@ namespace BTCPayServer.Models
{
get; set;
}
[JsonProperty(PropertyName = "pairingCode")]
public string PairingCode
{
get; set;
}
}
public class PairingCodeResponse

View File

@ -56,38 +56,31 @@ namespace BTCPayServer.Servcices.Invoices
public Task AddPendingInvoice(string invoiceId)
public async Task AddPendingInvoice(string invoiceId)
{
using(var tx = _Engine.GetTransaction())
using(var ctx = _ContextFactory.CreateContext())
{
tx.Insert<string, byte[]>("T-Pending", invoiceId, new byte[0]);
tx.Commit();
ctx.PendingInvoices.Add(new PendingInvoiceData() { Id = invoiceId });
await ctx.SaveChangesAsync();
}
return Task.FromResult(true);
}
public Task RemovePendingInvoice(string invoiceId)
public async Task RemovePendingInvoice(string invoiceId)
{
using(var tx = _Engine.GetTransaction())
using(var ctx = _ContextFactory.CreateContext())
{
tx.RemoveKey("T-Pending", invoiceId);
tx.Commit();
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
await ctx.SaveChangesAsync();
}
return Task.FromResult(true);
}
public string[] GetPendingInvoices()
public async Task<string[]> GetPendingInvoices()
{
List<string> pending = new List<string>();
using(var tx = _Engine.GetTransaction())
using(var ctx = _ContextFactory.CreateContext())
{
foreach(var row in tx.SelectForward<string, byte[]>("T-Pending"))
{
pending.Add(row.Key);
}
return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync();
}
return pending.ToArray();
}
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)

View File

@ -36,9 +36,23 @@ namespace BTCPayServer.Servcices.Invoices
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
}
private async Task StartWatchInvoice(string invoiceId)
public async Task NotifyReceived(Script scriptPubKey)
{
Logs.PayServer.LogInformation("Watching invoice " + invoiceId);
var invoice = await _Wallet.GetInvoiceId(scriptPubKey);
_WatchRequests.Add(invoice);
}
public async Task NotifyBlock()
{
foreach(var invoice in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(invoice);
}
}
private async Task UpdateInvoice(string invoiceId)
{
Logs.PayServer.LogInformation("Updating invoice " + invoiceId);
UTXOChanges changes = null;
while(true)
{
@ -53,7 +67,8 @@ namespace BTCPayServer.Servcices.Invoices
if(result.NeedSave)
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
if(stateBefore != invoice.Status)
var changed = stateBefore != invoice.Status;
if(changed)
{
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
}
@ -64,6 +79,9 @@ namespace BTCPayServer.Servcices.Invoices
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break;
}
if(!changed || _Cts.Token.IsCancellationRequested)
break;
}
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
{
@ -96,7 +114,7 @@ namespace BTCPayServer.Servcices.Invoices
if(invoice.Status == "new" || invoice.Status == "paidPartial")
{
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, false, _Cts.Token).ConfigureAwait(false);
changes = await _ExplorerClient.SyncAsync(strategy, changes, true, _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();
@ -232,34 +250,39 @@ namespace BTCPayServer.Servcices.Invoices
Thread _Thread;
TaskCompletionSource<bool> _RunningTask;
CancellationTokenSource _Cts;
Timer _UpdatePendingInvoices;
public Task StartAsync(CancellationToken cancellationToken)
{
foreach(var pending in _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
_Thread.Start();
_UpdatePendingInvoices = new Timer(async s =>
{
foreach(var pending in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
}, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
return Task.CompletedTask;
}
void Run()
{
Logs.PayServer.LogInformation("Start watching invoices");
List<Task> watching = new List<Task>();
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
try
{
foreach(var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
{
watching.Add(StartWatchInvoice(item));
foreach(var task in watching.ToList())
var localItem = item;
// If the invoice is already updating, ignore
Lazy<Task> updateInvoice =new Lazy<Task>(() => UpdateInvoice(localItem), false);
if(updating.TryAdd(item, updateInvoice))
{
if(task.Status != TaskStatus.Running)
{
watching.Remove(task);
}
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
}
}
}
@ -267,7 +290,7 @@ namespace BTCPayServer.Servcices.Invoices
{
try
{
Task.WaitAll(watching.ToArray());
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
}
catch(AggregateException) { }
_RunningTask.TrySetResult(true);
@ -287,6 +310,7 @@ namespace BTCPayServer.Servcices.Invoices
public Task StopAsync(CancellationToken cancellationToken)
{
_UpdatePendingInvoices.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}

View File

@ -14,7 +14,6 @@ namespace BTCPayServer.Services.Wallets
{
private ExplorerClient _Client;
private Serializer _Serializer;
private DerivationStrategyFactory _DerivationStrategyFactory;
ApplicationDbContextFactory _DBFactory;
public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory)
@ -26,19 +25,18 @@ namespace BTCPayServer.Services.Wallets
_Client = client;
_DBFactory = factory;
_Serializer = new NBXplorer.Serializer(_Client.Network);
_DerivationStrategyFactory = new DerivationStrategyFactory(_Client.Network);
}
public async Task<BitcoinAddress> ReserveAddressAsync(string walletIdentifier)
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
var pathInfo = await _Client.GetUnusedAsync(_DerivationStrategyFactory.Parse(walletIdentifier), DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(_DerivationStrategyFactory.Network);
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network);
}
public Task TrackAsync(string walletIdentifier)
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
{
return _Client.TrackAsync(_DerivationStrategyFactory.Parse(walletIdentifier));
await _Client.TrackAsync(derivationStrategy);
}
public async Task<string> GetInvoiceId(Script scriptPubKey)
@ -74,9 +72,9 @@ namespace BTCPayServer.Services.Wallets
return Task.WhenAll(tasks);
}
public async Task<Money> GetBalance(string derivationStrategy)
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
{
var result = await _Client.SyncAsync(_DerivationStrategyFactory.Parse(derivationStrategy), null, true);
var result = await _Client.SyncAsync(derivationStrategy, null, true);
return result.Confirmed.UTXOs.Select(u => u.Output.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Output.Value))
.Sum();

View File

@ -17,6 +17,7 @@
</div>
<div class="form-group">
<label asp-for="PublicKey"></label>
<small class="text-muted">Keep empty for server-initiated pairing</small>
<input asp-for="PublicKey" class="form-control" />
<span asp-validation-for="PublicKey" class="text-danger"></span>
</div>

View File

@ -27,8 +27,7 @@
<td>@token.Facade</td>
<td>
<form asp-action="DeleteToken" method="post">
<input type="hidden" name="name" value="@token.Facade">
<input type="hidden" name="sin" value="@token.SIN">
<input type="hidden" name="tokenId" value="@token.Id">
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
</form>
</td>