Compare commits

...

91 Commits

Author SHA1 Message Date
1c510df3fc bump 2018-01-10 02:41:33 +09:00
c80ffe396e forgot passing cancellation 2018-01-10 02:13:49 +09:00
401a31e5c2 Cleanup code 2018-01-10 02:07:42 +09:00
2df60bd121 bump 2018-01-09 23:55:37 +09:00
6d10c8a6c1 Can change crypto on checkout page 2018-01-09 23:55:08 +09:00
44898b5e23 Checkout page: Bind crypto image to client cycle view model, add logo on main QR code 2018-01-09 22:43:36 +09:00
c0f53db561 fix sync bar 2018-01-09 18:52:16 +09:00
133fb96d28 bump 2018-01-09 17:27:54 +09:00
98b7ad62af Fix order accounting 2018-01-09 17:27:26 +09:00
3069fe0dd9 BTCPayServer should work on HTTP even if externalurl is https 2018-01-09 16:54:40 +09:00
729555b96f Fix NBxplorerListener disconnecting itself every minute 2018-01-09 16:10:16 +09:00
b4040ba7ad Update NBXplorer, bump 2018-01-09 14:12:28 +09:00
863752a471 update nbxplorer (fix ltc testnet) 2018-01-09 12:53:58 +09:00
6ae9d13c43 Allow checkout with litecoin 2018-01-09 11:41:07 +09:00
0c735f4e29 Fix accounting calculation when multi crypto 2018-01-09 10:54:19 +09:00
76d50b018b Calculate rate properly per crypto 2018-01-09 02:57:06 +09:00
31672a2587 Add litecoin to docker-compose fix bugs when two networks generate same address 2018-01-09 01:56:37 +09:00
a048494f34 bump version 2018-01-08 23:12:28 +09:00
c513d6bd44 Fix litecoin registration 2018-01-08 23:05:41 +09:00
c3d37b1f78 Can set derivation scheme for a specific crypto currency 2018-01-08 22:45:09 +09:00
5910644cda Remove useless field 2018-01-08 20:57:11 +09:00
a16cd3e287 Improve invoice page with currencies information 2018-01-08 20:06:16 +09:00
e3a0122eb3 make sure to not crash whole process if nbxplorer unavailable 2018-01-08 18:18:34 +09:00
1cda0eff16 bump nbxplorer 2018-01-08 17:17:39 +09:00
6003aa4236 Add polling for connection through websocket 2018-01-08 16:50:56 +09:00
8753dd15de Remove BOM from IPN 2018-01-08 04:18:15 +09:00
6ae6335c6d Fix layout_cshtml 2018-01-08 04:14:35 +09:00
e3a1eed8b3 Use Websocket for blockchain notifications 2018-01-08 02:36:41 +09:00
eb44203475 Remove internal url 2018-01-07 21:58:46 +09:00
80e878c2f5 Removing http callback notification system 2018-01-07 21:48:00 +09:00
6cb1649fc2 fix leak 2018-01-07 21:07:06 +09:00
63fceed5f4 invoice watcher can watch several currencies 2018-01-07 02:16:42 +09:00
781b2885cc Refactoring to prepare multiple DerivationSchemes per store and invoices 2018-01-06 19:10:55 +09:00
2f9afda0ab bump 2018-01-06 11:38:54 +09:00
108146ca92 Fixing QR Code and Button to use BIP21 2018-01-06 11:38:24 +09:00
bb46294a6d Fix progress bar on synching 2018-01-06 01:24:04 +09:00
f8aad6ac80 bump 2018-01-05 00:41:21 +09:00
658d1f1df0 Merge pull request from lepipele/dev-lepi
Indicator now faintly visible without hover
2018-01-05 00:34:26 +09:00
ee3144f34a Indicator now faintly visible without hover
Reference: https://github.com/btcpayserver/btcpayserver/pull/23#issuecomment-355302053
2018-01-04 08:59:44 -06:00
9a34fe46fb Fix 2018-01-04 22:56:49 +09:00
766d96c02d fix layout 2018-01-04 22:52:14 +09:00
7445c89773 Merge pull request from lepipele/dev-lepi
Implementing indicator that shows total line row can be expanded
2018-01-04 22:44:00 +09:00
28ac5608a5 Update NBxplorer, bump version 2018-01-04 22:43:28 +09:00
44c925a4ba Fix 2018-01-04 22:21:47 +09:00
51cd89f177 Implementing indicator that shows total line row can be expanded 2018-01-03 17:17:47 -06:00
ab188ad54f use EmptyResult instead of custom NoResponse 2017-12-25 21:52:27 +09:00
513835ed0f remove eclair dependency 2017-12-21 18:06:21 +09:00
a863812f90 Refactor how invoice payments are computed 2017-12-21 18:01:26 +09:00
a37fdde214 Big refactorying for supporting multi currencies 2017-12-21 15:52:04 +09:00
d5ef36fe50 add deploy to azure button 2017-12-19 12:41:15 +09:00
7430ceb23d update doc 2017-12-19 12:19:08 +09:00
395b550c21 add more doc for contributing 2017-12-19 11:47:43 +09:00
774565b121 fix test README 2017-12-19 11:43:52 +09:00
72d1344002 add banner to README 2017-12-19 11:42:04 +09:00
b4ee5dcb0d Update README.md 2017-12-19 11:35:46 +09:00
a0f0ff0bf1 bump 2017-12-18 17:23:27 +09:00
db2cc8f951 show message when bitcoin core is starting 2017-12-18 16:35:16 +09:00
24007f1515 Fix: Forgot to pass in the Rate field to the invoice DTO 2017-12-18 08:56:27 +09:00
3d7445f359 Fix logs name for Events, try catch websocket closure 2017-12-17 22:57:27 +09:00
34760afe77 Do not show release in footer if compiled in release 2017-12-17 22:50:05 +09:00
417209b057 fix checkout page bug 2017-12-17 22:37:40 +09:00
9026378b86 bump version 2017-12-17 22:14:37 +09:00
9b3dca1683 Electrum v3.0 use xpub for testnet see https://github.com/spesmilo/electrum/issues/3539#issuecomment-352246947 2017-12-17 22:02:49 +09:00
cde593a935 bump 2017-12-17 20:30:43 +09:00
f0755260a6 don't crash if fail to connect to websockets 2017-12-17 20:28:18 +09:00
582e1eb4f8 version bump 2017-12-17 20:01:21 +09:00
aaadda3e0f Use websockets in checkout page to get notified of paid invoices 2017-12-17 19:58:55 +09:00
9d7f5b5b6e Fix bug: If electrum zpub is entered, the wrong value is saved into database 2017-12-17 19:41:46 +09:00
99040597dc BTCPrice should be bitcoin price of item 2017-12-17 19:40:42 +09:00
d9794216dd Send InvoicePaymentEvent 2017-12-17 14:33:38 +09:00
84bb6056d3 Use EventAggregator to decouple several classes 2017-12-17 14:17:42 +09:00
dfed2daa8e Fix synching information 2017-12-17 11:07:11 +09:00
1521ec8071 Fix nullreferenceexception 2017-12-17 02:38:04 +09:00
bf7ae178ef Fix , fix electrum format not recognizing standard p2pkh on testnet 2017-12-17 02:28:37 +09:00
dc7f96c6da Show a modal when node is synching 2017-12-17 02:07:11 +09:00
c6959bb0bc Can start without NBXplorer being ready 2017-12-17 01:04:20 +09:00
d4dd6c84bc Auto detect NGinx X-Forwarded 2017-12-15 19:11:48 +09:00
e59678360c Update background 2017-12-13 22:38:07 +09:00
1b6fa0c7d8 Prepare Eclair integration 2017-12-13 15:49:19 +09:00
95a5936daf Update youtube links 2017-12-11 18:03:06 +09:00
477d4117ce update slack invite site 2017-12-08 22:04:52 +09:00
444f119e50 Add twitter link 2017-12-08 17:02:10 +09:00
fa13a2874e Estimate rate with BTCPay if BitcoinAverage stops works 2017-12-08 15:04:47 +09:00
24ce325e31 Support electrum segwit xpub format 2017-12-06 18:08:21 +09:00
a52a1901c4 Can delete user 2017-12-04 14:39:02 +09:00
45aee607e3 Can lock down registrations 2017-12-04 00:55:39 +09:00
c263016939 fix help 2017-12-03 23:42:10 +09:00
741915b1f8 Allow filtering of invoices over storeid and status 2017-12-03 23:35:52 +09:00
6f2534ba82 Can set currency in the create invoice form fix 2017-12-03 22:36:04 +09:00
43635071d9 Show ISO code in checkout page 2017-12-03 22:14:08 +09:00
22f06ecd4e Can set store policy to define how much time to wait before passing a transaction from paid to invalid. 2017-12-03 14:43:52 +09:00
106 changed files with 6574 additions and 1666 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.csBTCPayServer.csproj
Configuration
Controllers
Data
DerivationStrategy.cs
Eclair
EventAggregator.cs
Events
ExplorerClientProvider.csExtensions.cs
HostedServices
Hosting
Logging
Migrations
Models
Properties
SearchString.cs
Services
Validations
Views
wwwroot
DockerfileREADME.mdbtcpayserver.sln

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

@ -106,19 +106,15 @@ namespace BTCPayServer.Tests
.UseStartup<Startup>()
.Build();
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
}
public BTCPayServerRuntime Runtime
{
get; set;
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
}
public string HostName
{
get;
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public T GetService<T>()
{

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
public class EclairTester
{
ServerTester parent;
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
{
this.parent = parent;
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public EclairRPCClient RPC { get; }
public string P2PHost { get; }
NodeInfo _NodeInfo;
public async Task<NodeInfo> GetNodeInfoAsync()
{
if (_NodeInfo != null)
return _NodeInfo;
var info = await RPC.GetInfoAsync();
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
return _NodeInfo;
}
public NodeInfo GetNodeInfo()
{
return GetNodeInfoAsync().GetAwaiter().GetResult();
}
}
}

@ -1,12 +1,14 @@
# How to run the tests
# How to be started for development
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
BTCPay Server tests depend 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.
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
```
docker-compose up nbxplorer
docker-compose up dev
```
You can run the tests while it is running through your favorite IDE, or with
@ -43,4 +45,4 @@ docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitc
If you are using Powershell:
```
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
```

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -16,6 +17,7 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
@ -45,7 +47,6 @@ namespace BTCPayServer.Tests
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"))
@ -56,9 +57,42 @@ namespace BTCPayServer.Tests
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1");
CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2");
}
private string GetEnvironment(string variable, string defaultValue)
/// <summary>
/// This will setup a channel going from customer to merchant
/// </summary>
public void PrepareLightning()
{
PrepareLightningAsync().GetAwaiter().GetResult();
}
public async Task PrepareLightningAsync()
{
// Activate segwit
var blockCount = ExplorerNode.GetBlockCountAsync();
// Fetch node info, but that in cache
var merchant = MerchantEclair.GetNodeInfoAsync();
var customer = CustomerEclair.GetNodeInfoAsync();
var channels = CustomerEclair.RPC.ChannelsAsync();
var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result);
await Task.WhenAll(blockCount, merchant, customer, channels, connect);
// Mine until segwit is activated
if (blockCount.Result <= 432)
{
ExplorerNode.Generate(433 - blockCount.Result);
}
}
public EclairTester MerchantEclair { get; set; }
public EclairTester CustomerEclair { get; set; }
internal string GetEnvironment(string variable, string defaultValue)
{
var var = Environment.GetEnvironmentVariable(variable);
return String.IsNullOrEmpty(var) ? defaultValue : var;
@ -68,12 +102,6 @@ namespace BTCPayServer.Tests
{
return new TestAccount(this);
}
public bool FakeCallback
{
get;
set;
}
public RPCClient ExplorerNode
{
get; set;
@ -178,43 +206,6 @@ namespace BTCPayServer.Tests
}
}
/// <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
{

@ -59,8 +59,14 @@ namespace BTCPayServer.Tests
DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.UpdateStore(StoreId, new StoreViewModel()
{
DerivationScheme = DerivationScheme.ToString(),
SpeedPolicy = SpeedPolicy.MediumSpeed
});
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = "BTC",
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
}, "Save");
return store;
}

@ -22,6 +22,8 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
namespace BTCPayServer.Tests
{
@ -37,33 +39,126 @@ namespace BTCPayServer.Tests
public void CanCalculateCryptoDue()
{
var entity = new InvoiceEntity();
#pragma warning disable CS0618
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
var cryptoData = entity.GetCryptoData("BTC");
Assert.NotNull(cryptoData); // Should use legacy data to build itself
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
Assert.Equal(Money.Coins(1.1m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.1m), entity.GetTotalCryptoDue());
var accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.2m), entity.GetTotalCryptoDue());
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Coins(0.6m), entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
accounting = cryptoData.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
Assert.Equal(Money.Zero, entity.GetCryptoDue());
Assert.Equal(Money.Coins(1.3m), entity.GetTotalCryptoDue());
accounting = cryptoData.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.SetCryptoData(new System.Collections.Generic.Dictionary<string, CryptoData>(new KeyValuePair<string, CryptoData>[] {
new KeyValuePair<string,CryptoData>("BTC", new CryptoData()
{
Rate = 1000,
TxFee = Money.Coins(0.1m)
}),
new KeyValuePair<string,CryptoData>("LTC", new CryptoData()
{
Rate = 500,
TxFee = Money.Coins(0.01m)
})
}));
entity.Payments = new List<PaymentEntity>();
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
// Paying 2 BTC fee, LTC fee removed because fully paid
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
Assert.Equal(1, accounting.TxCount);
Assert.Equal(accounting.Paid, accounting.TotalDue);
#pragma warning restore CS0618
}
[Fact]
@ -107,7 +202,6 @@ 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);
@ -115,6 +209,24 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanUseLightMoney()
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
}
//[Fact]
//public void CanSendLightningPayment()
//{
// using (var tester = ServerTester.Create())
// {
// tester.Start();
// tester.PrepareLightning();
// }
//}
[Fact]
public void CanUseServerInitiatedPairingCode()
{
@ -163,7 +275,6 @@ 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();
@ -223,7 +334,6 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
@ -244,13 +354,36 @@ namespace BTCPayServer.Tests
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
});
}
}
[Fact]
public void CanParseFilter()
{
var filter = "storeid:abc status:abed blabhbalh ";
var search = new SearchString(filter);
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
Assert.Equal("blabhbalh", search.TextSearch);
Assert.Equal("abc", search.Filters["storeid"]);
Assert.Equal("abed", search.Filters["status"]);
}
[Fact]
public void TestAccessBitpayAPI()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -258,9 +391,7 @@ namespace BTCPayServer.Tests
{
tester.Start();
var user = tester.NewAccount();
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
user.GrantAccess();
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
@ -277,31 +408,31 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
var textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.OrderId
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
textSearchResult = tester.PayTester.Runtime.InvoiceRepository.GetInvoices(new InvoiceQuery()
Assert.Single(textSearchResult);
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
{
StoreId = user.StoreId,
TextSearch = invoice.Id
}).GetAwaiter().GetResult();
Assert.Equal(1, textSearchResult.Length);
Assert.Single(textSearchResult);
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.Equal(false, (bool)((JValue)invoice.ExceptionStatus).Value);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)).Length);
Assert.Equal(1, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime).Length);
Assert.Equal(0, user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)).Length);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);
@ -318,27 +449,26 @@ namespace BTCPayServer.Tests
cashCow.SendToAddress(invoiceAddress, firstPayment);
var invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
Assert.Equal(1, invoiceEntity.HistoricalAddresses.Length);
Assert.Single(invoiceEntity.HistoricalAddresses);
Assert.Null(invoiceEntity.HistoricalAddresses[0].UnAssigned);
Money secondPayment = Money.Zero;
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("new", localInvoice.Status);
Assert.Equal(firstPayment, localInvoice.BtcPaid);
txFee = localInvoice.BtcDue - invoice.BtcDue;
Assert.Equal("paidPartial", localInvoice.ExceptionStatus);
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
Assert.True(IsMapped(invoice, ctx));
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == invoice.BitcoinAddress.ToString());
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress.ToString());
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.Address == localInvoice.BitcoinAddress.ToString());
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress.ToString());
Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
@ -348,21 +478,19 @@ 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);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
Assert.True(IsMapped(localInvoice, ctx));
Assert.Equal(false, (bool)((JValue)localInvoice.ExceptionStatus).Value);
Assert.False((bool)((JValue)localInvoice.ExceptionStatus).Value);
});
cashCow.Generate(1); //The user has medium speed settings, so 1 conf is enough to be confirmed
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("confirmed", localInvoice.Status);
});
@ -371,9 +499,9 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback();
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("complete", localInvoice.Status);
Assert.NotEqual(0.0, localInvoice.Rate);
});
invoice = user.BitPay.CreateInvoice(new Invoice()
@ -393,7 +521,6 @@ namespace BTCPayServer.Tests
Eventually(() =>
{
tester.SimulateCallback(invoiceAddress);
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal("paid", localInvoice.Status);
Assert.Equal(Money.Zero, localInvoice.BtcDue);
@ -404,7 +531,6 @@ 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);
@ -416,11 +542,11 @@ namespace BTCPayServer.Tests
[Fact]
public void CheckRatesProvider()
{
var coinAverage = new CoinAverageRateProvider();
var coinAverage = new CoinAverageRateProvider("BTC");
var jpy = coinAverage.GetRateAsync("JPY").GetAwaiter().GetResult();
var jpy2 = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRateAsync("JPY").GetAwaiter().GetResult();
var cached = new CachedRateProvider(coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
var cached = new CachedRateProvider("BTC", coinAverage, new MemoryCache(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }));
cached.CacheSpan = TimeSpan.FromSeconds(10);
var a = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
@ -430,8 +556,8 @@ namespace BTCPayServer.Tests
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.Address == h) != null;
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash;
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetHash() == h) != null;
}
private void Eventually(Action act)

