Compare commits

...

35 Commits

Author SHA1 Message Date
e3c5efa929 dump version 2017-10-17 13:56:17 +09:00
8db9d93d23 Add api-tokens page, do not hide qrcode if small 2017-10-17 13:52:30 +09:00
517bb94b8b Do not crash if user is not authorized to go on the store 2017-10-17 11:45:52 +09:00
c804f55d82 bump 2017-10-17 11:19:57 +09:00
c96a25c9b9 Checkout page was not working correctly on /invoice?id= 2017-10-17 11:16:34 +09:00
4c57726be7 bump 2017-10-15 15:33:15 +09:00
8f723d7131 Fix typos 2017-10-15 15:32:53 +09:00
a7e10c0fb9 Can't pair same SIN to different store 2017-10-13 18:06:46 +09:00
15e73e1cad Properly limit CORS to bitpay api 2017-10-13 17:46:19 +09:00
a17192ee99 Add Cors 2017-10-13 17:18:32 +09:00
27200d1fb0 X-Frame-Options 2017-10-13 17:13:21 +09:00
9ddceae824 Validate email in the api 2017-10-13 16:59:02 +09:00
d1961e0938 Support other way of passing buyer info 2017-10-13 16:44:55 +09:00
033432d6fb Keep compatible checkout page address 2017-10-13 16:07:57 +09:00
c98f0ba55b Fix watcher loop 2017-10-13 15:05:06 +09:00
76993d2532 prevent watcher loop to crash 2017-10-13 14:59:05 +09:00
62e3f2d8e1 Prevent null invoice to be added to InvoiceWatcher 2017-10-13 14:53:42 +09:00
9806cab090 Fix Bitpay api route detection 2017-10-13 14:41:28 +09:00
0d9fbe2d41 Fix expiration field in Invoice details page 2017-10-13 11:27:05 +09:00
016db76306 Add page for viewing the Invoice details 2017-10-13 00:25:45 +09:00
d469084596 Remove useless line from response 2017-10-12 19:16:01 +09:00
bb4decd522 Add AppInsight logs 2017-10-12 18:16:15 +09:00
caca8e81c2 Do not drop column for sqlite 2017-10-12 16:41:11 +09:00
bae08b6966 Use callback to update invoice state instead of long polling 2017-10-12 16:33:53 +09:00
212a816598 Remove BOM 2017-10-11 19:02:45 +09:00
1fd9cb5e2a Remove BOM + add content-type if there is an error 2017-10-11 17:59:35 +09:00
6d3ea65e03 Refactor token handling, support server-initiated pairing 2017-10-11 12:20:44 +09:00
7d8c3c1c81 Remove useless address mapping dbreeze 2017-10-06 14:58:58 +09:00
47ddbff817 version bump 2017-10-06 11:09:26 +09:00
783132a012 Add balance of the store in the stores page 2017-10-06 11:07:22 +09:00
f456d62d3c Properly map addresses to invoice, use new nbxplorer 2017-10-06 10:37:38 +09:00
8b4e572e16 Allow flexible derivation scheme for the store 2017-10-05 00:05:38 +09:00
51ef3ec656 Update to NBXplorer supporting several scheme 2017-10-04 19:29:39 +09:00
35a3618a42 Add launchSettings, fix docker-compose version 2017-10-03 01:29:35 +09:00
aa59725176 Doc how to run tests 2017-10-03 01:01:21 +09:00
61 changed files with 3397 additions and 487 deletions

1
.gitignore vendored
View File

@ -47,7 +47,6 @@ dlldata.c
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c

View File

@ -64,16 +64,20 @@ namespace BTCPayServer.Tests
}
IWebHost _Host;
public int Port
{
get; set;
}
public void Start()
{
if(!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
HDPrivateKey = new ExtKey();
var port = Utils.FreeTcpPort();
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"port={port}");
config.AppendLine($"port={Port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
@ -81,7 +85,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
ServerUri = new Uri("http://127.0.0.1:" + port + "/");
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
@ -94,6 +98,7 @@ namespace BTCPayServer.Tests
{
l.SetMinimumLevel(LogLevel.Information)
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddProvider(Logs.LogProvider);
});
})
@ -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

View File

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

View File

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

View File

@ -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
```

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using DBreeze;
using BTCPayServer.Data;
using DBreeze;
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
@ -6,175 +7,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);
}
}

View File

@ -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" />

View File

@ -51,7 +51,6 @@ namespace BTCPayServer.Configuration
}
DBreezeEngine db = new DBreezeEngine(CreateDBPath(opts, "TokensDB"));
_Resources.Add(db);
TokenRepository = new TokenRepository(db);
db = new DBreezeEngine(CreateDBPath(opts, "InvoiceDB"));
_Resources.Add(db);
@ -70,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;

View File

@ -21,7 +21,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("tokens")]
public async Task<GetTokensResponse> GetTokens()
public async Task<GetTokensResponse> Tokens()
{
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
return new GetTokensResponse(tokens);
@ -29,33 +29,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);
}
}

View File

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

View File

@ -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;

View File

@ -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]

View File

@ -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));

View File

@ -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

View 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;
}
}
}

View File

@ -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);
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
}
}

View 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;
}
}
}

View File

@ -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>();

View File

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

View File

@ -33,6 +33,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 =>

View 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
}
}
}

View 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");
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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")

View File

@ -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);
}
}
}

View 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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}
}

View File

@ -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
{

View File

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

View File

@ -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 { }
}
}
}

View 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/"
}
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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));
}

View File

@ -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();
}
}
}

View File

@ -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)

View File

@ -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">

View 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>

View File

@ -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>

View File

@ -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()

View File

@ -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>

View File

@ -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>
}

View File

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

View File

@ -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>

View File

@ -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"

View File

@ -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