Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
e3c5efa929 | |||
8db9d93d23 | |||
517bb94b8b | |||
c804f55d82 | |||
c96a25c9b9 | |||
4c57726be7 | |||
8f723d7131 | |||
a7e10c0fb9 | |||
15e73e1cad | |||
a17192ee99 | |||
27200d1fb0 | |||
9ddceae824 | |||
d1961e0938 | |||
033432d6fb | |||
c98f0ba55b | |||
76993d2532 | |||
62e3f2d8e1 | |||
9806cab090 | |||
0d9fbe2d41 | |||
016db76306 | |||
d469084596 | |||
bb4decd522 | |||
caca8e81c2 | |||
bae08b6966 | |||
212a816598 | |||
1fd9cb5e2a | |||
6d3ea65e03 | |||
7d8c3c1c81 | |||
47ddbff817 | |||
783132a012 | |||
f456d62d3c | |||
8b4e572e16 | |||
51ef3ec656 | |||
35a3618a42 | |||
aa59725176 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,7 +47,6 @@ dlldata.c
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
|
@ -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);
|
||||
});
|
||||
})
|
||||
@ -103,13 +108,18 @@ namespace BTCPayServer.Tests
|
||||
_Host.Start();
|
||||
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
|
||||
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
|
||||
watcher.PollInterval = TimeSpan.FromMilliseconds(50);
|
||||
watcher.PollInterval = TimeSpan.FromMilliseconds(500);
|
||||
}
|
||||
|
||||
public BTCPayServerRuntime Runtime
|
||||
{
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
BTCPayServer.Tests/Properties/launchSettings.json
Normal file
7
BTCPayServer.Tests/Properties/launchSettings.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"profiles": {
|
||||
"BTCPayServer.Tests": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
32
BTCPayServer.Tests/README.md
Normal file
32
BTCPayServer.Tests/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
# How to run the tests
|
||||
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
```
|
||||
|
||||
You can run the tests while it is running through your favorite IDE, or with
|
||||
|
||||
```
|
||||
dotnet test
|
||||
```
|
||||
|
||||
Once you want to stop
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
You can run the tests inside a container by running
|
||||
|
||||
```
|
||||
docker-compose run --rm tests
|
||||
```
|
||||
|
||||
The Bitcoin RPC server is exposed to the host, for example, you can send 0.23111090 BTC to mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf.
|
||||
|
||||
```
|
||||
bitcoin-cli -regtest -rpcport=43782 -rpcuser=ceiwHEbqWI83 -rpcpassword=DwubwWsoo3 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
@ -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;
|
||||
|
@ -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()
|
||||
{
|
||||
ExtPubKey = extKey.Neuter().ToString(),
|
||||
SpeedPolicy = SpeedPolicy.MediumSpeed
|
||||
});
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
|
||||
public Bitpay BitPay
|
||||
|
@ -13,6 +13,9 @@ using BTCPayServer.Servcices.Invoices;
|
||||
using Newtonsoft.Json;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Authentication;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -63,10 +66,11 @@ 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()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
@ -97,6 +101,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 +109,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 +142,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 +156,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();
|
||||
@ -137,13 +169,32 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CantPairTwiceWithSamePubkey()
|
||||
{
|
||||
using(var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.Register();
|
||||
var store = acc.CreateStore();
|
||||
var pairingCode = acc.BitPay.RequestClientAuthorization("test", Facade.Merchant);
|
||||
Assert.IsType<RedirectToActionResult>(store.Pair(pairingCode.ToString(), acc.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||
var store2 = acc.CreateStore();
|
||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
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 +252,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 +265,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 +277,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
});
|
||||
@ -232,6 +286,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
tester.SimulateCallback();
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
});
|
||||
@ -253,6 +308,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 +319,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);
|
||||
|
@ -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.12
|
||||
image: nicolasdorier/nbxplorer:1.0.0.18
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using DBreeze;
|
||||
using BTCPayServer.Data;
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
@ -6,175 +7,192 @@ 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 enum PairingResult
|
||||
{
|
||||
Partial,
|
||||
Complete,
|
||||
ReusedKey,
|
||||
Expired
|
||||
}
|
||||
|
||||
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
|
||||
Id = pairingCodeId,
|
||||
DateCreated = now,
|
||||
Expiration = expiration,
|
||||
TokenValue = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32))
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
return pairingCodeId;
|
||||
}
|
||||
|
||||
public async Task<PairingCodeEntity> UpdatePairingCode(PairingCodeEntity pairingCodeEntity)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeEntity.Id);
|
||||
pairingCode.Label = pairingCodeEntity.Label;
|
||||
pairingCode.Facade = pairingCodeEntity.Facade;
|
||||
await ctx.SaveChangesAsync();
|
||||
return CreatePairingCodeEntity(pairingCode);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingResult> PairWithStoreAsync(string pairingCodeId, string storeId)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return PairingResult.Expired;
|
||||
pairingCode.StoreDataId = storeId;
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PairingResult> PairWithSINAsync(string pairingCodeId, string sin)
|
||||
{
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var pairingCode = await ctx.PairingCodes.FindAsync(pairingCodeId);
|
||||
if(pairingCode == null || pairingCode.Expiration < DateTimeOffset.UtcNow)
|
||||
return PairingResult.Expired;
|
||||
pairingCode.SIN = sin;
|
||||
var result = await ActivateIfComplete(ctx, pairingCode);
|
||||
await ctx.SaveChangesAsync();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<PairingResult> ActivateIfComplete(ApplicationDbContext ctx, PairingCodeData pairingCode)
|
||||
{
|
||||
if(!string.IsNullOrEmpty(pairingCode.SIN) && !string.IsNullOrEmpty(pairingCode.StoreDataId))
|
||||
{
|
||||
ctx.PairingCodes.Remove(pairingCode);
|
||||
|
||||
// Can have concurrency issues... but no harm can be done
|
||||
var alreadyUsed = await ctx.PairedSINData.Where(p => p.SIN == pairingCode.SIN && p.StoreDataId != pairingCode.StoreDataId).AnyAsync();
|
||||
if(alreadyUsed)
|
||||
return PairingResult.ReusedKey;
|
||||
await ctx.PairedSINData.AddAsync(new PairedSINData()
|
||||
{
|
||||
tx.Commit();
|
||||
}
|
||||
Id = pairingCode.TokenValue,
|
||||
PairingTime = DateTime.UtcNow,
|
||||
Facade = pairingCode.Facade,
|
||||
Label = pairingCode.Label,
|
||||
StoreDataId = pairingCode.StoreDataId,
|
||||
SIN = pairingCode.SIN
|
||||
});
|
||||
return PairingResult.Complete;
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
return PairingResult.Partial;
|
||||
}
|
||||
|
||||
public Task<BitTokenEntity[]> GetTokensByPairedIdAsync(string pairedId)
|
||||
{
|
||||
List<BitTokenEntity> tokens = new List<BitTokenEntity>();
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
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)
|
||||
public async Task<BitTokenEntity[]> GetTokensByStoreIdAsync(string storeId)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
using(var ctx = _Factory.CreateContext())
|
||||
{
|
||||
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);
|
||||
return (await ctx.PairedSINData.Where(p => p.StoreDataId == storeId).ToListAsync())
|
||||
.Select(c => CreateTokenEntity(c))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
public Task<PairingCodeEntity> AddPairingCodeAsync(PairingCodeEntity pairingCodeEntity)
|
||||
|
||||
public async Task<PairingCodeEntity> GetPairingAsync(string pairingCode)
|
||||
{
|
||||
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 CreatePairingCodeEntity(await ctx.PairingCodes.FindAsync(pairingCode));
|
||||
}
|
||||
return Task.FromResult(pairingCodeEntity);
|
||||
}
|
||||
|
||||
private byte[] ToBytes<T>(T obj)
|
||||
private PairingCodeEntity CreatePairingCodeEntity(PairingCodeData data)
|
||||
{
|
||||
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())
|
||||
return new PairingCodeEntity()
|
||||
{
|
||||
tx.RemoveKey<string>($"T_{sin}", tokenName);
|
||||
if(token.PairedId != null)
|
||||
tx.RemoveKey<string>($"TbP_" + token.PairedId, token.Value);
|
||||
tx.Commit();
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Task<BitTokenEntity> GetToken(string sin, string tokenName)
|
||||
public async Task<BitTokenEntity> GetToken(string tokenId)
|
||||
{
|
||||
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 token = await ctx.PairedSINData.FindAsync(tokenId);
|
||||
return CreateTokenEntity(token);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.3</Version>
|
||||
<Version>1.0.0.15</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.11" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.12" />
|
||||
<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" />
|
||||
|
@ -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,11 +69,6 @@ namespace BTCPayServer.Configuration
|
||||
}
|
||||
DBFactory = dbContext;
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, db, Network);
|
||||
|
||||
|
||||
db = new DBreezeEngine(CreateDBPath(opts, "AddressMapping"));
|
||||
_Resources.Add(db);
|
||||
Wallet = new BTCPayWallet(Explorer, db);
|
||||
}
|
||||
|
||||
private static string CreateDBPath(BTCPayServerOptions opts, string name)
|
||||
@ -104,20 +98,11 @@ namespace BTCPayServer.Configuration
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public TokenRepository TokenRepository
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public BTCPayWallet Wallet
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ApplicationDbContextFactory DBFactory
|
||||
{
|
||||
get;
|
||||
|
@ -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,53 @@ 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;
|
||||
|
||||
var result = await _TokenRepository.PairWithSINAsync(request.PairingCode, sin);
|
||||
if(result != PairingResult.Complete && result != PairingResult.Partial)
|
||||
throw new BitpayHttpException(400, $"Error while pairing ({result})");
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
116
BTCPayServer/Controllers/CallbackController.cs
Normal file
116
BTCPayServer/Controllers/CallbackController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,25 +11,43 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await CreateInvoiceCore(invoice, store);
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
@ -44,7 +62,6 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices")]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<InvoiceResponse[]>> GetInvoices(
|
||||
string token,
|
||||
DateTimeOffset? dateStart = null,
|
||||
@ -83,26 +100,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 +128,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;
|
||||
|
@ -3,6 +3,7 @@ using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
@ -17,14 +18,92 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
|
||||
[HttpPost]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId, string command)
|
||||
{
|
||||
if(command == "refresh")
|
||||
{
|
||||
await _Watcher.WatchAsync(invoiceId, true);
|
||||
}
|
||||
StatusMessage = "Invoice is state is being refreshed, please refresh the page soon...";
|
||||
return RedirectToAction(nameof(Invoice), new
|
||||
{
|
||||
invoiceId = invoiceId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> Invoice(string invoiceId)
|
||||
{
|
||||
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId
|
||||
})).FirstOrDefault();
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
InvoiceDetailsModel model = new InvoiceDetailsModel()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Rate = invoice.Rate,
|
||||
Fiat = dto.Price + " " + dto.Currency,
|
||||
BTC = invoice.GetTotalCryptoDue().ToString() + " BTC",
|
||||
BTCDue = invoice.GetCryptoDue().ToString() + " BTC",
|
||||
BTCPaid = invoice.GetTotalPaid().ToString() + " BTC",
|
||||
NetworkFee = invoice.GetNetworkFee().ToString() + " BTC",
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
BitcoinAddress = invoice.DepositAddress,
|
||||
PaymentUrl = dto.PaymentUrls.BIP72
|
||||
};
|
||||
|
||||
var payments = invoice
|
||||
.Payments
|
||||
.Select(async payment =>
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
|
||||
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId)
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if(invoice == null)
|
||||
return NotFound();
|
||||
@ -55,7 +134,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var expiration = TimeSpan.FromSeconds((double)model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return View(model);
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
@ -167,15 +246,15 @@ namespace BTCPayServer.Controllers
|
||||
ItemDesc = model.ItemDesc,
|
||||
FullNotifications = true,
|
||||
BuyerEmail = model.BuyerEmail,
|
||||
}, store);
|
||||
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
|
||||
StatusMessage = $"Invoice {result.Data.Id} just created!";
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
private async Task<SelectList> GetStores(string userId, string storeId = null)
|
||||
{
|
||||
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
return new SelectList(await _StoreRepository.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -33,13 +33,16 @@ 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;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
IRateProvider _RateProvider;
|
||||
@ -48,21 +51,22 @@ namespace BTCPayServer.Controllers
|
||||
Network _Network;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
IFeeProvider _FeeProvider;
|
||||
ExplorerClient _Explorer;
|
||||
|
||||
public InvoiceController(
|
||||
Network network,
|
||||
InvoiceRepository invoiceRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
IRateProvider rateProvider,
|
||||
StoreRepository storeRepository,
|
||||
InvoiceWatcher watcher,
|
||||
ExplorerClient explorerClient,
|
||||
IFeeProvider feeProvider)
|
||||
{
|
||||
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_Network = network ?? throw new ArgumentNullException(nameof(network));
|
||||
_TokenRepository = tokenRepository ?? throw new ArgumentNullException(nameof(tokenRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
@ -71,7 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
|
||||
}
|
||||
|
||||
private async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var derivationStrategy = store.DerivationStrategy;
|
||||
var entity = new InvoiceEntity
|
||||
@ -84,11 +88,18 @@ namespace BTCPayServer.Controllers
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime + TimeSpan.FromMinutes(15.0);
|
||||
entity.ServerUrl = HttpContext.Request.GetAbsoluteRoot();
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.RefundMail = EmailValidator.IsEmail(entity?.BuyerInformation?.BuyerEmail) ? entity.BuyerInformation.BuyerEmail : null;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if(entity?.BuyerInformation?.BuyerEmail != null)
|
||||
{
|
||||
if(!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
entity.Status = "new";
|
||||
@ -96,15 +107,35 @@ 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, entity.Id);
|
||||
await _Wallet.MapAsync(entity.DepositAddress.ScriptPubKey, entity.Id);
|
||||
await _Watcher.WatchAsync(entity.Id);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
|
||||
{
|
||||
if(buyer == null)
|
||||
return;
|
||||
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
|
||||
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
|
||||
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
|
||||
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
|
||||
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
|
||||
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
|
||||
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
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));
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -8,8 +9,10 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -20,14 +23,17 @@ namespace BTCPayServer.Controllers
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = "CanAccessStore")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class StoresController : Controller
|
||||
{
|
||||
public StoresController(
|
||||
StoreRepository repo,
|
||||
TokenRepository tokenRepo,
|
||||
CallbackController callbackController,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWallet wallet,
|
||||
Network network,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Repo = repo;
|
||||
@ -36,7 +42,11 @@ namespace BTCPayServer.Controllers
|
||||
_TokenController = tokenController;
|
||||
_Wallet = wallet;
|
||||
_Env = env;
|
||||
_Network = network;
|
||||
_CallbackController = callbackController;
|
||||
}
|
||||
Network _Network;
|
||||
CallbackController _CallbackController;
|
||||
BTCPayWallet _Wallet;
|
||||
AccessTokenController _TokenController;
|
||||
StoreRepository _Repo;
|
||||
@ -82,13 +92,17 @@ namespace BTCPayServer.Controllers
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
result.StatusMessage = StatusMessage;
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
foreach(var store in stores)
|
||||
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++)
|
||||
{
|
||||
var store = stores[i];
|
||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite
|
||||
WebSite = store.StoreWebsite,
|
||||
Balance = await balances[i]
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
@ -106,15 +120,14 @@ namespace BTCPayServer.Controllers
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.ExtPubKey = store.DerivationStrategy;
|
||||
vm.DerivationScheme = store.DerivationStrategy;
|
||||
vm.StatusMessage = StatusMessage;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
|
||||
{
|
||||
if(!ModelState.IsValid)
|
||||
{
|
||||
@ -124,48 +137,71 @@ namespace BTCPayServer.Controllers
|
||||
if(store == null)
|
||||
return NotFound();
|
||||
|
||||
bool needUpdate = false;
|
||||
if(store.SpeedPolicy != model.SpeedPolicy)
|
||||
if(command == "Save")
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
if(store.StoreName != model.StoreName)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
}
|
||||
if(store.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
if(store.DerivationStrategy != model.ExtPubKey)
|
||||
{
|
||||
needUpdate = true;
|
||||
try
|
||||
bool needUpdate = false;
|
||||
if(store.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
await _Wallet.TrackAsync(model.ExtPubKey);
|
||||
store.DerivationStrategy = model.ExtPubKey;
|
||||
needUpdate = true;
|
||||
store.SpeedPolicy = model.SpeedPolicy;
|
||||
}
|
||||
catch
|
||||
if(store.StoreName != model.StoreName)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.ExtPubKey), "Invalid Derivation Scheme");
|
||||
return View(model);
|
||||
needUpdate = true;
|
||||
store.StoreName = model.StoreName;
|
||||
}
|
||||
if(store.StoreWebsite != model.StoreWebsite)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
}
|
||||
|
||||
if(needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
if(store.DerivationStrategy != model.DerivationScheme)
|
||||
{
|
||||
needUpdate = true;
|
||||
try
|
||||
{
|
||||
var strategy = ParseDerivationStrategy(model.DerivationScheme);
|
||||
await _Wallet.TrackAsync(strategy);
|
||||
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
|
||||
store.DerivationStrategy = model.DerivationScheme;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
if(needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
var facto = new DerivationStrategyFactory(_Network);
|
||||
var scheme = facto.Parse(model.DerivationScheme);
|
||||
var line = scheme.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for(int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
|
||||
{
|
||||
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -173,11 +209,11 @@ namespace BTCPayServer.Controllers
|
||||
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
|
||||
@ -186,7 +222,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public async Task<IActionResult> CreateToken(string storeId, CreateTokenViewModel model)
|
||||
{
|
||||
@ -195,42 +231,83 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var pairingCode = await _TokenController.GetPairingCode(new PairingCodeRequest()
|
||||
if(storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
|
||||
{
|
||||
storeId = model.StoreId;
|
||||
var userId = GetUserId();
|
||||
if(userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if(store == null)
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
public IActionResult CreateToken()
|
||||
public async Task<IActionResult> CreateToken(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if(string.IsNullOrWhiteSpace(userId))
|
||||
return Unauthorized();
|
||||
var model = new CreateTokenViewModel();
|
||||
model.Facade = "merchant";
|
||||
if(_Env.IsDevelopment())
|
||||
ViewBag.HidePublicKey = storeId == null;
|
||||
ViewBag.ShowStores = storeId == null;
|
||||
ViewBag.ShowMenu = storeId != null;
|
||||
model.StoreId = storeId;
|
||||
if(storeId == null)
|
||||
{
|
||||
model.PublicKey = new Key().PubKey.ToHex();
|
||||
model.Stores = new SelectList(await _Repo.GetStoresByUserId(userId), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -253,7 +330,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()
|
||||
{
|
||||
@ -265,16 +342,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Route("api-access-request")]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
var store = await _Repo.FindStore(selectedStore, GetUserId());
|
||||
if(store == null)
|
||||
if(pairingCode == null)
|
||||
return NotFound();
|
||||
if(pairingCode != null && await _TokenRepository.PairWithAsync(pairingCode, store.Id))
|
||||
var store = await _Repo.FindStore(selectedStore, GetUserId());
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if(store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if(pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
StatusMessage = "Pairing is successfull";
|
||||
if(pairingResult == PairingResult.Partial)
|
||||
StatusMessage = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
@ -282,7 +365,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = "Pairing failed";
|
||||
StatusMessage = $"Pairing failed ({pairingResult})";
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
{
|
||||
storeId = store.Id
|
||||
|
25
BTCPayServer/Data/AddressInvoiceData.cs
Normal file
25
BTCPayServer/Data/AddressInvoiceData.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class AddressInvoiceData
|
||||
{
|
||||
public string Address
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -26,6 +26,10 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<PendingInvoiceData> PendingInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DbSet<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
@ -46,11 +50,27 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<AddressInvoiceData> AddressInvoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<SettingData> Settings
|
||||
{
|
||||
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();
|
||||
@ -86,6 +106,18 @@ namespace BTCPayServer.Data
|
||||
.HasOne(pt => pt.StoreData)
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
39
BTCPayServer/Data/PairedSINData.cs
Normal file
39
BTCPayServer/Data/PairedSINData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
50
BTCPayServer/Data/PairingCodeData.cs
Normal file
50
BTCPayServer/Data/PairingCodeData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
15
BTCPayServer/Data/PendingInvoiceData.cs
Normal file
15
BTCPayServer/Data/PendingInvoiceData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,9 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
return context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any() == IsBitpayAPI;
|
||||
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
|
||||
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
|
||||
return (hasVersion || hasIdentity) == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,8 +71,8 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var match = context.RouteContext.HttpContext.Request.Headers["Accept"].FirstOrDefault()?.StartsWith(MediaType, StringComparison.Ordinal);
|
||||
return (match.HasValue && match.Value) == ExpectedValue;
|
||||
var hasHeader = context.RouteContext.HttpContext.Request.Headers["Accept"].Any(m => m.StartsWith(MediaType, StringComparison.Ordinal));
|
||||
return hasHeader == ExpectedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
BTCPayServer/Filters/XFrameOptionsAttribute.cs
Normal file
33
BTCPayServer/Filters/XFrameOptionsAttribute.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XFrameOptionsAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public XFrameOptionsAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
|
||||
if(existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("x-frame-options");
|
||||
else
|
||||
context.HttpContext.Response.Headers["x-frame-options"] = Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Authentication;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -67,12 +69,16 @@ namespace BTCPayServer.Hosting
|
||||
object storeId = null;
|
||||
if(!((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).RouteData.Values.TryGetValue("storeId", out storeId))
|
||||
context.Succeed(requirement);
|
||||
else
|
||||
else if(storeId != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User));
|
||||
if(store != null)
|
||||
if(requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
var user = _UserManager.GetUserId(((Microsoft.AspNetCore.Mvc.ActionContext)context.Resource).HttpContext.User);
|
||||
if(user != null)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore((string)storeId, user);
|
||||
if(store != null)
|
||||
if(requirement.Role == null || requirement.Role == store.Role)
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,12 +112,12 @@ 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);
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton(o => o.GetRequiredService<BTCPayServerRuntime>().Wallet);
|
||||
services.TryAddSingleton<BTCPayWallet>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
|
||||
{
|
||||
@ -138,6 +144,8 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<CallbackController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -33,6 +33,9 @@ using Hangfire.Dashboard;
|
||||
using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -50,29 +53,32 @@ namespace BTCPayServer.Hosting
|
||||
return context.GetHttpContext().User.IsInRole(_Role);
|
||||
}
|
||||
}
|
||||
public Startup(IConfiguration conf)
|
||||
public Startup(IConfiguration conf, IHostingEnvironment env)
|
||||
{
|
||||
Configuration = conf;
|
||||
_Env = env;
|
||||
}
|
||||
|
||||
IHostingEnvironment _Env;
|
||||
public IConfiguration Configuration
|
||||
{
|
||||
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();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
});
|
||||
}
|
||||
|
||||
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||
@ -99,12 +105,26 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
configuration(config);
|
||||
}));
|
||||
|
||||
services.AddHangfire(configuration);
|
||||
services.AddCors(o =>
|
||||
{
|
||||
o.AddPolicy("BitpayAPI", b =>
|
||||
{
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
|
||||
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
|
||||
{
|
||||
o.Value.DeveloperMode = _Env.IsDevelopment();
|
||||
});
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IHostingEnvironment env,
|
||||
IServiceProvider prov,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
if(env.IsDevelopment())
|
||||
@ -115,10 +135,13 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
|
||||
Logs.Configure(loggerFactory);
|
||||
|
||||
//App insight do not that by itself...
|
||||
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
|
||||
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
|
||||
app.UseMvc(routes =>
|
||||
|
392
BTCPayServer/Migrations/20171006013443_AddressMapping.Designer.cs
generated
Normal file
392
BTCPayServer/Migrations/20171006013443_AddressMapping.Designer.cs
generated
Normal file
@ -0,0 +1,392 @@
|
||||
// <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("20171006013443_AddressMapping")]
|
||||
partial class AddressMapping
|
||||
{
|
||||
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.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
|
||||
}
|
||||
}
|
||||
}
|
41
BTCPayServer/Migrations/20171006013443_AddressMapping.cs
Normal file
41
BTCPayServer/Migrations/20171006013443_AddressMapping.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class AddressMapping : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AddressInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AddressInvoices", x => x.Address);
|
||||
table.ForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
column: x => x.InvoiceDataId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AddressInvoices_InvoiceDataId",
|
||||
table: "AddressInvoices",
|
||||
column: "InvoiceDataId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AddressInvoices");
|
||||
}
|
||||
}
|
||||
}
|
444
BTCPayServer/Migrations/20171010082424_Tokens.Designer.cs
generated
Normal file
444
BTCPayServer/Migrations/20171010082424_Tokens.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
67
BTCPayServer/Migrations/20171010082424_Tokens.cs
Normal file
67
BTCPayServer/Migrations/20171010082424_Tokens.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
450
BTCPayServer/Migrations/20171012020112_PendingInvoices.Designer.cs
generated
Normal file
450
BTCPayServer/Migrations/20171012020112_PendingInvoices.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
54
BTCPayServer/Migrations/20171012020112_PendingInvoices.cs
Normal file
54
BTCPayServer/Migrations/20171012020112_PendingInvoices.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,20 @@ namespace BTCPayServer.Migrations
|
||||
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")
|
||||
@ -48,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")
|
||||
@ -64,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")
|
||||
@ -286,6 +358,13 @@ namespace BTCPayServer.Migrations
|
||||
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")
|
||||
|
@ -39,15 +39,15 @@ 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);
|
||||
await writer.WriteAsync(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
146
BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs
Normal file
146
BTCPayServer/Models/InvoicingModels/InvoiceDetailsModel.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Servcices.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class InvoiceDetailsModel
|
||||
{
|
||||
public class Payment
|
||||
{
|
||||
public int Confirmations
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public BitcoinAddress DepositAddress
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Amount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string TransactionId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset ReceivedTime
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string TransactionLink
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public String Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<Payment> Payments
|
||||
{
|
||||
get; set;
|
||||
} = new List<Payment>();
|
||||
|
||||
public string Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset CreatedDate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DateTimeOffset ExpirationDate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string RefundEmail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public BuyerInformation BuyerInformation
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public object StoreName
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string StoreLink
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public double Rate
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string NotificationUrl
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Fiat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTC
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTCDue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string BTCPaid
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public String NetworkFee
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public BitcoinAddress BitcoinAddress
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string PaymentUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
@ -27,8 +27,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
[ExtPubKeyValidator]
|
||||
public string ExtPubKey
|
||||
[DerivationStrategyValidator]
|
||||
public string DerivationScheme
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -39,6 +39,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<(string KeyPath, string Address)> AddressSamples
|
||||
{
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -31,6 +32,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public Money Balance
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -25,6 +26,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Required]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public SelectList Stores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
public class TokenViewModel
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -40,22 +40,16 @@ namespace BTCPayServer
|
||||
.UseIISIntegration()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(services =>
|
||||
.UseApplicationInsights()
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
services.AddLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddProvider(new CustomConsoleLogProvider());
|
||||
});
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddProvider(new CustomConsoleLogProvider());
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
host.StartAsync().GetAwaiter().GetResult();
|
||||
var urls = host.ServerFeatures.Get<IServerAddressesFeature>().Addresses;
|
||||
if(urls.Count != 0)
|
||||
{
|
||||
OpenBrowser(urls.Select(url => url.Replace("0.0.0.0", "127.0.0.1")).First());
|
||||
}
|
||||
foreach(var url in urls)
|
||||
{
|
||||
logger.LogInformation("Listening on " + url);
|
||||
@ -79,29 +73,5 @@ namespace BTCPayServer
|
||||
loggerProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenBrowser(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); // Works ok on windows
|
||||
}
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
Process.Start("xdg-open", url); // Works ok on linux
|
||||
}
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
Process.Start("open", url); // Not tested
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
BTCPayServer/Properties/launchSettings.json
Normal file
23
BTCPayServer/Properties/launchSettings.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:14139/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Docker-Regtest": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_EXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14142/"
|
||||
}
|
||||
}
|
||||
}
|
@ -302,6 +302,12 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
var str = JsonConvert.SerializeObject(from);
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
|
||||
public Money GetNetworkFee()
|
||||
{
|
||||
var item = Calculate();
|
||||
return TxFee * item.TxCount;
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentEntity
|
||||
|
@ -56,38 +56,36 @@ 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<bool> RemovePendingInvoice(string invoiceId)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
tx.RemoveKey("T-Pending", invoiceId);
|
||||
tx.Commit();
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
|
||||
public string[] GetPendingInvoices()
|
||||
{
|
||||
List<string> pending = new List<string>();
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
{
|
||||
foreach(var row in tx.SelectForward<string, byte[]>("T-Pending"))
|
||||
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
|
||||
try
|
||||
{
|
||||
pending.Add(row.Key);
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch(DbUpdateException) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<string[]> GetPendingInvoices()
|
||||
{
|
||||
using(var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.PendingInvoices.Select(p => p.Id).ToArrayAsync();
|
||||
}
|
||||
return pending.ToArray();
|
||||
}
|
||||
|
||||
public async Task UpdateInvoice(string invoiceId, UpdateCustomerModel data)
|
||||
@ -214,12 +212,16 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
using(var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
|
||||
IQueryable<InvoiceData> query = context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
.Include(o => o.RefundAddresses);
|
||||
|
||||
if(!string.IsNullOrEmpty(queryObject.InvoiceId))
|
||||
{
|
||||
query = query.Where(i => i.Id == queryObject.InvoiceId);
|
||||
}
|
||||
|
||||
if(!string.IsNullOrEmpty(queryObject.StoreId))
|
||||
{
|
||||
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
|
||||
@ -387,5 +389,10 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string InvoiceId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using System.Threading;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Collections.Concurrent;
|
||||
using Hangfire;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Servcices.Invoices
|
||||
{
|
||||
@ -21,20 +22,44 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
ExplorerClient _ExplorerClient;
|
||||
DerivationStrategyFactory _DerivationFactory;
|
||||
InvoiceNotificationManager _NotificationManager;
|
||||
BTCPayWallet _Wallet;
|
||||
|
||||
public InvoiceWatcher(ExplorerClient explorerClient,
|
||||
public InvoiceWatcher(ExplorerClient explorerClient,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayWallet wallet,
|
||||
InvoiceNotificationManager notificationManager)
|
||||
{
|
||||
LongPollingMode = explorerClient.Network == Network.RegTest;
|
||||
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_ExplorerClient = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
|
||||
_DerivationFactory = new DerivationStrategyFactory(_ExplorerClient.Network);
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_NotificationManager = notificationManager ?? throw new ArgumentNullException(nameof(notificationManager));
|
||||
}
|
||||
|
||||
private async Task StartWatchInvoice(string invoiceId)
|
||||
public bool LongPollingMode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public async Task NotifyReceived(Script scriptPubKey)
|
||||
{
|
||||
var invoice = await _Wallet.GetInvoiceId(scriptPubKey);
|
||||
if(invoice != null)
|
||||
_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("Watching invoice " + invoiceId);
|
||||
UTXOChanges changes = null;
|
||||
while(true)
|
||||
{
|
||||
@ -49,17 +74,21 @@ 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}");
|
||||
}
|
||||
|
||||
if(invoice.Status == "complete" || invoice.Status == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false);
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
if(await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
break;
|
||||
}
|
||||
|
||||
if(!changed || _Cts.Token.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
@ -75,28 +104,29 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
|
||||
|
||||
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
|
||||
{
|
||||
if(invoice.Status == "invalid")
|
||||
{
|
||||
return (false, changes);
|
||||
}
|
||||
{
|
||||
bool needSave = false;
|
||||
bool shouldWait = true;
|
||||
|
||||
if(invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial"))
|
||||
if(invoice.Status != "invalid" && invoice.ExpirationTime < DateTimeOffset.UtcNow && (invoice.Status == "new" || invoice.Status == "paidPartial"))
|
||||
{
|
||||
needSave = true;
|
||||
invoice.Status = "invalid";
|
||||
}
|
||||
|
||||
if(invoice.Status == "new" || invoice.Status == "paidPartial")
|
||||
if(invoice.Status == "invalid" || invoice.Status == "new" || invoice.Status == "paidPartial")
|
||||
{
|
||||
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, false, _Cts.Token).ConfigureAwait(false);
|
||||
shouldWait = false; //should not wait, Sync is blocking call
|
||||
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
|
||||
var invoiceIds = utxos.Select(u => _Wallet.GetInvoiceId(u.Output.ScriptPubKey)).ToArray();
|
||||
utxos =
|
||||
utxos
|
||||
.Where((u, i) => invoiceIds[i].GetAwaiter().GetResult() == invoice.Id)
|
||||
.ToArray();
|
||||
|
||||
List<Coin> receivedCoins = new List<Coin>();
|
||||
foreach(var received in changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs))
|
||||
foreach(var received in utxos)
|
||||
if(received.Output.ScriptPubKey == invoice.DepositAddress.ScriptPubKey)
|
||||
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
|
||||
|
||||
@ -188,24 +218,33 @@ namespace BTCPayServer.Servcices.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
shouldWait = shouldWait && !needSave;
|
||||
|
||||
if(shouldWait)
|
||||
{
|
||||
await Task.Delay(PollInterval, _Cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return (needSave, changes);
|
||||
}
|
||||
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get; set;
|
||||
} = TimeSpan.FromSeconds(10);
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if(_UpdatePendingInvoices != null)
|
||||
{
|
||||
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WatchAsync(string invoiceId)
|
||||
public async Task WatchAsync(string invoiceId, bool singleShot = false)
|
||||
{
|
||||
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
|
||||
if(invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
if(!singleShot)
|
||||
await _InvoiceRepository.AddPendingInvoice(invoiceId).ConfigureAwait(false);
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
@ -220,61 +259,68 @@ 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)PollInterval.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())
|
||||
try
|
||||
{
|
||||
if(task.Status != TaskStatus.Running)
|
||||
_Cts.Token.ThrowIfCancellationRequested();
|
||||
var localItem = item;
|
||||
// If the invoice is already updating, ignore
|
||||
Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
|
||||
if(updating.TryAdd(item, updateInvoice))
|
||||
{
|
||||
watching.Remove(task);
|
||||
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
|
||||
}
|
||||
}
|
||||
catch(Exception ex) when(!_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
||||
_Cts.Token.WaitHandle.WaitOne(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(OperationCanceledException) when(_Cts.Token.IsCancellationRequested)
|
||||
catch(OperationCanceledException)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.WaitAll(watching.ToArray());
|
||||
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
|
||||
}
|
||||
catch(AggregateException) { }
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
_RunningTask.TrySetException(ex);
|
||||
Logs.PayServer.LogCritical(ex, "Error in the InvoiceWatcher loop");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_UpdatePendingInvoices.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
@ -7,56 +6,59 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets
|
||||
{
|
||||
public class BTCPayWallet
|
||||
{
|
||||
public class BTCPayWallet
|
||||
{
|
||||
private ExplorerClient _Client;
|
||||
private DBreezeEngine _Engine;
|
||||
private Serializer _Serializer;
|
||||
private DerivationStrategyFactory _DerivationStrategyFactory;
|
||||
ApplicationDbContextFactory _DBFactory;
|
||||
|
||||
public BTCPayWallet(ExplorerClient client, DBreezeEngine dbreeze)
|
||||
public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory)
|
||||
{
|
||||
if(client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
if(dbreeze == null)
|
||||
throw new ArgumentNullException(nameof(dbreeze));
|
||||
if(factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
_Client = client;
|
||||
_Engine = dbreeze;
|
||||
_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);
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
|
||||
return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network);
|
||||
}
|
||||
|
||||
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
await _Client.TrackAsync(derivationStrategy);
|
||||
}
|
||||
|
||||
public async Task<string> GetInvoiceId(Script scriptPubKey)
|
||||
{
|
||||
using(var db = _DBFactory.CreateContext())
|
||||
{
|
||||
var pathInfoBytes = ToBytes(pathInfo);
|
||||
tx.Insert(AddressToKeyInfo, pathInfo.Address.ToString(), pathInfoBytes);
|
||||
tx.Commit();
|
||||
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString());
|
||||
return result?.InvoiceDataId;
|
||||
}
|
||||
return pathInfo.Address;
|
||||
}
|
||||
|
||||
public async Task TrackAsync(string walletIdentifier)
|
||||
public async Task MapAsync(Script address, string invoiceId)
|
||||
{
|
||||
await _Client.SyncAsync(_DerivationStrategyFactory.Parse(walletIdentifier), null, null, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
const string AddressToId = "AtI";
|
||||
const string AddressToKeyInfo = "AtK";
|
||||
public Task MapAsync(BitcoinAddress address, string id)
|
||||
{
|
||||
using(var tx = _Engine.GetTransaction())
|
||||
using(var db = _DBFactory.CreateContext())
|
||||
{
|
||||
tx.Insert(AddressToId, address.ToString(), id);
|
||||
tx.Commit();
|
||||
db.AddressInvoices.Add(new AddressInvoiceData()
|
||||
{
|
||||
Address = address.Hash.ToString(),
|
||||
InvoiceDataId = invoiceId
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private byte[] ToBytes<T>(T obj)
|
||||
@ -69,5 +71,13 @@ namespace BTCPayServer.Services.Wallets
|
||||
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -6,7 +7,7 @@ using System.Text;
|
||||
|
||||
namespace BTCPayServer.Validations
|
||||
{
|
||||
public class ExtPubKeyValidatorAttribute : ValidationAttribute
|
||||
public class DerivationStrategyValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
@ -19,7 +20,7 @@ namespace BTCPayServer.Validations
|
||||
return new ValidationResult("No Network specified");
|
||||
try
|
||||
{
|
||||
new BitcoinExtPubKey((string)value, network);
|
||||
new DerivationStrategyFactory(network).Parse((string)value);
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
catch(Exception ex)
|
@ -27,6 +27,7 @@
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var invoiceId = "@Model.InvoiceId";
|
||||
var btcAddress = "@Model.BTCAddress";
|
||||
var btcDue = "@Model.BTCDue"; //must be a string
|
||||
var customerEmail = "@Model.CustomerEmail"; // Place holder
|
||||
@ -178,7 +179,7 @@
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan" style="opacity: 1;">
|
||||
<div class="payment__scan">
|
||||
<div class="payment__details__instruction__open-wallet hidden-sm-up">
|
||||
@*<div class="payment__details__instruction__open-wallet hidden-sm-up">
|
||||
<!---->
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button action-button--secondary">
|
||||
<span i18n="">Show QR code</span>
|
||||
@ -192,9 +193,9 @@
|
||||
<!---->
|
||||
<div class="qr-codes"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>*@
|
||||
<!---->
|
||||
<div class="qr-codes hidden-xs-down"></div>
|
||||
<div class="qr-codes"></div>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" href="@Model.InvoiceBitcoinUrl">
|
||||
|
187
BTCPayServer/Views/Invoice/Invoice.cshtml
Normal file
187
BTCPayServer/Views/Invoice/Invoice.cshtml
Normal file
@ -0,0 +1,187 @@
|
||||
@model InvoiceDetailsModel
|
||||
@{
|
||||
ViewData["Title"] = "Invoice " + Model.Id;
|
||||
}
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
<p>Invoice details</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Information</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Store</th>
|
||||
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>@Model.Id</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created date</th>
|
||||
<td>@Model.CreatedDate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expiration date</th>
|
||||
<td>@Model.ExpirationDate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>@Model.Status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Refund email</th>
|
||||
<td>@Model.RefundEmail</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
<td>@Model.OrderId</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Rate</th>
|
||||
<td>@Model.Rate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total fiat due</th>
|
||||
<td>@Model.Fiat</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Network Fee</th>
|
||||
<td>@Model.NetworkFee</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total crypto due</th>
|
||||
<td>@Model.BTC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Crypto due</th>
|
||||
<td>@Model.BTCDue</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Crypto paid</th>
|
||||
<td>@Model.BTCPaid</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Notification Url</th>
|
||||
<td>@Model.NotificationUrl</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment address</th>
|
||||
<td>@Model.BitcoinAddress</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Payment Url</th>
|
||||
<td><a href="@Model.PaymentUrl">@Model.PaymentUrl</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Buyer information</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Name
|
||||
<th>
|
||||
<td>@Model.BuyerInformation.BuyerName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>@Model.BuyerInformation.BuyerEmail</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@Model.BuyerInformation.BuyerPhone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@Model.BuyerInformation.BuyerAddress1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@Model.BuyerInformation.BuyerAddress2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@Model.BuyerInformation.BuyerCity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@Model.BuyerInformation.BuyerState</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@Model.BuyerInformation.BuyerCountry</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@Model.BuyerInformation.BuyerZip</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Product information</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@Model.ProductInformation.ItemCode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@Model.ProductInformation.ItemDesc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Price</th>
|
||||
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Payments</h3>
|
||||
<div class="form-group">
|
||||
<form asp-action="Invoice" method="post">
|
||||
<button type="submit" name="command" class="btn btn-success" value="refresh" title="Refresh State">
|
||||
Refresh state
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th>Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.Payments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.ReceivedTime</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td>@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
@ -50,7 +50,7 @@
|
||||
<td>@invoice.InvoiceId</td>
|
||||
<td>@invoice.Status</td>
|
||||
<td>@invoice.AmountCurrency</td>
|
||||
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a></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>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -1,5 +1,6 @@
|
||||
@{
|
||||
Layout = "/Views/Shared/_Layout.cshtml";
|
||||
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +16,10 @@
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
@await Html.PartialAsync("_Nav")
|
||||
@if(ViewBag.ShowMenu)
|
||||
{
|
||||
@await Html.PartialAsync("_Nav")
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
@RenderBody()
|
||||
|
@ -9,17 +9,21 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form asp-action="CreateToken" method="post">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="Label"></label>
|
||||
<input asp-for="Label" class="form-control" />
|
||||
<span asp-validation-for="Label" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PublicKey"></label>
|
||||
<input asp-for="PublicKey" class="form-control" />
|
||||
<span asp-validation-for="PublicKey" class="text-danger"></span>
|
||||
</div>
|
||||
@if(!ViewBag.HidePublicKey)
|
||||
{
|
||||
<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>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label asp-for="Facade"></label>
|
||||
<select asp-for="Facade" class="form-control">
|
||||
@ -28,6 +32,19 @@
|
||||
</select>
|
||||
<span asp-validation-for="Facade" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
@if(ViewBag.ShowStores)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreId" class="control-label"></label>
|
||||
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
|
||||
<span asp-validation-for="StoreId" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="hidden" asp-for="StoreId" />
|
||||
}
|
||||
<div class="form-group">
|
||||
<input type="submit" value="Request pairing" class="btn btn-default" />
|
||||
</div>
|
||||
|
@ -27,6 +27,7 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Website</th>
|
||||
<th>Balance</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -40,6 +41,7 @@
|
||||
{
|
||||
<a href="@store.WebSite">@store.WebSite</a>
|
||||
}</td>
|
||||
<td>@store.Balance</td>
|
||||
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a></td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -36,11 +36,77 @@
|
||||
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ExtPubKey"></label>
|
||||
<input asp-for="ExtPubKey" class="form-control" />
|
||||
<span asp-validation-for="ExtPubKey" class="text-danger"></span>
|
||||
<h5>Derivation Scheme</h5>
|
||||
@if(Model.AddressSamples.Count == 0)
|
||||
{
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||
}
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Save</button>
|
||||
<div class="form-group">
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@if(Model.AddressSamples.Count == 0)
|
||||
{
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Address type</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>P2WPKH</td>
|
||||
<td>xpub</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2SH-P2WPKH</td>
|
||||
<td>xpub-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>P2PKH</td>
|
||||
<td>xpub-[legacy]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH-P2WSH</td>
|
||||
<td>2-of-xpub1-xpub2-[p2sh]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-sig P2SH</td>
|
||||
<td>2-of-xpub1-xpub2-[legacy]</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var sample in Model.AddressSamples)
|
||||
{
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,7 +69,7 @@ function emailForm() {
|
||||
// Push the email to a server, once the reception is confirmed move on
|
||||
customerEmail = emailAddress;
|
||||
|
||||
var path = window.location.pathname + "/UpdateCustomer";
|
||||
var path = "i/" + invoiceId + "/UpdateCustomer";
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
@ -186,7 +186,7 @@ function updateState(status) {
|
||||
}
|
||||
|
||||
var watcher = setInterval(function () {
|
||||
var path = window.location.pathname + "/status";
|
||||
var path = "i/" + invoiceId + "/status";
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "GET"
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26730.3
|
||||
VisualStudioVersion = 15.0.26730.16
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServer\BTCPayServer.csproj", "{949A0870-8D8C-4DE5-8845-DDD560489177}"
|
||||
EndProject
|
||||
|
Reference in New Issue
Block a user