@ -1,5 +1,8 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
services:
tests:
@ -8,20 +11,34 @@ services:
dockerfile: BTCPayServer.Tests/Dockerfile
environment:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_BTCNBXPLORERURL: http://bitcoin-nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://litecoin-nbxplorer:32839/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_FAKECALLBACK: 'true'
TESTS_PORT: 80
TESTS_HOSTNAME: tests
expose:
- "80"
links:
- nbxplorer
- bitcoin-nbxplorer
- litecoin-nbxplorer
- postgres
extra_hosts:
- "tests:127.0.0.1"
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.29
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.15.0.1
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
connect=bitcoind:39388
links:
- bitcoin-nbxplorer
- litecoin-nbxplorer
- postgres
bitcoin-nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.42
ports:
- "32838:32838"
expose:
@ -37,30 +54,72 @@ services:
NBXPLORER_NOAUTH: 1
links:
- bitcoind
- postgres
eclair:
image: nicolasdorier/docker-bitcoin:0.15.0.1
litecoin-nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.42
ports:
- "32839:32839"
expose:
- "32839"
environment:
NBXPLORER_NETWORK: ltc-regtest
NBXPLORER_RPCURL: http://litecoind:43782/
NBXPLORER_RPCUSER: ceiwHEbqWI83
NBXPLORER_RPCPASSWORD: DwubwWsoo3
NBXPLORER_NODEENDPOINT: litecoind:39388
NBXPLORER_BIND: 0.0.0.0:32839
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.15.0.1
ports:
- "43782:43782"
- "39388:39388"
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
ports:
- "43782:43782"
expose:
- "43782"
- "39388"
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
litecoind:
container_name: btcpayserver_dev_litecoind
image: nicolasdorier/docker-litecoin:0.14.2
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
rpcpassword=DwubwWsoo3
regtest=1
server=1
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
ports:
- "43783:43782"
expose:
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
postgres:
image: postgres:9.6.5
ports:
- "39372:5432"
expose:
- "5432"

@ -0,0 +1 @@
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
namespace BTCPayServer
{
public class BTCPayNetwork
{
public Network NBitcoinNetwork { get; set; }
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string UriScheme { get; internal set; }
public IRateProvider DefaultRateProvider { get; set; }
[Obsolete("Should not be needed")]
public bool IsBTC
{
get
{
return CryptoCode == "BTC";
}
}
public string CryptoImagePath { get; set; }
}
}

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitpayClient;
namespace BTCPayServer
{
public class BTCPayNetworkProvider
{
static BTCPayNetworkProvider()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
}
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
public BTCPayNetworkProvider(Network network)
{
var coinaverage = new CoinAverageRateProvider("BTC");
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
var ltcRate = new CoinAverageRateProvider("LTC");
if (network == Network.Main)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.Main,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "https://live.blockcypher.com/ltc/tx/{0}/",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Mainnet,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
}
if (network == Network.TestNet)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.TestNet,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Testnet,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
}
if (network == Network.RegTest)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.RegTest,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Regtest,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
}
}
[Obsolete("To use only for legacy stuff")]
public BTCPayNetwork BTC
{
get
{
return GetNetwork("BTC");
}
}
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode, network);
}
public IEnumerable<BTCPayNetwork> GetAll()
{
return _Networks.Values.ToArray();
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
return network;
}
}
}

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.33</Version>
<Version>1.0.0.61</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -18,13 +18,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.1" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<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.48" />
<PackageReference Include="NBitpayClient" Version="1.0.0.12" />
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<PackageReference Include="NBitpayClient" Version="1.0.0.14" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.18" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.26" />
<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" />
@ -34,9 +34,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

@ -9,6 +9,7 @@ using System.Net;
using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
namespace BTCPayServer.Configuration
{
@ -18,15 +19,6 @@ namespace BTCPayServer.Configuration
{
get; set;
}
public Uri Explorer
{
get; set;
}
public string CookieFile
{
get; set;
}
public string ConfigurationFile
{
get;
@ -53,17 +45,42 @@ namespace BTCPayServer.Configuration
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + Network);
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
foreach (var net in new BTCPayNetworkProvider(Network).GetAll())
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(net.NBitcoinNetwork.Name);
var explorer = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", null);
var cookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
if (explorer != null)
{
ExplorerFactories.Add(net.CryptoCode, (n) => CreateExplorerClient(n, explorer, cookieFile));
}
}
// Handle legacy explorer.url and explorer.cookiefile
if (ExplorerFactories.Count == 0)
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(Network.Name); // Will get BTC info
var explorer = conf.GetOrDefault<Uri>($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute));
var cookieFile = conf.GetOrDefault<string>($"explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
ExplorerFactories.Add("BTC", (n) => CreateExplorerClient(n, explorer, cookieFile));
}
//////
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
}
public bool RequireHttps
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
get; set;
var explorer = new ExplorerClient(n.NBitcoinNetwork, uri);
if (!explorer.SetCookieAuth(cookieFile))
explorer.SetNoAuth();
return explorer;
}
public Dictionary<string, Func<BTCPayNetwork, ExplorerClient>> ExplorerFactories = new Dictionary<string, Func<BTCPayNetwork, ExplorerClient>>();
public string PostgresConnectionString
{
get;

@ -1,110 +0,0 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using DBreeze;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Configuration
{
public class BTCPayServerRuntime : IDisposable
{
public ExplorerClient Explorer
{
get;
private set;
}
public void Configure(BTCPayServerOptions opts)
{
ConfigureAsync(opts).GetAwaiter().GetResult();
}
public async Task ConfigureAsync(BTCPayServerOptions opts)
{
Network = opts.Network;
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
if (!Explorer.SetCookieAuth(opts.CookieFile))
Explorer.SetNoAuth();
CancellationTokenSource cts = new CancellationTokenSource(30000);
try
{
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
Logs.Configuration.LogInformation("Connection successfull");
}
catch (Exception ex)
{
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
}
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
DBFactory = dbContext;
InvoiceRepository = new InvoiceRepository(dbContext, CreateDBPath(opts, "InvoiceDB"), Network);
_Resources.Add(InvoiceRepository);
}
private static string CreateDBPath(BTCPayServerOptions opts, string name)
{
var dbpath = Path.Combine(opts.DataDir, name);
if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return dbpath;
}
List<IDisposable> _Resources = new List<IDisposable>();
public void Dispose()
{
lock (_Resources)
{
foreach (var r in _Resources)
{
r.Dispose();
}
_Resources.Clear();
}
}
public Network Network
{
get;
private set;
}
public InvoiceRepository InvoiceRepository
{
get;
set;
}
public ApplicationDbContextFactory DBFactory
{
get;
set;
}
}
}

@ -26,11 +26,13 @@ namespace BTCPayServer.Configuration
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
app.Option("--requirehttps", $"Will redirect to https version of the website (default: false)", CommandOptionType.BoolValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
app.Option("--explorerurl", $"Url of the NBxplorer (default: : Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
app.Option("--externalurl", $"The expected external url of this service, use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
foreach (var network in new BTCPayNetworkProvider(Network.Main).GetAll())
{
app.Option($"--{network.CryptoCode}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: If no explorer is specified, the default for Bitcoin will be selected)", CommandOptionType.SingleValue);
app.Option($"--{network.CryptoCode}explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
}
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
return app;
}
@ -77,7 +79,6 @@ namespace BTCPayServer.Configuration
builder.AppendLine("#regtest=0");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#requirehttps=0");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
@ -85,8 +86,12 @@ namespace BTCPayServer.Configuration
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
builder.AppendLine("#explorer.url=" + network.DefaultExplorerUrl.AbsoluteUri);
builder.AppendLine("#explorer.cookiefile=" + network.DefaultExplorerCookieFile);
foreach (var n in new BTCPayNetworkProvider(network.Network).GetAll())
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(n.NBitcoinNetwork.ToString());
builder.AppendLine($"#{n.CryptoCode}.explorer.url={nbxplorer.GetDefaultExplorerUrl()}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ nbxplorer.GetDefaultCookieFile()}");
}
return builder.ToString();
}

@ -13,25 +13,20 @@ namespace BTCPayServer.Configuration
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
foreach (var network in Network.GetNetworks())
foreach (var network in new[] { Network.Main, Network.TestNet, Network.RegTest })
{
NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.DefaultExplorerCookieFile = Path.Combine(StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", network.Name, false), ".cookie");
info.Network = network;
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24446", UriKind.Absolute);
info.DefaultPort = 23002;
_Networks.Add(network.Name, info);
if (network == Network.Main)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24444", UriKind.Absolute);
Main = info;
info.DefaultPort = 23000;
}
if (network == Network.TestNet)
{
info.DefaultExplorerUrl = new Uri("http://127.0.0.1:24445", UriKind.Absolute);
info.DefaultPort = 23001;
}
}
@ -54,12 +49,7 @@ namespace BTCPayServer.Configuration
}
return null;
}
public static NetworkInformation Main
{
get;
set;
}
public Network Network
{
get; set;
@ -74,21 +64,11 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri DefaultExplorerUrl
{
get;
internal set;
}
public int DefaultPort
{
get;
private set;
}
public string DefaultExplorerCookieFile
{
get;
internal set;
}
public override string ToString()
{

@ -235,8 +235,11 @@ namespace BTCPayServer.Controllers
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string returnUrl = null)
public async Task<IActionResult> Register(string returnUrl = null)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
ViewData["ReturnUrl"] = returnUrl;
return View();
}
@ -247,9 +250,11 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
if (policies.LockSubscription)
return RedirectToAction(nameof(HomeController.Index), "Home");
if (ModelState.IsValid)
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
var user = new ApplicationUser { UserName = model.Email, Email = model.Email, RequiresEmailConfirmation = policies.RequiresConfirmedEmail };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
@ -268,7 +273,6 @@ namespace BTCPayServer.Controllers
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_logger.LogInformation("User created a new account with password.");
if (!policies.RequiresConfirmedEmail)
{
await _signInManager.SignInAsync(user, isPersistent: false);

@ -1,116 +0,0 @@
using BTCPayServer.Logging;
using BTCPayServer.Services.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;
}
}
}

@ -24,16 +24,19 @@ namespace BTCPayServer.Controllers
private InvoiceRepository _InvoiceRepository;
private TokenRepository _TokenRepository;
private StoreRepository _StoreRepository;
private BTCPayNetworkProvider _NetworkProvider;
public InvoiceControllerAPI(InvoiceController invoiceController,
InvoiceRepository invoceRepository,
TokenRepository tokenRepository,
StoreRepository storeRepository)
StoreRepository storeRepository,
BTCPayNetworkProvider networkProvider)
{
this._InvoiceController = invoiceController;
this._InvoiceRepository = invoceRepository;
this._TokenRepository = tokenRepository;
this._StoreRepository = storeRepository;
this._NetworkProvider = networkProvider;
}
[HttpPost]
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
if (invoice == null)
throw new BitpayHttpException(404, "Object not found");
var resp = invoice.EntityToDTO();
var resp = invoice.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp);
}
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
var entities = (await _InvoiceRepository.GetInvoices(query))
.Select((o) => o.EntityToDTO()).ToArray();
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
return DataWrapper.Create(entities);
}

@ -17,21 +17,25 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
return NotFound();
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
};
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = _Network;
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
@ -57,14 +61,18 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("i/{invoiceId}", Order = 99)]
[Route("i/{invoiceId}/{cryptoCode}", Order = 99)]
[MediaTypeConstraint("application/bitcoin-payment")]
public async Task<IActionResult> PostPayment(string invoiceId)
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
return NotFound();
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(payment.Transactions);
var unused = _Wallet.BroadcastTransactionsAsync(network, payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}

@ -15,27 +15,16 @@ using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpPost]
[Route("invoices/{invoiceId}")]
public IActionResult Invoice(string invoiceId, string command)
{
if (command == "refresh")
{
_Watcher.Watch(invoiceId);
}
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)
@ -43,13 +32,15 @@ namespace BTCPayServer.Controllers
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId
InvoiceId = invoiceId,
IncludeAddresses = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
var dto = invoice.EntityToDTO();
var dto = invoice.EntityToDTO(_NetworkProvider);
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
@ -59,34 +50,46 @@ namespace BTCPayServer.Controllers
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
MonitoringDate = invoice.MonitoringExpiration,
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",
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation,
BitcoinAddress = invoice.DepositAddress,
PaymentUrl = dto.PaymentUrls.BIP72
};
foreach (var data in invoice.GetCryptoData())
{
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
var accounting = data.Value.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Address = data.Value.DepositAddress.ToString();
cryptoPayment.Rate = FormatCurrency(data.Value);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
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;
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.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}";
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
return m;
})
.ToArray();
await Task.WhenAll(payments);
model.Addresses = invoice.HistoricalAddresses;
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
model.StatusMessage = StatusMessage;
return View(model);
@ -94,60 +97,90 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId);
var model = await GetInvoiceModel(invoiceId, cryptoCode);
if (model == null)
return NotFound();
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
var cryptoFormat = _CurrencyNameTable.GetCurrencyProvider("BTC");
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (cryptoCode == null)
cryptoCode = store.GetDefaultCrypto();
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || network == null || !invoice.Support(network))
return null;
var cryptoData = invoice.GetCryptoData(network);
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var currency = invoice.ProductInformation.Currency;
var accounting = cryptoData.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BtcAddress = invoice.DepositAddress.ToString(),
BtcAmount = (invoice.GetTotalCryptoDue() - invoice.TxFee).ToString(),
BtcTotalDue = invoice.GetTotalCryptoDue().ToString(),
BtcDue = invoice.GetCryptoDue().ToString(),
BtcAddress = cryptoData.DepositAddress,
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = invoice.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(invoice.ProductInformation.Currency)),
Rate = FormatCurrency(cryptoData),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.GetTxCount(),
BtcPaid = invoice.GetTotalPaid().ToString(),
Status = invoice.Status
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
TxCount = accounting.TxCount,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {cryptoData.TxFee} {network.CryptoCode}",
AvailableCryptos = invoice.GetCryptoData().Select(kv=> new PaymentModel.AvailableCrypto()
{
CryptoCode = kv.Key,
CryptoImage = "/" + _NetworkProvider.GetNetwork(kv.Key).CryptoImagePath,
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, cryptoCode = kv.Key })
}).ToList()
};
var isMultiCurrency = invoice.Payments.Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
return model;
}
private string FormatCurrency(CryptoData cryptoData)
{
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
return FormatCurrency(cryptoData.Rate, currency);
}
public string FormatCurrency(decimal price, string currency)
{
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
}
private string PrettyPrint(TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();
@ -161,14 +194,75 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId)
[Route("i/{invoiceId}/{cryptoCode}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
{
var model = await GetInvoiceModel(invoiceId);
var model = await GetInvoiceModel(invoiceId, cryptoCode);
if (model == null)
return NotFound();
return Json(model);
}
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
if (message.MessageType == WebSocketMessageType.Close)
break;
}
}
finally
{
leases.Dispose();
await CloseSocket(webSocket);
}
return new EmptyResult();
}
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
return;
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
try
{
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
}
catch { await CloseSocket(webSocket); }
}
private static async Task CloseSocket(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { webSocket.Dispose(); }
}
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
@ -188,12 +282,15 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 20)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = searchTerm,
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId()
UserId = GetUserId(),
Status = filterString.Filters.TryGet("status"),
StoreId = filterString.Filters.TryGet("storeid")
}))
{
model.SearchTerm = searchTerm;
@ -232,13 +329,13 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model)
{
model.Stores = await GetStores(GetUserId(), model.StoreId);
if (!ModelState.IsValid)
{
model.Stores = await GetStores(GetUserId(), model.StoreId);
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
if (string.IsNullOrEmpty(store.DerivationStrategy))
if (store.GetDerivationStrategies(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
@ -246,21 +343,30 @@ namespace BTCPayServer.Controllers
storeId = store.Id
});
}
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = "USD",
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
try
{
var result = await CreateInvoiceCore(new Invoice()
{
Price = model.Amount.Value,
Currency = model.Currency,
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,
BuyerEmail = model.BuyerEmail,
}, store, HttpContext.Request.GetAbsoluteRoot());
StatusMessage = $"Invoice {result.Data.Id} just created!";
return RedirectToAction(nameof(ListInvoices));
}
catch (RateUnavailableException)
{
ModelState.TryAddModelError(nameof(model.Currency), "Unsupported currency");
return View(model);
}
}
private async Task<SelectList> GetStores(string userId, string storeId = null)

@ -38,6 +38,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Controllers
{
@ -45,57 +46,60 @@ namespace BTCPayServer.Controllers
{
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
IRateProviderFactory _RateProviders;
StoreRepository _StoreRepository;
Network _Network;
UserManager<ApplicationUser> _UserManager;
IFeeProvider _FeeProvider;
IFeeProviderFactory _FeeProviderFactory;
private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
EventAggregator _EventAggregator;
BTCPayNetworkProvider _NetworkProvider;
ExplorerClientProvider _ExplorerClients;
public InvoiceController(InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
IRateProvider rateProvider,
IRateProviderFactory rateProviders,
StoreRepository storeRepository,
InvoiceWatcher watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
EventAggregator eventAggregator,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerClientProviders,
IFeeProviderFactory feeProviderFactory)
{
_ExplorerClients = explorerClientProviders;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_Explorer = explorerClient ?? throw new ArgumentNullException(nameof(explorerClient));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_Network = network ?? throw new ArgumentNullException(nameof(network));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_Watcher = watcher ?? throw new ArgumentNullException(nameof(watcher));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15, double monitoringMinutes = 60)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
{
//TODO: expiryMinutes (time before a new invoice can become paid) and monitoringMinutes (time before a paid invoice becomes invalid) should be configurable at store level
var derivationStrategy = store.DerivationStrategy;
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
if (derivationStrategies.Count == 0)
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
InvoiceTime = DateTimeOffset.UtcNow
};
entity.SetDerivationStrategies(derivationStrategies);
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.MonitoringExpiration = entity.InvoiceTime.AddMinutes(monitoringMinutes);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
entity.FullNotifications = invoice.FullNotifications;
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
//Another way of passing buyer info to support
@ -111,16 +115,43 @@ namespace BTCPayServer.Controllers
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var getFeeRate = _FeeProvider.GetFeeRateAsync();
var getRate = _RateProvider.GetRateAsync(invoice.Currency);
var getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy));
entity.TxFee = store.GetStoreBlob(_Network).NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate;
var queries = derivationStrategies
.Select(derivationStrategy =>
{
return new
{
network = derivationStrategy.Network,
getFeeRate = _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network).GetFeeRateAsync(),
getRate = _RateProviders.GetRateProvider(derivationStrategy.Network).GetRateAsync(invoice.Currency),
getAddress = _Wallet.ReserveAddressAsync(derivationStrategy)
};
});
var cryptoDatas = new Dictionary<string, CryptoData>();
foreach (var q in queries)
{
CryptoData cryptoData = new CryptoData();
cryptoData.CryptoCode = q.network.CryptoCode;
cryptoData.FeeRate = (await q.getFeeRate);
cryptoData.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : cryptoData.FeeRate.GetFee(100); // assume price for 100 bytes
cryptoData.Rate = await q.getRate;
cryptoData.DepositAddress = (await q.getAddress).ToString();
#pragma warning disable CS0618
if (q.network.IsBTC)
{
entity.TxFee = cryptoData.TxFee;
entity.Rate = cryptoData.Rate;
entity.DepositAddress = cryptoData.DepositAddress;
}
#pragma warning restore CS0618
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
}
entity.SetCryptoData(cryptoDatas);
entity.PosData = invoice.PosData;
entity.DepositAddress = await getAddress;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity);
_Watcher.Watch(entity.Id);
var resp = entity.EntityToDTO();
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -152,9 +183,9 @@ namespace BTCPayServer.Controllers
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy)
private DerivationStrategyBase ParseDerivationStrategy(string derivationStrategy, BTCPayNetwork network)
{
return new DerivationStrategyFactory(_Network).Parse(derivationStrategy);
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
}
private TDest Map<TFrom, TDest>(TFrom data)

@ -32,15 +32,50 @@ namespace BTCPayServer.Controllers
public IActionResult ListUsers()
{
var users = new UsersViewModel();
users.StatusMessage = StatusMessage;
users.Users
= _UserManager.Users.Select(u => new UsersViewModel.UserViewModel()
{
Name = u.UserName,
Email = u.Email
Email = u.Email,
Id = u.Id
}).ToList();
return View(users);
}
[Route("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUser(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete user " + user.Email,
Description = "This user will be permanently deleted",
Action = "Delete"
});
}
[Route("server/users/{userId}/delete")]
[HttpPost]
public async Task<IActionResult> DeleteUserPost(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
await _UserManager.DeleteAsync(user);
StatusMessage = "User deleted";
return RedirectToAction(nameof(ListUsers));
}
[TempData]
public string StatusMessage
{
get; set;
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using System;
@ -29,11 +30,11 @@ namespace BTCPayServer.Controllers
public StoresController(
StoreRepository repo,
TokenRepository tokenRepo,
CallbackController callbackController,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
Network network,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IHostingEnvironment env)
{
_Repo = repo;
@ -42,11 +43,11 @@ namespace BTCPayServer.Controllers
_TokenController = tokenController;
_Wallet = wallet;
_Env = env;
_Network = network;
_CallbackController = callbackController;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
}
Network _Network;
CallbackController _CallbackController;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
BTCPayWallet _Wallet;
AccessTokenController _TokenController;
StoreRepository _Repo;
@ -92,8 +93,12 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray();
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(async ss => (await _Wallet.GetBalance(ss)).ToString() + " " + ss.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
@ -102,7 +107,7 @@ namespace BTCPayServer.Controllers
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
Balance = await balances[i]
Balances = balances[i].Select(t => t.Result).ToArray()
});
}
return View(result);
@ -144,19 +149,117 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !store.GetStoreBlob(_Network).NetworkFeeDisabled;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.DerivationScheme = store.DerivationStrategy;
AddDerivationSchemes(store, vm);
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
return View(vm);
}
private void AddDerivationSchemes(StoreData store, StoreViewModel vm)
{
var strategies = store
.GetDerivationStrategies(_NetworkProvider)
.ToDictionary(s => s.Network.CryptoCode);
foreach (var explorerProvider in _ExplorerProvider.GetAll())
{
if (strategies.TryGetValue(explorerProvider.Item1.CryptoCode, out DerivationStrategy strat))
{
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = explorerProvider.Item1.CryptoCode,
Value = strat.DerivationStrategyBase.ToString()
});
}
}
}
[HttpGet]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
return View(vm);
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
if (command == "Save")
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
await _Wallet.TrackAsync(strategy);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(scheme.Network.NBitcoinNetwork).ToString()));
}
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
}
}
return View(vm);
}
}
[HttpPost]
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model, string command)
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
{
if (!ModelState.IsValid)
{
@ -165,83 +268,88 @@ namespace BTCPayServer.Controllers
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
AddDerivationSchemes(store, model);
if (command == "Save")
bool needUpdate = false;
if (store.SpeedPolicy != model.SpeedPolicy)
{
bool needUpdate = false;
if (store.SpeedPolicy != model.SpeedPolicy)
{
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.DerivationScheme)
{
needUpdate = true;
try
{
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
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);
}
}
if (store.GetStoreBlob(_Network).NetworkFeeDisabled != !model.NetworkFee)
{
var blob = store.GetStoreBlob(_Network);
blob.NetworkFeeDisabled = !model.NetworkFee;
store.SetStoreBlob(blob, _Network);
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(UpdateStore), new
{
storeId = storeId
});
needUpdate = true;
store.SpeedPolicy = model.SpeedPolicy;
}
else
if (store.StoreName != model.StoreName)
{
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);
needUpdate = true;
store.StoreName = model.StoreName;
}
if (store.StoreWebsite != model.StoreWebsite)
{
needUpdate = true;
store.StoreWebsite = model.StoreWebsite;
}
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
{
needUpdate = true;
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(UpdateStore), new
{
storeId = storeId
});
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
{
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, new string[] { });
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBitcoinNetwork == Network.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return DerivationStrategy.Parse(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme).ToString(), network);
}
[HttpGet]
@ -307,6 +415,7 @@ namespace BTCPayServer.Controllers
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
}
GeneratedPairingCode = pairingCode;
return RedirectToAction(nameof(RequestPairing), new
{
pairingCode = pairingCode,
@ -314,6 +423,8 @@ namespace BTCPayServer.Controllers
});
}
public string GeneratedPairingCode { get; set; }
[HttpGet]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]

@ -2,16 +2,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Data
{
public class AddressInvoiceData
{
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetHash instead")]
public string Address
{
get; set;
}
#pragma warning disable CS0618
public ScriptId GetHash()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
if (index == -1)
return new ScriptId(Address);
return new ScriptId(Address.Substring(0, index));
}
public AddressInvoiceData SetHash(ScriptId scriptId, string cryptoCode)
{
Address = scriptId + "#" + cryptoCode;
return this;
}
public string GetCryptoCode()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
if (index == -1)
return "BTC";
return Address.Substring(index + 1);
}
#pragma warning restore CS0618
public InvoiceData InvoiceData
{
get; set;
@ -26,5 +59,6 @@ namespace BTCPayServer.Data
{
get; set;
}
}
}

@ -113,7 +113,9 @@ namespace BTCPayServer.Data
.HasForeignKey(pt => pt.StoreDataId);
builder.Entity<AddressInvoiceData>()
#pragma warning disable CS0618
.HasKey(o => o.Address);
#pragma warning restore CS0618
builder.Entity<PairingCodeData>()
.HasKey(o => o.Id);
@ -128,7 +130,9 @@ namespace BTCPayServer.Data
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
}
}

@ -12,11 +12,42 @@ namespace BTCPayServer.Data
get; set;
}
/// <summary>
/// Some crypto currencies share same address prefix
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
/// </summary>
[Obsolete("Use GetCryptoCode instead")]
public string Address
{
get; set;
}
[Obsolete("Use GetCryptoCode instead")]
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
}
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
if (index == -1)
return Address;
return Address.Substring(0, index);
}
public HistoricalAddressInvoiceData SetAddress(string depositAddress, string cryptoCode)
{
Address = depositAddress + "#" + cryptoCode;
CryptoCode = cryptoCode;
return this;
}
#pragma warning restore CS0618
public DateTimeOffset Assigned
{
get; set;

@ -8,6 +8,9 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Data
{
@ -24,11 +27,90 @@ namespace BTCPayServer.Data
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get; set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies
{
get;
set;
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
if (!string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
{
btcReturned = true;
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
}
}
if (!string.IsNullOrEmpty(DerivationStrategies))
{
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var network = networks.GetNetwork(strat.Name);
if (network != null)
{
if (network == networks.BTC && btcReturned)
continue;
if (strat.Value.Type == JTokenType.Null)
continue;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
}
}
}
#pragma warning restore CS0618
}
public void SetDerivationStrategy(BTCPayNetwork network, string derivationScheme)
{
#pragma warning disable CS0618
JObject strategies = string.IsNullOrEmpty(DerivationStrategies) ? new JObject() : JObject.Parse(DerivationStrategies);
bool existing = false;
foreach (var strat in strategies.Properties().ToList())
{
if (strat.Name == network.CryptoCode)
{
if (network.IsBTC)
DerivationStrategy = null;
if (string.IsNullOrEmpty(derivationScheme))
{
strat.Remove();
}
else
{
strat.Value = new JValue(derivationScheme);
}
existing = true;
break;
}
}
if (!existing && string.IsNullOrEmpty(derivationScheme))
{
if(network.IsBTC)
DerivationStrategy = null;
}
else if(!existing)
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
// This is deprecated so we don't have to set anymore
//if (network.IsBTC)
// DerivationStrategy = derivationScheme;
DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
}
public string StoreName
{
get; set;
@ -59,23 +141,54 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
public string DefaultCrypto { get; set; }
public StoreBlob GetStoreBlob(Network network)
#pragma warning disable CS0618
public string GetDefaultCrypto()
{
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
return DefaultCrypto ?? "BTC";
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
DefaultCrypto = defaultCryptoCurrency;
}
#pragma warning restore CS0618
static Network Dummy = Network.Main;
public StoreBlob GetStoreBlob()
{
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
}
public void SetStoreBlob(StoreBlob storeBlob, Network network)
public bool SetStoreBlob(StoreBlob storeBlob)
{
StoreBlob = Encoding.UTF8.GetBytes(new Serializer(network).ToString(storeBlob));
var original = new Serializer(Dummy).ToString(GetStoreBlob());
var newBlob = new Serializer(Dummy).ToString(storeBlob);
if (original == newBlob)
return false;
StoreBlob = Encoding.UTF8.GetBytes(newBlob);
return true;
}
}
public class StoreBlob
{
public StoreBlob()
{
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
{
get; set;
}
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
{
get;
set;
}
}
}

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationStrategy
{
private DerivationStrategyBase _DerivationStrategy;
private BTCPayNetwork _Network;
DerivationStrategy(DerivationStrategyBase result, BTCPayNetwork network)
{
this._DerivationStrategy = result;
this._Network = network;
}
public static DerivationStrategy Parse(string derivationStrategy, BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var result = new NBXplorer.DerivationStrategy.DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationStrategy);
return new DerivationStrategy(result, network);
}
public BTCPayNetwork Network { get { return this._Network; } }
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
public override string ToString()
{
return _DerivationStrategy.ToString();
}
}
}

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class AllChannelResponse
{
public string ShortChannelId { get; set; }
public string NodeId1 { get; set; }
public string NodeId2 { get; set; }
}
}

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class ChannelResponse
{
public string NodeId { get; set; }
public string ChannelId { get; set; }
public string State { get; set; }
}
public static class ChannelStates
{
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
public const string NORMAL = "NORMAL";
}
}

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin.RPC;
namespace BTCPayServer.Eclair
{
public class EclairRPCClient
{
public EclairRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
}
public Network Network { get; private set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
}
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
{
var response = await SendCommandAsync(request, throwIfRPCError);
return Serializer.ToObject<T>(response.ResultString, Network);
}
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
{
RPCResponse response = null;
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
if (response == null)
{
var writer = new StringWriter();
request.WriteJSON(writer);
writer.Flush();
var json = writer.ToString();
var bytes = Encoding.UTF8.GetBytes(json);
#if !(PORTABLE || NETCORE)
webRequest.ContentLength = bytes.Length;
#endif
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await dataStream.FlushAsync().ConfigureAwait(false);
dataStream.Dispose();
}
WebResponse webResponse = null;
WebResponse errorResponse = null;
try
{
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
catch (WebException ex)
{
if (ex.Response == null || ex.Response.ContentLength == 0 ||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
throw;
errorResponse = ex.Response;
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
finally
{
if (errorResponse != null)
{
errorResponse.Dispose();
errorResponse = null;
}
if (webResponse != null)
{
webResponse.Dispose();
webResponse = null;
}
}
return response;
}
public AllChannelResponse[] AllChannels()
{
return AllChannelsAsync().GetAwaiter().GetResult();
}
public async Task<AllChannelResponse[]> AllChannelsAsync()
{
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
}
public string[] Channels()
{
return ChannelsAsync().GetAwaiter().GetResult();
}
public async Task<string[]> ChannelsAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
}
public void Close(string channelId)
{
CloseAsync(channelId).GetAwaiter().GetResult();
}
public async Task CloseAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
try
{
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
}
catch (RPCException ex) when (ex.Message == "closing already in progress")
{
}
}
public ChannelResponse Channel(string channelId)
{
return ChannelAsync(channelId).GetAwaiter().GetResult();
}
public async Task<ChannelResponse> ChannelAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
}
public string[] AllNodes()
{
return AllNodesAsync().GetAwaiter().GetResult();
}
public async Task<string[]> AllNodesAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
}
public Uri Address { get; private set; }
private HttpWebRequest CreateWebRequest()
{
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
webRequest.ContentType = "application/json";
webRequest.Method = "POST";
return webRequest;
}
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
{
MemoryStream ms = new MemoryStream();
await stream.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
return ms;
}
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
}
public string Connect(NodeInfo node)
{
return ConnectAsync(node).GetAwaiter().GetResult();
}
public async Task<string> ConnectAsync(NodeInfo node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
}
public string Receive(LightMoney amount, string description = null)
{
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
}
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
{
if (amount == null)
throw new ArgumentNullException(nameof(amount));
List<object> args = new List<object>();
args.Add(amount.MilliSatoshi);
if(description != null)
{
args.Add(description);
}
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
}
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
if (fundingSatoshi == null)
throw new ArgumentNullException(nameof(fundingSatoshi));
if (node == null)
throw new ArgumentNullException(nameof(node));
pushAmount = pushAmount ?? LightMoney.Zero;
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
return result.ResultString;
}
}
}

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Eclair
{
public class GetInfoResponse
{
public string NodeId { get; set; }
public string Alias { get; set; }
public int Port { get; set; }
public uint256 ChainHash { get; set; }
public int BlockHeight { get; set; }
}
}

@ -0,0 +1,569 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public enum LightMoneyUnit : ulong
{
BTC = 100000000000,
MilliBTC = 100000000,
Bit = 100000,
Satoshi = 1000,
MilliSatoshi = 1
}
public class LightMoney : IComparable, IComparable<LightMoney>, IEquatable<LightMoney>
{
// for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style
private const NumberStyles BitcoinStyle =
NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite
| NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint;
/// <summary>
/// Parse a bitcoin amount (Culture Invariant)
/// </summary>
/// <param name="bitcoin"></param>
/// <param name="nRet"></param>
/// <returns></returns>
public static bool TryParse(string bitcoin, out LightMoney nRet)
{
nRet = null;
decimal value;
if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value))
{
return false;
}
try
{
nRet = new LightMoney(value, LightMoneyUnit.BTC);
return true;
}
catch (OverflowException)
{
return false;
}
}
/// <summary>
/// Parse a bitcoin amount (Culture Invariant)
/// </summary>
/// <param name="bitcoin"></param>
/// <returns></returns>
public static LightMoney Parse(string bitcoin)
{
LightMoney result;
if (TryParse(bitcoin, out result))
{
return result;
}
throw new FormatException("Impossible to parse the string in a bitcoin amount");
}
long _MilliSatoshis;
public long MilliSatoshi
{
get
{
return _MilliSatoshis;
}
// used as a central point where long.MinValue checking can be enforced
private set
{
CheckLongMinValue(value);
_MilliSatoshis = value;
}
}
/// <summary>
/// Get absolute value of the instance
/// </summary>
/// <returns></returns>
public LightMoney Abs()
{
var a = this;
if (a < LightMoney.Zero)
a = -a;
return a;
}
public LightMoney(int satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(uint satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(long satoshis)
{
MilliSatoshi = satoshis;
}
public LightMoney(ulong satoshis)
{
// overflow check.
// ulong.MaxValue is greater than long.MaxValue
checked
{
MilliSatoshi = (long)satoshis;
}
}
public LightMoney(decimal amount, LightMoneyUnit unit)
{
// sanity check. Only valid units are allowed
CheckMoneyUnit(unit, "unit");
checked
{
var satoshi = amount * (long)unit;
MilliSatoshi = (long)satoshi;
}
}
/// <summary>
/// Split the Money in parts without loss
/// </summary>
/// <param name="parts">The number of parts (must be more than 0)</param>
/// <returns>The splitted money</returns>
public IEnumerable<LightMoney> Split(int parts)
{
if (parts <= 0)
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
long remain;
long result = DivRem(_MilliSatoshis, parts, out remain);
for (int i = 0; i < parts; i++)
{
yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0));
remain--;
}
}
private static long DivRem(long a, long b, out long result)
{
result = a % b;
return a / b;
}
public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit)
{
return new LightMoney(amount, unit);
}
/// <summary>
/// Convert Money to decimal (same as ToDecimal)
/// </summary>
/// <param name="unit"></param>
/// <returns></returns>
public decimal ToUnit(LightMoneyUnit unit)
{
CheckMoneyUnit(unit, "unit");
// overflow safe because (long / int) always fit in decimal
// decimal operations are checked by default
return (decimal)MilliSatoshi / (int)unit;
}
/// <summary>
/// Convert Money to decimal (same as ToUnit)
/// </summary>
/// <param name="unit"></param>
/// <returns></returns>
public decimal ToDecimal(LightMoneyUnit unit)
{
return ToUnit(unit);
}
public static LightMoney Coins(decimal coins)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC);
}
public static LightMoney Bits(decimal bits)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
}
public static LightMoney Cents(decimal cents)
{
// overflow safe.
// decimal operations are checked by default
return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC);
}
public static LightMoney Satoshis(decimal sats)
{
return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC);
}
public static LightMoney Satoshis(ulong sats)
{
return new LightMoney(sats);
}
public static LightMoney Satoshis(long sats)
{
return new LightMoney(sats);
}
public static LightMoney MilliSatoshis(long msats)
{
return new LightMoney(msats);
}
public static LightMoney MilliSatoshis(ulong msats)
{
return new LightMoney(msats);
}
#region IEquatable<Money> Members
public bool Equals(LightMoney other)
{
if (other == null)
return false;
return _MilliSatoshis.Equals(other._MilliSatoshis);
}
public int CompareTo(LightMoney other)
{
if (other == null)
return 1;
return _MilliSatoshis.CompareTo(other._MilliSatoshis);
}
#endregion
#region IComparable Members
public int CompareTo(object obj)
{
if (obj == null)
return 1;
LightMoney m = obj as LightMoney;
if (m != null)
return _MilliSatoshis.CompareTo(m._MilliSatoshis);
#if !(PORTABLE || NETCORE)
return _MilliSatoshis.CompareTo(obj);
#else
return _Satoshis.CompareTo((long)obj);
#endif
}
#endregion
public static LightMoney operator -(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis));
}
public static LightMoney operator -(LightMoney left)
{
if (left == null)
throw new ArgumentNullException("left");
return new LightMoney(checked(-left._MilliSatoshis));
}
public static LightMoney operator +(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis));
}
public static LightMoney operator *(int left, LightMoney right)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator *(LightMoney right, int left)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(right._MilliSatoshis * left));
}
public static LightMoney operator *(long left, LightMoney right)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator *(LightMoney right, long left)
{
if (right == null)
throw new ArgumentNullException("right");
return LightMoney.Satoshis(checked(left * right._MilliSatoshis));
}
public static LightMoney operator /(LightMoney left, long right)
{
if (left == null)
throw new ArgumentNullException("left");
return new LightMoney(checked(left._MilliSatoshis / right));
}
public static bool operator <(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis < right._MilliSatoshis;
}
public static bool operator >(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis > right._MilliSatoshis;
}
public static bool operator <=(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis <= right._MilliSatoshis;
}
public static bool operator >=(LightMoney left, LightMoney right)
{
if (left == null)
throw new ArgumentNullException("left");
if (right == null)
throw new ArgumentNullException("right");
return left._MilliSatoshis >= right._MilliSatoshis;
}
public static implicit operator LightMoney(long value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(int value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(uint value)
{
return new LightMoney(value);
}
public static implicit operator LightMoney(ulong value)
{
return new LightMoney(checked((long)value));
}
public static implicit operator long(LightMoney value)
{
return value.MilliSatoshi;
}
public static implicit operator ulong(LightMoney value)
{
return checked((ulong)value.MilliSatoshi);
}
public static implicit operator LightMoney(string value)
{
return LightMoney.Parse(value);
}
public override bool Equals(object obj)
{
LightMoney item = obj as LightMoney;
if (item == null)
return false;
return _MilliSatoshis.Equals(item._MilliSatoshis);
}
public static bool operator ==(LightMoney a, LightMoney b)
{
if (Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a._MilliSatoshis == b._MilliSatoshis;
}
public static bool operator !=(LightMoney a, LightMoney b)
{
return !(a == b);
}
public override int GetHashCode()
{
return _MilliSatoshis.GetHashCode();
}
/// <summary>
/// Returns a culture invariant string representation of Bitcoin amount
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(false, false);
}
/// <summary>
/// Returns a culture invariant string representation of Bitcoin amount
/// </summary>
/// <param name="fplus">True if show + for a positive amount</param>
/// <param name="trimExcessZero">True if trim excess zeroes</param>
/// <returns></returns>
public string ToString(bool fplus, bool trimExcessZero = true)
{
var fmt = string.Format("{{0:{0}{1}B}}",
(fplus ? "+" : null),
(trimExcessZero ? "2" : "11"));
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
}
static LightMoney _Zero = new LightMoney(0);
public static LightMoney Zero
{
get
{
return _Zero;
}
}
internal class BitcoinFormatter : IFormatProvider, ICustomFormatter
{
public static readonly BitcoinFormatter Formatter = new BitcoinFormatter();
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (!this.Equals(formatProvider))
{
return null;
}
var i = 0;
var plus = format[i] == '+';
if (plus)
i++;
int decPos = 0;
if (int.TryParse(format.Substring(i, 1), out decPos))
{
i++;
}
var unit = format[i];
var unitToUseInCalc = LightMoneyUnit.BTC;
switch (unit)
{
case 'B':
unitToUseInCalc = LightMoneyUnit.BTC;
break;
}
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
var zeros = new string('0', decPos);
var rest = new string('#', 11 - decPos);
var fmt = plus && val > 0 ? "+" : string.Empty;
fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}";
return string.Format(CultureInfo.InvariantCulture, fmt, val);
}
}
/// <summary>
/// Tell if amount is almost equal to this instance
/// </summary>
/// <param name="amount"></param>
/// <param name="dust">more or less amount</param>
/// <returns>true if equals, else false</returns>
public bool Almost(LightMoney amount, LightMoney dust)
{
if (amount == null)
throw new ArgumentNullException("amount");
if (dust == null)
throw new ArgumentNullException("dust");
return (amount - this).Abs() <= dust;
}
/// <summary>
/// Tell if amount is almost equal to this instance
/// </summary>
/// <param name="amount"></param>
/// <param name="margin">error margin (between 0 and 1)</param>
/// <returns>true if equals, else false</returns>
public bool Almost(LightMoney amount, decimal margin)
{
if (amount == null)
throw new ArgumentNullException("amount");
if (margin < 0.0m || margin > 1.0m)
throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1");
var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin);
return Almost(amount, dust);
}
public static LightMoney Min(LightMoney a, LightMoney b)
{
if (a == null)
throw new ArgumentNullException("a");
if (b == null)
throw new ArgumentNullException("b");
if (a <= b)
return a;
return b;
}
public static LightMoney Max(LightMoney a, LightMoney b)
{
if (a == null)
throw new ArgumentNullException("a");
if (b == null)
throw new ArgumentNullException("b");
if (a >= b)
return a;
return b;
}
private static void CheckLongMinValue(long value)
{
if (value == long.MinValue)
throw new OverflowException("satoshis amount should be greater than long.MinValue");
}
private static void CheckMoneyUnit(LightMoneyUnit value, string paramName)
{
var typeOfMoneyUnit = typeof(LightMoneyUnit);
if (!Enum.IsDefined(typeOfMoneyUnit, value))
{
throw new ArgumentException("Invalid value for MoneyUnit", paramName);
}
}
#region IComparable Members
int IComparable.CompareTo(object obj)
{
return this.CompareTo(obj);
}
#endregion
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Eclair
{
public class NodeInfo
{
public NodeInfo(string nodeId, string host, int port)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (nodeId == null)
throw new ArgumentNullException(nameof(nodeId));
Port = port;
Host = host;
NodeId = nodeId;
}
public string NodeId { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
}
}

@ -0,0 +1,152 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using System.Threading;
namespace BTCPayServer
{
public interface IEventAggregatorSubscription : IDisposable
{
void Unsubscribe();
void Resubscribe();
}
public class EventAggregator : IDisposable
{
class Subscription : IEventAggregatorSubscription
{
private EventAggregator aggregator;
Type t;
public Subscription(EventAggregator aggregator, Type t)
{
this.aggregator = aggregator;
this.t = t;
}
public Action<Object> Act { get; set; }
bool _Disposed;
public void Dispose()
{
if (_Disposed)
return;
_Disposed = true;
lock (this.aggregator._Subscriptions)
{
if (this.aggregator._Subscriptions.TryGetValue(t, out Dictionary<Subscription, Action<object>> actions))
{
if (actions.Remove(this))
{
if (actions.Count == 0)
this.aggregator._Subscriptions.Remove(t);
}
}
}
}
public void Resubscribe()
{
aggregator.Subscribe(t, this);
}
public void Unsubscribe()
{
Dispose();
}
}
public Task<T> WaitNext<T>(CancellationToken cancellation = default(CancellationToken))
{
return WaitNext<T>(o => true, cancellation);
}
public async Task<T> WaitNext<T>(Func<T, bool> predicate, CancellationToken cancellation = default(CancellationToken))
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
var subscription = Subscribe<T>((a, b) => { if (predicate(b)) { tcs.TrySetResult(b); a.Unsubscribe(); } });
using (cancellation.Register(() => { tcs.TrySetCanceled(); subscription.Unsubscribe(); }))
{
return await tcs.Task.ConfigureAwait(false);
}
}
public void Publish<T>(T evt) where T : class
{
Publish(evt, typeof(T));
}
public void Publish(object evt, Type evtType)
{
if (evt == null)
throw new ArgumentNullException(nameof(evt));
List<Action<object>> actionList = new List<Action<object>>();
lock (_Subscriptions)
{
if (_Subscriptions.TryGetValue(evtType, out Dictionary<Subscription, Action<object>> actions))
{
actionList = actions.Values.ToList();
}
}
Logs.Events.LogInformation($"New event: {evt.ToString()}");
foreach (var sub in actionList)
{
try
{
sub(evt);
}
catch (Exception ex)
{
Logs.Events.LogError(ex, $"Error while calling event handler");
}
}
}
public IEventAggregatorSubscription Subscribe<T>(Action<IEventAggregatorSubscription, T> subscription)
{
var eventType = typeof(T);
var s = new Subscription(this, eventType);
s.Act = (o) => subscription(s, (T)o);
return Subscribe(eventType, s);
}
private IEventAggregatorSubscription Subscribe(Type eventType, Subscription subscription)
{
lock (_Subscriptions)
{
if (!_Subscriptions.TryGetValue(eventType, out Dictionary<Subscription, Action<object>> actions))
{
actions = new Dictionary<Subscription, Action<object>>();
_Subscriptions.Add(eventType, actions);
}
actions.Add(subscription, subscription.Act);
}
return subscription;
}
Dictionary<Type, Dictionary<Subscription, Action<object>>> _Subscriptions = new Dictionary<Type, Dictionary<Subscription, Action<object>>>();
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<T, TReturn> subscription)
{
return Subscribe(new Action<T>((t) => subscription(t)));
}
public IEventAggregatorSubscription Subscribe<T, TReturn>(Func<IEventAggregatorSubscription, T, TReturn> subscription)
{
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(sub, t)));
}
public IEventAggregatorSubscription Subscribe<T>(Action<T> subscription)
{
return Subscribe(new Action<IEventAggregatorSubscription, T>((sub, t) => subscription(t)));
}
public void Dispose()
{
lock (_Subscriptions)
{
_Subscriptions.Clear();
}
}
}
}

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceCreatedEvent
{
public InvoiceCreatedEvent(string id)
{
InvoiceId = id;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} created";
}
}
}

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceDataChangedEvent
{
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} data changed";
}
}
}

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoicePaymentEvent
{
public InvoicePaymentEvent(string invoiceId)
{
InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} received a payment";
}
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceStatusChangedEvent
{
public InvoiceStatusChangedEvent()
{
}
public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState)
{
OldState = invoice.Status;
InvoiceId = invoice.Id;
NewState = newState;
}
public string InvoiceId { get; set; }
public string OldState { get; set; }
public string NewState { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}";
}
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Events
{
public class NBXplorerStateChangedEvent
{
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
{
Network = network;
NewState = newState;
OldState = old;
}
public BTCPayNetwork Network { get; set; }
public NBXplorerState NewState { get; set; }
public NBXplorerState OldState { get; set; }
public override string ToString()
{
return $"NBXplorer {Network.CryptoCode}: {OldState} => {NewState}";
}
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class NewBlockEvent
{
public override string ToString()
{
return "New block";
}
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Events
{
public class TxOutReceivedEvent
{
public BTCPayNetwork Network { get; set; }
public Script ScriptPubKey { get; set; }
public override string ToString()
{
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
return $"{address} received a transaction";
}
}
}

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using NBXplorer;
namespace BTCPayServer
{
public class ExplorerClientProvider
{
BTCPayNetworkProvider _NetworkProviders;
BTCPayServerOptions _Options;
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
{
_NetworkProviders = networkProviders;
_Options = options;
}
public ExplorerClient GetExplorerClient(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
if (_Options.ExplorerFactories.TryGetValue(network.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
{
return factory(network);
}
return null;
}
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
{
return GetExplorerClient(network.CryptoCode);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
if (_Options.ExplorerFactories.ContainsKey(network.CryptoCode))
return network;
return null;
}
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
{
foreach(var net in _NetworkProviders.GetAll())
{
if(_Options.ExplorerFactories.TryGetValue(net.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
{
yield return (net, factory(net));
}
}
}
}
}

@ -17,16 +17,31 @@ using NBXplorer;
using NBXplorer.Models;
using System.Linq;
using System.Threading;
using BTCPayServer.Services.Wallets;
using System.IO;
namespace BTCPayServer
{
public static class Extensions
{
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this ExplorerClient client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
public static string GetDefaultExplorerUrl(this NBXplorer.Configuration.NetworkInformation networkInfo)
{
return $"http://127.0.0.1:{networkInfo.DefaultExplorerPort}/";
}
public static string GetDefaultCookieFile(this NBXplorer.Configuration.NetworkInformation networkInfo)
{
return Path.Combine(networkInfo.DefaultDataDirectory, ".cookie");
}
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, BTCPayNetwork network, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
var transactions = hashes
.Select(async o => await client.GetTransactionAsync(o, cts))
.Select(async o => await client.GetTransactionAsync(network, o, cts))
.ToArray();
await Task.WhenAll(transactions).ConfigureAwait(false);
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
@ -71,10 +86,10 @@ namespace BTCPayServer
return res;
}
public static HtmlString ToSrvModel(this object o)
public static HtmlString ToJSVariableModel(this object o, string variableName)
{
var encodedJson = JavaScriptEncoder.Default.Encode(o.ToJson());
return new HtmlString("var srvModel = JSON.parse('" + encodedJson + "');");
return new HtmlString($"var {variableName} = JSON.parse('" + encodedJson + "');");
}

@ -0,0 +1,197 @@
using Hangfire;
using Hangfire.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Net.Http;
using System.Text;
using System.Threading;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.HostedServices
{
public class InvoiceNotificationManager : IHostedService
{
public static HttpClient _Client = new HttpClient();
public class ScheduledJob
{
public int TryCount
{
get; set;
}
public InvoiceEntity Invoice
{
get; set;
}
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
}
async Task Notify(InvoiceEntity invoice)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
await SendNotification(invoice, cts.Token);
return;
}
catch // It fails, it is OK because we try with hangfire after
{
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
}
catch (Exception ex)
{
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
if(btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);
return response;
}
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.NewState == "expired" ||
e.NewState == "paid" ||
e.NewState == "invalid" ||
e.NewState == "complete"
)
await Notify(invoice);
}
if(e.NewState == "confirmed")
{
await Notify(invoice);
}
}));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
return Task.CompletedTask;
}
}
}

@ -0,0 +1,436 @@
using NBXplorer;
using Microsoft.Extensions.Logging;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
using Microsoft.AspNetCore.Hosting;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.HostedServices
{
public class InvoiceWatcher : IHostedService
{
class UpdateInvoiceContext
{
public UpdateInvoiceContext()
{
}
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
public InvoiceEntity Invoice { get; set; }
public List<object> Events { get; set; } = new List<object>();
bool _Dirty = false;
public void MarkDirty()
{
_Dirty = true;
}
public bool Dirty => _Dirty;
}
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayWallet _Wallet;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(
IHostingEnvironment env,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWallet wallet)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
async Task NotifyReceived(Script scriptPubKey, BTCPayNetwork network)
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
if (invoice != null)
_WatchRequests.Add(invoice);
}
async Task NotifyBlock()
{
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(invoice);
}
}
private async Task UpdateInvoice(string invoiceId, CancellationToken cancellation)
{
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
while (!cancellation.IsCancellationRequested)
{
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
if (invoice == null)
break;
var stateBefore = invoice.Status;
var updateContext = new UpdateInvoiceContext()
{
Invoice = invoice,
KnownStates = changes
};
await UpdateInvoice(updateContext).ConfigureAwait(false);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
foreach (var modifiedKnownState in updateContext.ModifiedKnownStates)
{
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break;
}
if (!changed || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, cancellation).ConfigureAwait(false);
}
}
}
private async Task UpdateInvoice(UpdateInvoiceContext context)
{
var invoice = context.Invoice;
//Fetch unknown payments
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var getCoinsResponsesAsync = strategies
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
.ToArray();
await Task.WhenAll(getCoinsResponsesAsync);
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
foreach (var response in getCoinsResponses)
{
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
}
var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
bool dirtyAddress = false;
if (coins != null)
{
if (coins.State != null)
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
invoice.Payments.Add(payment);
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
dirtyAddress = true;
}
}
//////
var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key);
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
invoice.Status = "expired";
}
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
invoice.Status = "paid";
invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired")
{
invoice.ExceptionStatus = "paidLate";
context.MarkDirty();
}
}
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
if (dirtyAddress)
{
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
if (invoice.Status == "paid")
{
var transactions = await GetPaymentsWithTransaction(network, invoice);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(totalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (totalConfirmed >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(network, invoice);
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
invoice.Status = "complete";
context.MarkDirty();
}
}
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
{
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
foreach (var payment in invoice.Payments)
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
{
result.Remove(payment.Outpoint);
continue;
}
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
payments.Add(accountedPayment);
foreach (var txin in tx.Transaction.Inputs)
{
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
{
//We get a double spend
var existing = spentTxIn[txin.PrevOut];
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
{
spentTxIn[txin.PrevOut] = accountedPayment;
result.Remove(existing.Payment.Outpoint);
}
}
}
}
List<PaymentEntity> updated = new List<PaymentEntity>();
var accountedPayments = payments.Where(p =>
{
var accounted = result.Contains(p.Payment.Outpoint);
if (p.Payment.Accounted != accounted)
{
p.Payment.Accounted = accounted;
updated.Add(p.Payment);
}
return accounted;
}).ToArray();
await _InvoiceRepository.UpdatePayments(payments);
return accountedPayments;
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
}
}
private void Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Add(invoiceId);
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Task _Poller;
Task _Loop;
CancellationTokenSource _Cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Poller = StartPoller(_Cts.Token);
_Loop = StartLoop(_Cts.Token);
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
return Task.CompletedTask;
}
private async Task StartPoller(CancellationToken cancellation)
{
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(pending);
}
await Task.Delay(PollInterval, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in InvoiceWatcher poller");
await Task.Delay(PollInterval, cancellation);
}
}
}
catch when (cancellation.IsCancellationRequested) { }
}
async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation("Start watching invoices");
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
ConcurrentDictionary<string, Task> executing = new ConcurrentDictionary<string, Task>();
try
{
foreach (var item in _WatchRequests.GetConsumingEnumerable(cancellation))
{
var task = executing.GetOrAdd(item, async i =>
{
try
{
await UpdateInvoice(i, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
await Task.Delay(2000, cancellation);
}
finally { executing.TryRemove(item, out Task useless); }
});
}
}
catch when (cancellation.IsCancellationRequested)
{
}
finally
{
await Task.WhenAll(executing.Values);
}
Logs.PayServer.LogInformation("Stop watching invoices");
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAll(_Poller, _Loop);
}
}
}

@ -0,0 +1,204 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
namespace BTCPayServer.HostedServices
{
public class NBXplorerListener : IHostedService
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
NBXplorerDashboard _Dashboards;
public NBXplorerListener(ExplorerClientProvider explorerClients,
NBXplorerDashboard dashboard,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Dashboards = dashboard;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
await Listen(nbxplorerEvent.Network);
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var nbxplorerState in _Dashboards.GetAll())
{
if (nbxplorerState.Status != null && nbxplorerState.Status.IsFullySynched)
{
await Listen(nbxplorerState.Network);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceCreatedEvent>(async inv =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
{
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
}
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}));
return Task.CompletedTask;
}
private async Task Listen(BTCPayNetwork network)
{
bool cleanup = false;
try
{
if (_Sessions.ContainsKey(network.CryptoCode))
return;
var client = _ExplorerClients.GetExplorerClient(network);
if (client == null)
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_Sessions.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
_Aggregator.Publish(new Events.NewBlockEvent());
break;
case NBXplorer.Models.NewTransactionEvent evt:
foreach (var txout in evt.Match.Outputs)
{
_Aggregator.Publish(new Events.TxOutReceivedEvent()
{
Network = network,
ScriptPubKey = txout.ScriptPubKey
});
}
break;
default:
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested) { }
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
}
finally
{
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);
}
}
}
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var strategy = GetStrategy(network.CryptoCode, invoice);
if (strategy != null)
strategies.Add(strategy);
}
return strategies;
}
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
{
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
{
if (derivationStrategy.Network.CryptoCode == cryptoCode)
{
return derivationStrategy.DerivationStrategyBase;
}
}
return null;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

@ -0,0 +1,202 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using NBXplorer.Models;
using System.Collections.Concurrent;
using BTCPayServer.Events;
namespace BTCPayServer.HostedServices
{
public enum NBXplorerState
{
NotConnected,
Synching,
Ready
}
public class NBXplorerDashboard
{
public class NBXplorerSummary
{
public BTCPayNetwork Network { get; set; }
public NBXplorerState State { get; set; }
public StatusResult Status { get; set; }
}
ConcurrentDictionary<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status)
{
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status };
_Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary);
}
public bool IsFullySynched()
{
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;
}
}
public class NBXplorerWaiters : IHostedService
{
List<NBXplorerWaiter> _Waiters = new List<NBXplorerWaiter>();
public NBXplorerWaiters(NBXplorerDashboard dashboard, ExplorerClientProvider explorerClientProvider, EventAggregator eventAggregator)
{
foreach (var explorer in explorerClientProvider.GetAll())
{
_Waiters.Add(new NBXplorerWaiter(dashboard, explorer.Item1, explorer.Item2, eventAggregator));
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.WhenAll(_Waiters.Select(w => w.StartAsync(cancellationToken)).ToArray());
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.WhenAll(_Waiters.Select(w => w.StopAsync(cancellationToken)).ToArray());
}
}
public class NBXplorerWaiter : IHostedService
{
public NBXplorerWaiter(NBXplorerDashboard dashboard, BTCPayNetwork network, ExplorerClient client, EventAggregator aggregator)
{
_Network = network;
_Client = client;
_Aggregator = aggregator;
_Dashboard = dashboard;
}
NBXplorerDashboard _Dashboard;
BTCPayNetwork _Network;
EventAggregator _Aggregator;
ExplorerClient _Client;
CancellationTokenSource _Cts;
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Loop = StartLoop(_Cts.Token);
return Task.CompletedTask;
}
private async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
try
{
while (!cancellation.IsCancellationRequested)
{
try
{
while (await StepAsync(cancellation))
{
}
await Task.Delay(PollInterval, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
}
}
}
catch when (cancellation.IsCancellationRequested) { }
}
private async Task<bool> StepAsync(CancellationToken cancellation)
{
var oldState = State;
StatusResult status = null;
try
{
switch (State)
{
case NBXplorerState.NotConnected:
status = await _Client.GetStatusAsync(cancellation);
if (status != null)
{
if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
else
{
State = NBXplorerState.Synching;
}
}
break;
case NBXplorerState.Synching:
status = await _Client.GetStatusAsync(cancellation);
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
break;
case NBXplorerState.Ready:
status = await _Client.GetStatusAsync(cancellation);
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (!status.IsFullySynched)
{
State = NBXplorerState.Synching;
}
break;
}
}
catch when (cancellation.IsCancellationRequested)
{
}
catch (Exception ex)
{
State = NBXplorerState.NotConnected;
Logs.PayServer.LogError(ex, $"Error while trying to connect to NBXplorer ({_Network.CryptoCode})");
}
if (oldState != State)
{
if (State == NBXplorerState.Synching)
{
PollInterval = TimeSpan.FromSeconds(10);
}
else
{
PollInterval = TimeSpan.FromMinutes(1);
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
_Dashboard.Publish(_Network, State, status);
return oldState != State;
}
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
public NBXplorerState State { get; private set; }
public Task StopAsync(CancellationToken cancellationToken)
{
_Cts.Cancel();
return _Loop;
}
}
}

@ -1,4 +1,5 @@
using BTCPayServer.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
@ -34,6 +35,8 @@ using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Hosting
{
@ -83,19 +86,6 @@ namespace BTCPayServer.Hosting
}
}
}
class BTCPayServerConfigureOptions : IConfigureOptions<MvcOptions>
{
BTCPayServerOptions _Options;
public BTCPayServerConfigureOptions(BTCPayServerOptions options)
{
_Options = options;
}
public void Configure(MvcOptions options)
{
if (_Options.RequireHttps)
options.Filters.Add(new RequireHttpsAttribute());
}
}
public static IServiceCollection AddBTCPayServer(this IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>((provider, o) =>
@ -106,32 +96,58 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
services.TryAddSingleton<IConfigureOptions<MvcOptions>, BTCPayServerConfigureOptions>();
services.TryAddSingleton(o =>
services.TryAddSingleton<InvoiceRepository>(o =>
{
var runtime = new BTCPayServerRuntime();
runtime.Configure(o.GetRequiredService<BTCPayServerOptions>());
return runtime;
var opts = o.GetRequiredService<BTCPayServerOptions>();
var dbContext = o.GetRequiredService<ApplicationDbContextFactory>();
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return new InvoiceRepository(dbContext, dbpath, opts.Network);
});
services.AddSingleton<BTCPayServerEnvironment>();
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<EventAggregator>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
ApplicationDbContextFactory dbContext = null;
if (opts.PostgresConnectionString == null)
{
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
}
else
{
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
}
return dbContext;
});
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
return new BTCPayNetworkProvider(opts.Network);
});
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>()
});
services.TryAddSingleton<ExplorerClient>(o =>
{
var runtime = o.GetRequiredService<BTCPayServerRuntime>();
return runtime.Explorer;
BlockTarget = 20
});
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
{
if (o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main)
@ -139,17 +155,11 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<IRateProvider>(o =>
{
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
services.TryAddSingleton<IRateProviderFactory, CachedDefaultRateProviderFactory>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();
services.AddTransient<CallbackController>();
services.AddTransient<InvoiceController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
@ -172,12 +182,6 @@ namespace BTCPayServer.Hosting
public static IApplicationBuilder UsePayServer(this IApplicationBuilder app)
{
if (app.ApplicationServices.GetRequiredService<BTCPayServerOptions>().RequireHttps)
{
var options = new RewriteOptions().AddRedirectToHttps();
app.UseRewriter(options);
}
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
//Wait the DB is ready
@ -186,6 +190,7 @@ namespace BTCPayServer.Hosting
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
});
}
app.UseMiddleware<BTCPayMiddleware>();
return app;
}
@ -200,11 +205,9 @@ namespace BTCPayServer.Hosting
act();
return;
}
catch
catch when(!cts.IsCancellationRequested)
{
if (cts.IsCancellationRequested)
throw;
Thread.Sleep(1000);
Thread.Sleep(100);
}
}
}

@ -29,40 +29,21 @@ namespace BTCPayServer.Hosting
{
TokenRepository _TokenRepository;
RequestDelegate _Next;
CallbackController _CallbackController;
BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
BTCPayServerOptions options,
CallbackController callbackController)
BTCPayServerOptions options)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
_CallbackController = callbackController;
_Options = options ?? throw new ArgumentNullException(nameof(options));
}
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;
}
// Make sure that code executing after this point think that the external url has been hit.
if(_Options.ExternalUrl != null)
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if(_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);
@ -116,6 +97,87 @@ namespace BTCPayServer.Hosting
}
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
reverseProxyScheme = scheme;
}
}
ushort? reverseProxyPort = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
reverseProxyPort = pp;
}
}
// Make sure that code executing after this point think that the external url has been hit.
if (_Options.ExternalUrl != null)
{
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}'");
httpContext.Request.Scheme = reverseProxyScheme;
}
else
{
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
}
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
{
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
{
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
}
else
{
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
{
ushort? p = null;
if (reverseProxyScheme != null)
{
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;

@ -79,6 +79,15 @@ namespace BTCPayServer.Hosting
{
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
});
services.Configure<IdentityOptions>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequiredLength = 7;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
@ -144,6 +153,7 @@ namespace BTCPayServer.Hosting
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseWebSockets();
app.UseMvc(routes =>
{
routes.MapRoute(

@ -17,6 +17,7 @@ namespace BTCPayServer.Logging
{
Configuration = factory.CreateLogger("Configuration");
PayServer = factory.CreateLogger("PayServer");
Events = factory.CreateLogger("Events");
}
public static ILogger Configuration
{
@ -26,6 +27,12 @@ namespace BTCPayServer.Logging
{
get; set;
}
public static ILogger Events
{
get; set;
}
public const int ColumnLength = 16;
}

@ -8,7 +8,7 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (SupportDropColumn(migrationBuilder.ActiveProvider))
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
{
migrationBuilder.DropColumn(
name: "Name",
@ -30,11 +30,6 @@ namespace BTCPayServer.Migrations
});
}
private bool SupportDropColumn(string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(

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

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class AltcoinSupport : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CryptoCode",
table: "HistoricalAddressInvoices",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CryptoCode",
table: "HistoricalAddressInvoices");
}
}
}

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

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class DerivationStrategies : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DerivationStrategies",
table: "Stores",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DerivationStrategies",
table: "Stores");
}
}
}

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

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class defaultcrypto : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DefaultCrypto",
table: "Stores",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DefaultCrypto",
table: "Stores");
}
}
}

@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.0-rtm-26452");
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
@ -44,6 +44,8 @@ namespace BTCPayServer.Migrations
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
@ -188,6 +190,10 @@ namespace BTCPayServer.Migrations
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
@ -382,7 +388,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany()
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
@ -36,11 +37,87 @@ namespace BTCPayServer.Models
}
}
public class InvoiceCryptoInfo
{
[JsonProperty("cryptoCode")]
public string CryptoCode { get; set; }
[JsonProperty("rate")]
public decimal Rate { get; set; }
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
public Dictionary<string, double> ExRates
{
get; set;
}
//"btcPaid":"0.000000"
[JsonProperty("paid")]
public string Paid
{
get; set;
}
//"btcPrice":"0.001157"
[JsonProperty("price")]
public string Price
{
get; set;
}
//"btcDue":"0.001160"
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
[JsonProperty("due")]
public string Due
{
get; set;
}
[JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
}
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// Total amount of this invoice
/// </summary>
[JsonProperty("totalDue")]
public string TotalDue { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
[JsonProperty("networkFee")]
public string NetworkFee { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
[JsonProperty("txCount")]
public int TxCount { get; set; }
/// <summary>
/// Total amount of the invoice paid in this crypto
/// </summary>
[JsonProperty("cryptoPaid")]
public Money CryptoPaid { get; set; }
}
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
[JsonProperty("url")]
[Obsolete("Use CryptoInfo.Url instead")]
public string Url
{
get; set;
@ -59,6 +136,7 @@ namespace BTCPayServer.Models
}
//"btcPrice":"0.001157"
[JsonProperty("btcPrice")]
[Obsolete("Use CryptoInfo.Price instead")]
public string BTCPrice
{
get; set;
@ -66,11 +144,15 @@ namespace BTCPayServer.Models
//"btcDue":"0.001160"
[JsonProperty("btcDue")]
[Obsolete("Use CryptoInfo.Due instead")]
public string BTCDue
{
get; set;
}
[JsonProperty("cryptoInfo")]
public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
//"price":5
[JsonProperty("price")]
public double Price
@ -87,6 +169,7 @@ namespace BTCPayServer.Models
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
[Obsolete("Use CryptoInfo.ExRates instead")]
public Dictionary<string, double> ExRates
{
get; set;
@ -156,6 +239,7 @@ namespace BTCPayServer.Models
//"btcPaid":"0.000000"
[JsonProperty("btcPaid")]
[Obsolete("Use CryptoInfo.Paid instead")]
public string BTCPaid
{
get; set;
@ -163,7 +247,8 @@ namespace BTCPayServer.Models
//"rate":4320.02
[JsonProperty("rate")]
public double Rate
[Obsolete("Use CryptoInfo.Rate instead")]
public decimal Rate
{
get; set;
}
@ -178,6 +263,7 @@ namespace BTCPayServer.Models
//"paymentUrls":{"BIP21":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160","BIP72":"bitcoin:muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv?amount=0.001160&r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP72b":"bitcoin:?r=https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8","BIP73":"https://test.bitpay.com/i/9saCHtp1zyPcNoi3rDdBu8"}
[JsonProperty("paymentUrls")]
[Obsolete("Use CryptoInfo.PaymentsUrls instead")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
@ -197,6 +283,7 @@ namespace BTCPayServer.Models
//"bitcoinAddress":"muFQCEbfRJohcds3bkfv1sRFj8uVTfv2wv"
[JsonProperty("bitcoinAddress")]
[Obsolete("Use CryptoInfo.Address instead")]
public string BitcoinAddress
{
get; set;

@ -9,12 +9,22 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CreateInvoiceModel
{
public CreateInvoiceModel()
{
Currency = "USD";
}
[Required]
public double? Amount
{
get; set;
}
[Required]
public string Currency
{
get; set;
}
[Required]
public string StoreId
{

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using NBitcoin;
@ -9,8 +10,18 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class InvoiceDetailsModel
{
public class CryptoPayment
{
public string CryptoCode { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
}
public class Payment
{
public string CryptoCode { get; set; }
public int Confirmations
{
get; set;
@ -48,10 +59,12 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
public List<Payment> Payments
public List<CryptoPayment> CryptoPayments
{
get; set;
} = new List<Payment>();
} = new List<CryptoPayment>();
public List<Payment> Payments { get; set; } = new List<Payment>();
public string Status
{
@ -92,11 +105,6 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public double Rate
{
get;
internal set;
}
public string NotificationUrl
{
get;
@ -107,40 +115,12 @@ namespace BTCPayServer.Models.InvoicingModels
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;
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
}
}

@ -7,6 +7,14 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class PaymentModel
{
public class AvailableCrypto
{
public string CryptoCode { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
}
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
@ -22,14 +30,14 @@ namespace BTCPayServer.Models.InvoicingModels
public string ItemDesc { get; set; }
public string TimeLeft { get; set; }
public string Rate { get; set; }
public string BtcAmount { get; set; }
public string TxFees { get; set; }
public string OrderAmount { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string BtcTotalDue { get; set; }
public int TxCount { get; set; }
public string BtcPaid { get; set; }
public string StoreEmail { get; set; }
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
}
}

@ -9,6 +9,7 @@ namespace BTCPayServer.Models.ServerViewModels
{
public class UserViewModel
{
public string Id { get; set; }
public string Name
{
get; set;

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class DerivationSchemeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public DerivationSchemeViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string DerivationScheme
{
get; set;
}
public List<(string KeyPath, string Address)> AddressSamples
{
get; set;
} = new List<(string KeyPath, string Address)>();
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

@ -1,5 +1,6 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -10,6 +11,22 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoreViewModel
{
public class DerivationScheme
{
public string Crypto { get; set; }
public string Value { get; set; }
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
}
public string Id { get; set; }
[Display(Name = "Store Name")]
[Required]
[MaxLength(50)]
@ -28,10 +45,14 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[DerivationStrategyValidator]
public string DerivationScheme
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{
get; set;
get;
set;
}
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
@ -46,14 +67,21 @@ 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;
}
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
}
}

@ -32,7 +32,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
public Money Balance
public string[] Balances
{
get; set;
}

@ -15,7 +15,8 @@
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_EXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32839/",
"BTCPAY_NETWORK": "regtest",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
@ -23,4 +24,4 @@
"applicationUrl": "http://localhost:14142/"
}
}
}
}

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class SearchString
{
string _OriginalString;
public SearchString(string str)
{
str = str ?? string.Empty;
str = str.Trim();
_OriginalString = str.Trim();
TextSearch = _OriginalString;
var splitted = str.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
Filters
= splitted
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
.Where(kv => kv.Length == 2)
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
.ToDictionary(o => o.Key, o => o.Value);
foreach(var filter in splitted)
{
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
{
TextSearch = TextSearch.Replace(filter, string.Empty);
}
}
TextSearch = TextSearch.Trim();
}
public string TextSearch
{
get;
private set;
}
public Dictionary<string, string> Filters { get; private set; }
public override string ToString()
{
return _OriginalString;
}
}
}

@ -36,7 +36,7 @@ namespace BTCPayServer.Services
{
StringBuilder txt = new StringBuilder();
txt.Append($"@Copyright BTCPayServer v{Version}");
if (!Environment.IsProduction() || Build.Equals("Release", StringComparison.OrdinalIgnoreCase))
if (!Environment.IsProduction() || !Build.Equals("Release", StringComparison.OrdinalIgnoreCase))
{
txt.Append($" Environment: {Environment.EnvironmentName} Build: {Build}");
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IFeeProviderFactory
{
IFeeProvider CreateFeeProvider(BTCPayNetwork network);
}
}

@ -8,29 +8,44 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Fees
{
public class NBXplorerFeeProviderFactory : IFeeProviderFactory
{
public NBXplorerFeeProviderFactory(ExplorerClientProvider explorerClients)
{
if (explorerClients == null)
throw new ArgumentNullException(nameof(explorerClients));
_ExplorerClients = explorerClients;
}
private readonly ExplorerClientProvider _ExplorerClients;
public FeeRate Fallback { get; set; }
public int BlockTarget { get; set; }
public IFeeProvider CreateFeeProvider(BTCPayNetwork network)
{
return new NBXplorerFeeProvider(this, _ExplorerClients.GetExplorerClient(network));
}
}
public class NBXplorerFeeProvider : IFeeProvider
{
public ExplorerClient ExplorerClient
public NBXplorerFeeProvider(NBXplorerFeeProviderFactory parent, ExplorerClient explorerClient)
{
get; set;
}
public FeeRate Fallback
{
get; set;
}
public int BlockTarget
{
get; set;
if (explorerClient == null)
throw new ArgumentNullException(nameof(explorerClient));
_Factory = parent;
_ExplorerClient = explorerClient;
}
NBXplorerFeeProviderFactory _Factory;
ExplorerClient _ExplorerClient;
public async Task<FeeRate> GetFeeRateAsync()
{
try
{
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
}
catch (NBXplorerException ex) when (ex.Error.HttpCode == 400 && ex.Error.Code == "fee-estimation-unavailable")
{
return Fallback;
return _Factory.Fallback;
}
}
}

@ -10,6 +10,7 @@ using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
using NBXplorer;
namespace BTCPayServer.Services.Invoices
{
@ -82,7 +83,7 @@ namespace BTCPayServer.Services.Invoices
}
[JsonProperty(PropertyName = "price")]
public double Price
public decimal Price
{
get; set;
}
@ -111,65 +112,17 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
public int GetTxCount()
{
return Calculate().TxCount;
}
public string OrderId
{
get; set;
}
public Money GetTotalCryptoDue()
{
return Calculate().TotalDue;
}
private (Money TotalDue, Money Paid, int TxCount) Calculate()
{
var totalDue = Money.Coins((decimal)(ProductInformation.Price / Rate)) + TxFee;
var paid = Money.Zero;
int txCount = 1;
var payments =
Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
paid += _.Output.Value;
return _;
})
.TakeWhile(_ =>
{
var paidEnough = totalDue <= paid;
if (!paidEnough)
{
txCount++;
totalDue += TxFee;
}
return !paidEnough;
})
.ToArray();
return (totalDue, paid, txCount);
}
public Money GetTotalPaid()
{
return Calculate().Paid;
}
public Money GetCryptoDue()
{
var o = Calculate();
var v = o.TotalDue - o.Paid;
return v < Money.Zero ? Money.Zero : v;
}
public SpeedPolicy SpeedPolicy
{
get; set;
}
public double Rate
[Obsolete("Use GetCryptoData(network).Rate instead")]
public decimal Rate
{
get; set;
}
@ -181,7 +134,9 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
public BitcoinAddress DepositAddress
[Obsolete("Use GetCryptoData(network).DepositAddress instead")]
public string DepositAddress
{
get; set;
}
@ -198,11 +153,64 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy
{
get;
set;
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategies
{
get;
set;
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
bool btcReturned = false;
if (!string.IsNullOrEmpty(DerivationStrategies))
{
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var network = networks.GetNetwork(strat.Name);
if (network != null)
{
if (network == networks.BTC && btcReturned)
btcReturned = true;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
}
}
}
if (!btcReturned && !string.IsNullOrEmpty(DerivationStrategy))
{
if (networks.BTC != null)
{
yield return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, networks.BTC);
}
}
#pragma warning restore CS0618
}
internal void SetDerivationStrategies(IEnumerable<DerivationStrategy> derivationStrategies)
{
JObject obj = new JObject();
foreach (var strat in derivationStrategies)
{
obj.Add(strat.Network.CryptoCode, new JValue(strat.DerivationStrategyBase.ToString()));
#pragma warning disable CS0618
if (strat.Network.IsBTC)
DerivationStrategy = strat.DerivationStrategyBase.ToString();
}
DerivationStrategies = JsonConvert.SerializeObject(obj);
#pragma warning restore CS0618
}
public string Status
{
get;
@ -231,6 +239,8 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use GetCryptoData(network).TxFee instead")]
public Money TxFee
{
get;
@ -251,7 +261,12 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public DateTimeOffset? MonitoringExpiration
[Obsolete("Use Set/GetCryptoData() instead")]
public JObject CryptoData { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{
get;
set;
@ -267,6 +282,7 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public bool ExtendedNotifications { get; set; }
public bool IsExpired()
{
@ -274,7 +290,7 @@ namespace BTCPayServer.Services.Invoices
}
public InvoiceResponse EntityToDTO()
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
{
ServerUrl = ServerUrl ?? "";
InvoiceResponse dto = new InvoiceResponse
@ -285,33 +301,68 @@ namespace BTCPayServer.Services.Invoices
CurrentTime = DateTimeOffset.UtcNow,
InvoiceTime = InvoiceTime,
ExpirationTime = ExpirationTime,
BTCPrice = Money.Coins((decimal)(1.0 / Rate)).ToString(),
Status = Status,
Url = ServerUrl.WithTrailingSlash() + "invoice?id=" + Id,
Currency = ProductInformation.Currency,
Flags = new Flags() { Refundable = Refundable }
};
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
foreach (var info in this.GetCryptoData().Values)
{
var accounting = info.Calculate();
var cryptoInfo = new InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.CryptoCode;
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
cryptoInfo.Due = accounting.Due.ToString();
cryptoInfo.Paid = accounting.Paid.ToString();
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
cryptoInfo.TxCount = accounting.TxCount;
cryptoInfo.CryptoPaid = accounting.CryptoPaid;
cryptoInfo.Address = info.DepositAddress;
cryptoInfo.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
};
var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC";
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
{
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
#pragma warning disable CS0618
if (info.CryptoCode == "BTC")
{
dto.Url = cryptoInfo.Url;
dto.BTCPrice = cryptoInfo.Price;
dto.Rate = cryptoInfo.Rate;
dto.ExRates = cryptoInfo.ExRates;
dto.BitcoinAddress = cryptoInfo.Address;
dto.BTCPaid = cryptoInfo.Paid;
dto.BTCDue = cryptoInfo.Due;
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
dto.CryptoInfo.Add(cryptoInfo);
}
Populate(ProductInformation, dto);
Populate(BuyerInformation, dto);
dto.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, Rate }
};
dto.PaymentUrls = new InvoicePaymentUrls()
{
BIP72 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP72b = $"bitcoin:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}")}",
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}"),
BIP21 = $"bitcoin:{DepositAddress}?amount={GetCryptoDue()}",
};
dto.BitcoinAddress = DepositAddress.ToString();
dto.Token = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16)); //No idea what it is useful for
dto.Guid = Guid.NewGuid().ToString();
var paid = Payments.Where(p => p.Accounted).Select(p => p.Output.Value).Sum();
dto.BTCPaid = paid.ToString();
dto.BTCDue = GetCryptoDue().ToString();
dto.ExceptionStatus = ExceptionStatus == null ? new JValue(false) : new JValue(ExceptionStatus);
return dto;
}
@ -322,11 +373,161 @@ namespace BTCPayServer.Services.Invoices
JsonConvert.PopulateObject(str, dest);
}
public Money GetNetworkFee()
internal bool Support(BTCPayNetwork network)
{
var item = Calculate();
return TxFee * item.TxCount;
var rates = GetCryptoData();
return rates.TryGetValue(network.CryptoCode, out var data);
}
public CryptoData GetCryptoData(string cryptoCode)
{
GetCryptoData().TryGetValue(cryptoCode, out var data);
return data;
}
public CryptoData GetCryptoData(BTCPayNetwork network)
{
GetCryptoData().TryGetValue(network.CryptoCode, out var data);
return data;
}
public Dictionary<string, CryptoData> GetCryptoData()
{
Dictionary<string, CryptoData> rates = new Dictionary<string, CryptoData>();
var serializer = new Serializer(Dummy);
#pragma warning disable CS0618
// Legacy
if (Rate != 0.0m)
{
rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress });
}
if (CryptoData != null)
{
foreach (var prop in CryptoData.Properties())
{
var r = serializer.ToObject<CryptoData>(prop.Value.ToString());
r.CryptoCode = prop.Name;
r.ParentEntity = this;
rates.TryAdd(r.CryptoCode, r);
}
}
#pragma warning restore CS0618
return rates;
}
Network Dummy = Network.Main;
public void SetCryptoData(Dictionary<string, CryptoData> cryptoData)
{
var obj = new JObject();
var serializer = new Serializer(Dummy);
foreach (var kv in cryptoData)
{
var clone = serializer.ToObject<CryptoData>(serializer.ToString(kv.Value));
clone.CryptoCode = null;
obj.Add(new JProperty(kv.Key, JObject.Parse(serializer.ToString(clone))));
}
#pragma warning disable CS0618
CryptoData = obj;
#pragma warning restore CS0618
}
}
public class CryptoDataAccounting
{
/// <summary>
/// Total amount of this invoice
/// </summary>
public Money TotalDue { get; set; }
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
public Money Due { get; set; }
/// <summary>
/// Total amount of the invoice paid after conversion to this crypto currency
/// </summary>
public Money Paid { get; set; }
/// <summary>
/// Total amount of the invoice paid in this currency
/// </summary>
public Money CryptoPaid { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
public int TxCount { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
public Money NetworkFee { get; set; }
}
public class CryptoData
{
[JsonIgnore]
public InvoiceEntity ParentEntity { get; set; }
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
public string CryptoCode { get; set; }
[JsonProperty(PropertyName = "rate")]
public decimal Rate { get; set; }
[JsonProperty(PropertyName = "feeRate")]
public FeeRate FeeRate { get; set; }
[JsonProperty(PropertyName = "txFee")]
public Money TxFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
public string DepositAddress { get; set; }
public CryptoDataAccounting Calculate()
{
var cryptoData = ParentEntity.GetCryptoData();
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate);
var paid = Money.Zero;
var cryptoPaid = Money.Zero;
var paidTxFee = Money.Zero;
bool paidEnough = totalDue <= paid;
int txCount = 0;
var payments =
ParentEntity.Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
paid += _.GetValue(cryptoData, CryptoCode);
if (!paidEnough)
{
totalDue += txFee;
paidTxFee += txFee;
}
paidEnough |= totalDue <= paid;
if (CryptoCode == _.GetCryptoCode())
{
cryptoPaid += _.GetValue();
txCount++;
}
return _;
})
.ToArray();
if (!paidEnough)
{
txCount++;
totalDue += TxFee;
paidTxFee += TxFee;
}
var accounting = new CryptoDataAccounting();
accounting.TotalDue = totalDue;
accounting.Paid = paid;
accounting.TxCount = txCount;
accounting.CryptoPaid = cryptoPaid;
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.NetworkFee = paidTxFee;
return accounting;
}
}
public class AccountedPaymentEntity
@ -350,13 +551,60 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use GetValue() or GetScriptPubKey() instead")]
public TxOut Output
{
get; set;
}
public Script GetScriptPubKey()
{
#pragma warning disable CS0618
return Output.ScriptPubKey;
#pragma warning restore CS0618
}
public bool Accounted
{
get; set;
}
[Obsolete("Use GetCryptoCode() instead")]
public string CryptoCode
{
get;
set;
}
public Money GetValue()
{
#pragma warning disable CS0618
return Output.Value;
#pragma warning restore CS0618
}
public Money GetValue(Dictionary<string, CryptoData> cryptoData, string cryptoCode, Money value = null)
{
#pragma warning disable CS0618
value = value ?? Output.Value;
#pragma warning restore CS0618
var to = cryptoCode;
var from = GetCryptoCode();
if (to == from)
return value;
var fromRate = cryptoData[from].Rate;
var toRate = cryptoData[to].Rate;
var fiatValue = fromRate * value.ToDecimal(MoneyUnit.BTC);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return Money.Coins(otherCurrencyValue);
}
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
}

@ -1,126 +0,0 @@
using Hangfire;
using Hangfire.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Net.Http;
using System.Text;
using System.Threading;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Collections.Concurrent;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceNotificationManager
{
public static HttpClient _Client = new HttpClient();
public class ScheduledJob
{
public int TryCount
{
get; set;
}
public InvoiceEntity Invoice
{
get; set;
}
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
ILogger<InvoiceNotificationManager> logger)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
}
public void Notify(InvoiceEntity invoice)
{
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = job.Invoice.EntityToDTO();
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Url = dto.Url,
BTCDue = dto.BTCDue,
BTCPaid = dto.BTCPaid,
BTCPrice = dto.BTCPrice,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Rate = dto.Rate,
Status = dto.Status,
BuyerFields = job.Invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", job.Invoice.RefundMail) }
};
request.RequestUri = new Uri(job.Invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), Encoding.UTF8, "application/json");
var response = await _Client.SendAsync(request, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
}
catch (Exception ex)
{
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
}
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
}
}

@ -45,7 +45,7 @@ namespace BTCPayServer.Services.Invoices
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
@ -70,11 +70,11 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey)
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey, string cryptoCode)
{
using (var db = _ContextFactory.CreateContext())
{
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString());
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString() + "#" + cryptoCode);
return result?.InvoiceDataId;
}
}
@ -102,8 +102,9 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice)
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
invoice.Payments = new List<PaymentEntity>();
@ -121,59 +122,80 @@ namespace BTCPayServer.Services.Invoices
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
});
context.AddressInvoices.Add(new AddressInvoiceData()
foreach (var cryptoData in invoice.GetCryptoData().Values)
{
Address = invoice.DepositAddress.ScriptPubKey.Hash.ToString(),
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
});
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Address = invoice.DepositAddress.ToString(),
Assigned = DateTimeOffset.UtcNow
});
var network = networkProvider.GetNetwork(cryptoData.CryptoCode);
if (network == null)
throw new InvalidOperationException("CryptoCode unsupported");
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
}.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash, network.CryptoCode));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Assigned = DateTimeOffset.UtcNow
}.SetAddress(cryptoData.DepositAddress, cryptoData.CryptoCode));
textSearch.Add(cryptoData.DepositAddress);
textSearch.Add(cryptoData.Calculate().TotalDue.ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
await context.SaveChangesAsync().ConfigureAwait(false);
}
AddToTextSearch(invoice.Id,
invoice.Id,
invoice.DepositAddress.ToString(),
invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture),
invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture),
invoice.GetTotalCryptoDue().ToString(),
invoice.OrderId,
ToString(invoice.BuyerInformation),
ToString(invoice.ProductInformation),
invoice.StoreId
);
textSearch.Add(invoice.Id);
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.OrderId);
textSearch.Add(ToString(invoice.BuyerInformation));
textSearch.Add(ToString(invoice.ProductInformation));
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress)
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network)
{
using (var context = _ContextFactory.CreateContext())
{
var invoice = await context.Invoices.FirstOrDefaultAsync(i => i.Id == invoiceId);
if (invoice == null)
return false;
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob);
var old = invoiceEntity.DepositAddress;
invoiceEntity.DepositAddress = bitcoinAddress;
invoice.Blob = ToBytes(invoiceEntity);
if (old != null)
var cryptoData = invoiceEntity.GetCryptoData();
var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault();
if (currencyData == null)
return false;
if (currencyData.DepositAddress != null)
{
MarkUnassigned(invoiceId, old, context);
MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode);
}
context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow });
currencyData.DepositAddress = bitcoinAddress.ToString();
#pragma warning disable CS0618
if (network.IsBTC)
{
invoiceEntity.DepositAddress = currencyData.DepositAddress;
}
#pragma warning restore CS0618
invoiceEntity.SetCryptoData(cryptoData);
invoice.Blob = ToBytes(invoiceEntity);
context.AddressInvoices.Add(new AddressInvoiceData() {
InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }
.SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoiceId,
Address = bitcoinAddress.ToString(),
Assigned = DateTimeOffset.UtcNow
});
}.SetAddress(bitcoinAddress.ToString(), network.CryptoCode));
await context.SaveChangesAsync();
AddToTextSearch(invoice.Id, bitcoinAddress.ToString());
@ -181,14 +203,19 @@ namespace BTCPayServer.Services.Invoices
}
}
private static void MarkUnassigned(string invoiceId, BitcoinAddress old, ApplicationDbContext context)
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
{
var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId;
historical.Address = old.ToString();
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
foreach (var address in entity.GetCryptoData())
{
if (cryptoCode != null && cryptoCode != address.Value.CryptoCode)
continue;
var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId;
historical.SetAddress(address.Value.DepositAddress, cryptoCode);
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
}
}
public async Task UnaffectAddress(string invoiceId)
@ -199,9 +226,7 @@ namespace BTCPayServer.Services.Invoices
if (invoiceData == null)
return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
if (invoiceEntity.DepositAddress == null)
return;
MarkUnassigned(invoiceId, invoiceEntity.DepositAddress, context);
MarkUnassigned(invoiceId, invoiceEntity, context, null);
try
{
await context.SaveChangesAsync();
@ -223,11 +248,11 @@ namespace BTCPayServer.Services.Invoices
void AddToTextSearch(string invoiceId, params string[] terms)
{
_IndexerThread.DoAsync(() =>
_IndexerThread.DoAsync(() =>
{
using (var tx = _Engine.GetTransaction())
{
tx.TextInsert("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.TextAppend("InvoiceSearch", Encoders.Base58.DecodeData(invoiceId), string.Join(" ", terms.Where(t => !String.IsNullOrWhiteSpace(t))));
tx.Commit();
}
});
@ -284,7 +309,7 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
entity.Payments = invoice.Payments.Select(p =>
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
paymentEntity.Accounted = p.Accounted;
@ -300,13 +325,12 @@ namespace BTCPayServer.Services.Invoices
}
if (invoice.AddressInvoices != null)
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.Address).ToHashSet();
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetHash() + a.GetCryptoCode()).ToHashSet();
}
return entity;
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
@ -315,7 +339,8 @@ namespace BTCPayServer.Services.Invoices
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
@ -394,14 +419,17 @@ namespace BTCPayServer.Services.Invoices
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
}
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin)
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin, string cryptoCode)
{
using (var context = _ContextFactory.CreateContext())
{
PaymentEntity entity = new PaymentEntity
{
Outpoint = receivedCoin.Outpoint,
#pragma warning disable CS0618
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode,
#pragma warning restore CS0618
ReceivedTime = DateTime.UtcNow
};
@ -520,5 +548,6 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public bool IncludeAddresses { get; set; }
}
}

@ -1,393 +0,0 @@
using NBXplorer;
using Microsoft.Extensions.Logging;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceWatcher : IHostedService
{
InvoiceRepository _InvoiceRepository;
ExplorerClient _ExplorerClient;
DerivationStrategyFactory _DerivationFactory;
InvoiceNotificationManager _NotificationManager;
BTCPayWallet _Wallet;
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));
}
public bool LongPollingMode
{
get; set;
}
public async Task NotifyReceived(Script scriptPubKey)
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(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)
{
UTXOChanges changes = null;
while (true)
{
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
if (invoice == null)
break;
var stateBefore = invoice.Status;
var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false);
changes = result.Changes;
if (result.NeedSave)
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
var changed = stateBefore != invoice.Status;
if (changed)
{
Logs.PayServer.LogInformation($"Invoice {invoice.Id}: {stateBefore} => {invoice.Status}");
}
var expirationMonitoring = invoice.MonitoringExpiration.HasValue ? invoice.MonitoringExpiration.Value : invoice.InvoiceTime + TimeSpan.FromMinutes(60);
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && expirationMonitoring < DateTimeOffset.UtcNow))
{
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)
{
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
}
}
}
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
{
bool needSave = false;
//Fetch unknown payments
var strategy = _DerivationFactory.Parse(invoice.DerivationStrategy);
changes = await _ExplorerClient.SyncAsync(strategy, changes, !LongPollingMode, _Cts.Token).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).ToArray();
List<Coin> receivedCoins = new List<Coin>();
foreach (var received in utxos)
if (invoice.AvailableAddressHashes.Contains(received.Output.ScriptPubKey.Hash.ToString()))
receivedCoins.Add(new Coin(received.Outpoint, received.Output));
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
bool dirtyAddress = false;
foreach (var coin in receivedCoins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
invoice.Payments.Add(payment);
dirtyAddress = true;
}
//////
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "expired";
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
}
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.Output.Value).Sum();
if (totalPaid >= invoice.GetTotalCryptoDue())
{
if (invoice.Status == "new")
{
invoice.Status = "paid";
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
}
else if (invoice.Status == "expired")
{
invoice.ExceptionStatus = "paidLate";
needSave = true;
}
}
if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
}
if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress);
invoice.ExceptionStatus = "paidPartial";
needSave = true;
if (dirtyAddress)
{
var address = await _Wallet.ReserveAddressAsync(_DerivationFactory.Parse(invoice.DerivationStrategy));
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address);
}
}
}
if (invoice.Status == "paid")
{
if (!invoice.MonitoringExpiration.HasValue || invoice.MonitoringExpiration > DateTimeOffset.UtcNow)
{
var transactions = await GetPaymentsWithTransaction(invoice);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "confirmed";
_NotificationManager.Notify(invoice);
needSave = true;
}
}
else
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
invoice.Status = "invalid";
needSave = true;
}
}
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(invoice);
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
{
invoice.Status = "complete";
if (invoice.FullNotifications)
_NotificationManager.Notify(invoice);
needSave = true;
}
}
return (needSave, changes);
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(InvoiceEntity invoice)
{
var transactions = await _ExplorerClient.GetTransactions(invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
foreach (var payment in invoice.Payments)
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
{
result.Remove(payment.Outpoint);
continue;
}
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
payments.Add(accountedPayment);
foreach (var txin in tx.Transaction.Inputs)
{
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
{
//We get a double spend
var existing = spentTxIn[txin.PrevOut];
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
{
spentTxIn[txin.PrevOut] = accountedPayment;
result.Remove(existing.Payment.Outpoint);
}
}
}
}
List<PaymentEntity> updated = new List<PaymentEntity>();
var accountedPayments = payments.Where(p =>
{
var accounted = result.Contains(p.Payment.Outpoint);
if (p.Payment.Accounted != accounted)
{
p.Payment.Accounted = accounted;
updated.Add(p.Payment);
}
return accounted;
}).ToArray();
await _InvoiceRepository.UpdatePayments(payments);
return accountedPayments;
}
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
public void Watch(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Add(invoiceId);
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
public void Dispose()
{
_Cts.Cancel();
}
Thread _Thread;
TaskCompletionSource<bool> _RunningTask;
CancellationTokenSource _Cts;
Timer _UpdatePendingInvoices;
public Task StartAsync(CancellationToken cancellationToken)
{
_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");
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
try
{
foreach (var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
{
try
{
_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))
{
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)
{
try
{
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
}
catch (AggregateException) { }
_RunningTask.TrySetResult(true);
}
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));
}
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
@ -11,5 +12,8 @@ namespace BTCPayServer.Services
{
get; set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool LockSubscription { get; set; }
}
}

@ -0,0 +1,27 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedDefaultRateProviderFactory : IRateProviderFactory
{
IMemoryCache _Cache;
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
public CachedDefaultRateProviderFactory(IMemoryCache cache)
{
if (cache == null)
throw new ArgumentNullException(nameof(cache));
_Cache = cache;
}
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
public IRateProvider GetRateProvider(BTCPayNetwork network)
{
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
}
}
}

@ -10,8 +10,9 @@ namespace BTCPayServer.Services.Rates
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
private string _CryptoCode;
public CachedRateProvider(IRateProvider inner, IMemoryCache memoryCache)
public CachedRateProvider(string cryptoCode, IRateProvider inner, IMemoryCache memoryCache)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
@ -19,6 +20,7 @@ namespace BTCPayServer.Services.Rates
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this._MemoryCache = memoryCache;
this._CryptoCode = cryptoCode;
}
public TimeSpan CacheSpan
@ -29,7 +31,7 @@ namespace BTCPayServer.Services.Rates
public Task<decimal> GetRateAsync(string currency)
{
return _MemoryCache.GetOrCreateAsync("CURR_" + currency, (ICacheEntry entry) =>
return _MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);

@ -17,76 +17,35 @@ namespace BTCPayServer.Services.Rates
}
public class CoinAverageRateProvider : IRateProvider
{
public class RatesJson
{
public class RateJson
{
public string Code
{
get; set;
}
public decimal Rate
{
get; set;
}
}
[JsonProperty("rates")]
public JObject RatesInternal
{
get; set;
}
[JsonIgnore]
public List<RateJson> Rates
{
get; set;
}
[JsonIgnore]
public Dictionary<string, decimal> RatesByCurrency
{
get; set;
}
public decimal GetRate(string currency)
{
if (!RatesByCurrency.TryGetValue(currency.ToUpperInvariant(), out decimal currUSD))
throw new RateUnavailableException(currency);
if (!RatesByCurrency.TryGetValue("BTC", out decimal btcUSD))
throw new RateUnavailableException(currency);
return currUSD / btcUSD;
}
public void CalculateDictionary()
{
RatesByCurrency = new Dictionary<string, decimal>();
Rates = new List<RateJson>();
foreach (var rate in RatesInternal.OfType<JProperty>())
{
var rateJson = new RateJson();
rateJson.Code = rate.Name;
rateJson.Rate = decimal.Parse(rate.Value["rate"].Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
RatesByCurrency.Add(rate.Name, rateJson.Rate);
Rates.Add(rateJson);
}
}
}
static HttpClient _Client = new HttpClient();
public CoinAverageRateProvider(string cryptoCode)
{
CryptoCode = cryptoCode ?? "BTC";
}
public string CryptoCode { get; set; }
public string Market
{
get; set;
} = "global";
public async Task<decimal> GetRateAsync(string currency)
{
RatesJson rates = await GetRatesCore();
return rates.GetRate(currency);
var rates = await GetRatesCore();
return GetRate(rates, currency);
}
private async Task<RatesJson> GetRatesCore()
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
var resp = await _Client.GetAsync("https://apiv2.bitcoinaverage.com/constants/exchangerates/" + Market);
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);
}
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
using (resp)
{
@ -97,19 +56,25 @@ namespace BTCPayServer.Services.Rates
if ((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JsonConvert.DeserializeObject<RatesJson>(await resp.Content.ReadAsStringAsync());
rates.CalculateDictionary();
return rates;
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
return rates.Properties()
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => ToDecimal(p.Value["last"]));
}
}
private decimal ToDecimal(JToken token)
{
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
RatesJson rates = await GetRatesCore();
return rates.Rates.Select(o => new Rate()
var rates = await GetRatesCore();
return rates.Select(o => new Rate()
{
Currency = o.Code,
Value = rates.GetRate(o.Code)
Currency = o.Key,
Value = o.Value
}).ToList();
}
}

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public FallbackRateProvider(IRateProvider[] providers)
{
if (providers == null)
throw new ArgumentNullException(nameof(providers));
_Providers = providers;
}
public async Task<decimal> GetRateAsync(string currency)
{
foreach(var p in _Providers)
{
try
{
return await p.GetRateAsync(currency).ConfigureAwait(false);
}
catch { }
}
throw new RateUnavailableException(currency);
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
foreach (var p in _Providers)
{
try
{
return await p.GetRatesAsync().ConfigureAwait(false);
}
catch { }
}
throw new RateUnavailableException("ALL");
}
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface IRateProviderFactory
{
IRateProvider GetRateProvider(BTCPayNetwork network);
}
}

@ -7,16 +7,28 @@ using System.Text;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using System.Threading;
using NBXplorer.Models;
namespace BTCPayServer.Services.Wallets
{
public class KnownState
{
public uint256 UnconfirmedHash { get; set; }
public uint256 ConfirmedHash { get; set; }
}
public class GetCoinsResult
{
public Coin[] Coins { get; set; }
public KnownState State { get; set; }
public DerivationStrategy Strategy { get; set; }
}
public class BTCPayWallet
{
private ExplorerClient _Client;
private Serializer _Serializer;
private ExplorerClientProvider _Client;
ApplicationDbContextFactory _DBFactory;
public BTCPayWallet(ExplorerClient client, ApplicationDbContextFactory factory)
public BTCPayWallet(ExplorerClientProvider client, ApplicationDbContextFactory factory)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
@ -24,37 +36,56 @@ namespace BTCPayServer.Services.Wallets
throw new ArgumentNullException(nameof(factory));
_Client = client;
_DBFactory = factory;
_Serializer = new NBXplorer.Serializer(_Client.Network);
}
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategy derivationStrategy)
{
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network);
var client = _Client.GetExplorerClient(derivationStrategy.Network);
var pathInfo = await client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(client.Network);
}
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
public async Task TrackAsync(DerivationStrategy derivationStrategy)
{
await _Client.TrackAsync(derivationStrategy);
var client = _Client.GetExplorerClient(derivationStrategy.Network);
await client.TrackAsync(derivationStrategy.DerivationStrategyBase);
}
private byte[] ToBytes<T>(T obj)
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
{
return ZipUtils.Zip(_Serializer.ToString(obj));
var client = _Client.GetExplorerClient(network);
return client.GetTransactionAsync(txId, cancellation);
}
public Task BroadcastTransactionsAsync(List<Transaction> transactions)
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
var client = _Client.GetExplorerClient(strategy.Network);
if (client == null)
return new GetCoinsResult() { Coins = new Coin[0], State = null, Strategy = strategy };
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
return new GetCoinsResult()
{
Coins = utxos,
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
Strategy = strategy,
};
}
public Task BroadcastTransactionsAsync(BTCPayNetwork network, List<Transaction> transactions)
{
var client = _Client.GetExplorerClient(network);
var tasks = transactions.Select(t => client.BroadcastAsync(t)).ToArray();
return Task.WhenAll(tasks);
}
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
public async Task<Money> GetBalance(DerivationStrategy 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))
var client = _Client.GetExplorerClient(derivationStrategy.Network);
var result = await client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true);
return result.Confirmed.UTXOs.Select(u => u.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Value))
.Sum();
}
}

@ -1,32 +0,0 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace BTCPayServer.Validations
{
public class DerivationStrategyValidatorAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null)
{
return ValidationResult.Success;
}
var network = (Network)validationContext.GetService(typeof(Network));
if (network == null)
return new ValidationResult("No Network specified");
try
{
new DerivationStrategyFactory(network).Parse((string)value);
return ValidationResult.Success;
}
catch (Exception ex)
{
return new ValidationResult(ex.Message);
}
}
}
}

@ -57,10 +57,10 @@
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/npFMOu6tTpA" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
</div>
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/6rd8ZpLrz-4" frameborder="0" allowfullscreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>
@ -122,10 +122,18 @@
</div>
<div class="row">
<div class="col-lg-4 ml-auto text-center">
<a href="http://13.79.159.103:3000/">
<a href="http://52.191.212.129:3000/">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://13.79.159.103:3000/">On Slack</a></p>
<p><a href="http://52.191.212.129:3000/">On Slack</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">
<img src="~/img/twitter.png" height="100" />
</a>
<p>
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
</p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://github.com/btcpayserver/btcpayserver">

@ -12,21 +12,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>BTCPay Invoice</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
</environment>
<link href="~/vendor/font-awesome/css/font-awesome.css" rel="stylesheet" />
<link href="~/css/css.css" rel="stylesheet" type="text/css">
<link href="~/css/normalizer.css" rel="stylesheet" type="text/css">
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
crossorigin="anonymous"></script>
<script type="text/javascript">
@Model.ToSrvModel()
@Model.ToJSVariableModel("srvModel")
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
@ -102,7 +94,7 @@
</div>
<div class="order-details">
<!---->
<div class="single-item-order">
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
@ -113,39 +105,42 @@
</div>
<!---->
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price clickable" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} BTC</span>
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<!---->
<div class="single-item-order__right__ex-rate">
1 BTC = {{ srvModel.rate }}
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
<!---->
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<!---->
<line-items>
<div class="line-items">
<!---->
<div class="line-items__item">
<div class="line-items__item__label" i18n="">Payment Amount</div>
<div class="line-items__item__value">{{srvModel.btcAmount}} BTC</div>
<div class="line-items__item__label" i18n="">Order Amount</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Network Cost</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.txCount }} transaction x {{ srvModel.txFees}} BTC</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Already Paid</span>
</div>
<div class="line-items__item__value" i18n="">-{{srvModel.btcPaid }} BTC</div>
<div class="line-items__item__value" i18n="">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items__item--total">
<div class="line-items__item__label" i18n="">Due </div>
<div class="line-items__item__value">{{srvModel.btcDue}} BTC</div>
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
</div>
<!---->
</div>
@ -163,7 +158,8 @@
<div adjust-height="" class="payment-box">
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<qrcode :val="srvModel.btcAddress" :size="256" bg-color="#f5f5f7" fg-color="#000" />
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode :val="srvModel.invoiceBitcoinUrl" :size="256" bg-color="#f5f5f7" fg-color="#000" />
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
@ -316,7 +312,7 @@
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="~/imlegacy/bitcoin-symbol.svg"></div>
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
@ -341,14 +337,14 @@
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} BTC to the address below.</span>
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> BTC
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span i18n="">Copied</span>
</div>
@ -365,7 +361,7 @@
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
<img :src="srvModel.cryptoImage" />
</div>
<div class="manual-box__address__wrapper__value">{{srvModel.btcAddress}}</div>
</div>
@ -610,8 +606,17 @@
</div>
</div>
<div class="footer">
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px; display: none;">
<div></div>
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
@if(Model.AvailableCryptos.Count > 1)
{
<div style="text-align:center">Accepted here</div>
<div style="text-align:center">
@foreach(var crypto in Model.AvailableCryptos)
{
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.cryptoCode='@crypto.CryptoCode'; return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.CryptoCode" src="@crypto.CryptoImage" /></a>
}
</div>
}
</div>
</div>
</div>

@ -19,6 +19,11 @@
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OrderId" class="control-label"></label>
<input asp-for="OrderId" class="form-control" />

@ -49,6 +49,10 @@
<th>Expiration date</th>
<td>@Model.ExpirationDate</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
@ -61,42 +65,14 @@
<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 class="overflowbox"><a href="@Model.PaymentUrl">@Model.PaymentUrl</a></td>
</tr>
</table>
</div>
@ -161,17 +137,39 @@
</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>
<h3>Paid summary</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Rate</th>
<th>Paid</th>
<th>Due</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.CryptoCode</td>
<td>@payment.Rate</td>
<td>@payment.Paid</td>
<td>@payment.Due</td>
<td>@payment.Address</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Payments</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Date</th>
<th>Deposit address</th>
<th>Transaction Id</th>
@ -180,14 +178,39 @@
</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>
}
{
<tr>
<td>@payment.CryptoCode</td>
<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 class="row">
<div class="col-md-12">
<h3>Addresses</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Address</th>
<th>Current</th>
</tr>
</thead>
<tbody>
@foreach(var address in Model.Addresses)
{
<tr>
<td>@address.GetCryptoCode()</td>
<td>@address.GetAddress()</td>
<td>@(!address.UnAssigned.HasValue)</td>
</tr>
}
</tbody>
</table>
</div>

@ -16,7 +16,15 @@
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create, search or pay an invoice.</p>
<p>Create, search or pay an invoice. (<a href="#help" data-toggle="collapse">Help</a>)</p>
<div id="help" class="collapse text-left">
<p>You can search for invoice Id, deposit address, price, order id, store id, any buyer information and any product information.</br>
You can also apply filters to your search by searching for `filtername:value`, here is a list of supported filters</p>
<ul>
<li><b>storeid:id</b> for filtering a specific store</li>
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
</ul>
</div>
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<input asp-for="SearchTerm" class="form-control" />
@ -43,12 +51,12 @@
</tr>
</thead>
<tbody>
@foreach (var invoice in Model.Invoices)
@foreach(var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.InvoiceId</td>
@if (invoice.Status == "paid")
@if(invoice.Status == "paid")
{
<td>
<div class="btn-group">
@ -72,7 +80,7 @@
</tbody>
</table>
<span>
@if (Model.Skip != 0)
@if(Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
{

@ -14,6 +14,7 @@
<tr>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -22,6 +23,7 @@
<tr>
<td>@user.Name</td>
<td>@user.Email</td>
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
</tr>}
</tbody>
</table>

@ -19,6 +19,10 @@
<label asp-for="RequiresConfirmedEmail"></label>
<input asp-for="RequiresConfirmedEmail" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="LockSubscription"></label>
<input asp-for="LockSubscription" type="checkbox" class="form-check-inline" />
</div>
<button type="submit" class="btn btn-success" name="command" value="Save">Save</button>
</form>
</div>

@ -2,6 +2,7 @@
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
<!DOCTYPE html>
<html lang="en">
@ -78,6 +79,78 @@
</nav>
@RenderBody()
@if(!dashboard.IsFullySynched())
{
<!-- Modal -->
<div id="synching-modal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Your nodes are synching...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>
Some of your nodes are still synching...<br />
BTCPay Server will not work correctly until it is over.
</p>
@foreach(var line in dashboard.GetAll())
{
<h4>@line.Network.CryptoCode</h4>
@if(line.Status == null)
{
<p>NBXplorer is offline</p>
}
else
{
<ul>
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
@if(line.Status.BitcoinStatus == null)
{
if(line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
{
<li>The node is starting...</li>
}
else
{
<li>The node is offline</li>
}
}
else if(line.Status.BitcoinStatus.IsSynched)
{
<li>The node is synched</li>
}
else
{
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
}
</ul>
@if(!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
{
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
</div>
</div>
}
}
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
<footer class="bg-dark">
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
</footer>
@ -95,6 +168,15 @@
<!-- Custom scripts for this template -->
<script src="~/js/creative.js"></script>
@if(!dashboard.IsFullySynched())
{
<script type="text/javascript">
$(function () {
$("#synching-modal").modal();
});
</script>
}
@RenderSection("Scripts", required: false)
</body>

@ -0,0 +1,106 @@
@model DerivationSchemeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Add derivation scheme";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<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>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<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">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
@if(Model.AddressSamples.Count == 0)
{
<span>BTCPay format memo</span>
<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>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

@ -27,7 +27,7 @@
<tr>
<th>Name</th>
<th>Website</th>
<th>Balance</th>
<th>Balances</th>
<th>Actions</th>
</tr>
</thead>
@ -42,7 +42,16 @@
<a href="@store.WebSite">@store.WebSite</a>
}
</td>
<td>@store.Balance</td>
<td>
@for(int i = 0; i < store.Balances.Length; i++)
{
<span>@store.Balances[i]</span>
if(i != store.Balances.Length - 1)
{
<br />
}
}
</td>
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
</tr>
}

@ -16,6 +16,15 @@
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="Id"></label>
<input asp-for="Id" readonly class="form-control" />
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
@ -26,10 +35,19 @@
<input asp-for="StoreWebsite" class="form-control" />
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<input asp-for="MonitoringExpiration" class="form-control" />
<span asp-validation-for="MonitoringExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="SpeedPolicy"></label>
<select asp-for="SpeedPolicy" class="form-control">
@ -41,76 +59,30 @@
</div>
<div class="form-group">
<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>
}
<span>The DerivationScheme represents the destination of the funds received by your invoice.</span>
</div>
<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)
<a asp-action="AddDerivationScheme" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a derivation scheme</a>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.DerivationSchemes)
{
<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>
}
<tr>
<td>@scheme.Crypto</td>
<td>@scheme.Value</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>

@ -8467,8 +8467,40 @@ strong {
font-size: 14px;
}
.single-item-order__right__btc-price.clickable {
cursor: pointer;
.buyerTotalLine {
cursor: pointer;
}
.buyerTotalLine .fa-angle-double-down {
opacity: 0.2;
position: absolute;
bottom: 0px;
left: 49%;
}
.buyerTotalLine.expanded .fa-angle-double-down {
display: none !important;
}
.buyerTotalLine:hover .fa-angle-double-down {
opacity: 1;
}
.buyerTotalLine .fa-angle-double-up {
display: none;
position: absolute;
bottom: 0px;
left: 49%;
}
.buyerTotalLine.expanded .fa-angle-double-up {
display: block;
opacity: 0.2;
}
.buyerTotalLine.expanded:hover .fa-angle-double-up {
display: block;
opacity: 1;
}
.single-item-order__right__btc-price__chevron {

Binary file not shown.

Before

(image error) Size: 286 KiB

After

(image error) Size: 1018 KiB

Binary file not shown.

After

(image error) Size: 23 KiB

Some files were not shown because too many files have changed in this diff Show More