Compare commits

...

49 Commits

Author SHA1 Message Date
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 #25 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 #20 2018-01-04 22:56:49 +09:00
766d96c02d fix layout 2018-01-04 22:52:14 +09:00
7445c89773 Merge pull request #23 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 #24 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 #18, 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
70 changed files with 3622 additions and 727 deletions

View File

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

View File

@ -106,19 +106,21 @@ namespace BTCPayServer.Tests
.UseStartup<Startup>()
.Build();
_Host.Start();
Runtime = (BTCPayServerRuntime)_Host.Services.GetService(typeof(BTCPayServerRuntime));
var watcher = (InvoiceWatcher)_Host.Services.GetService(typeof(InvoiceWatcher));
}
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
public BTCPayServerRuntime Runtime
{
get; set;
var waiter = ((NBXplorerWaiterAccessor)_Host.Services.GetService(typeof(NBXplorerWaiterAccessor))).Instance;
while(waiter.State != NBXplorerState.Ready)
{
Thread.Sleep(10);
}
}
public string HostName
{
get;
internal set;
}
public InvoiceRepository InvoiceRepository { get; private set; }
public T GetService<T>()
{

View File

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

View File

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

View File

@ -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
{
@ -56,9 +58,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;
@ -195,7 +230,7 @@ namespace BTCPayServer.Tests
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();
var uri = controller.GetCallbackUriAsync().GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
@ -207,7 +242,7 @@ namespace BTCPayServer.Tests
else
{
var uri = controller.GetCallbackBlockUriAsync(req).GetAwaiter().GetResult();
var uri = controller.GetCallbackBlockUriAsync().GetAwaiter().GetResult();
HttpRequestMessage message = new HttpRequestMessage();
message.Method = HttpMethod.Post;
message.RequestUri = uri;

View File

@ -22,6 +22,7 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
namespace BTCPayServer.Tests
{
@ -37,33 +38,44 @@ 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);
#pragma warning restore CS0618
}
[Fact]
@ -115,6 +127,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()
{
@ -288,31 +318,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);
@ -329,7 +359,7 @@ 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;
@ -341,7 +371,7 @@ namespace BTCPayServer.Tests
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));
@ -366,7 +396,7 @@ namespace BTCPayServer.Tests
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
@ -385,6 +415,7 @@ namespace BTCPayServer.Tests
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()

View File

@ -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:
@ -10,18 +13,36 @@ services:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_NBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_FAKECALLBACK: 'true'
TESTS_FAKECALLBACK: 'false'
TESTS_PORT: 80
TESTS_HOSTNAME: tests
# TEST_ECLAIR1: http://eclair1:8080/
# TEST_ECLAIR2: http://eclair2:8080/
expose:
- "80"
links:
- bitcoind
- nbxplorer
# - eclair1
# - eclair2
extra_hosts:
- "tests:127.0.0.1"
# 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:
- bitcoind
- nbxplorer
# - eclair1
# - eclair2
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.29
image: nicolasdorier/nbxplorer:1.0.0.34
ports:
- "32838:32838"
expose:
@ -39,28 +60,73 @@ services:
- bitcoind
- postgres
eclair:
image: nicolasdorier/docker-bitcoin:0.15.0.1
eclair1:
image: acinq/eclair:latest
environment:
JAVA_OPTS: >
-Xmx512m
-Declair.printToConsole
-Declair.bitcoind.host=bitcoind
-Declair.bitcoind.rpcport=43782
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
-Declair.bitcoind.rpcpassword=DwubwWsoo3
-Declair.bitcoind.zmq=tcp://bitcoind:29000
-Declair.chain=regtest
-Declair.api.binding-ip=0.0.0.0
links:
- bitcoind
ports:
- "30992:8080" # api port
expose:
- "9735" # server port
- "8080" # api port
eclair2:
image: acinq/eclair:latest
environment:
JAVA_OPTS: >
-Xmx512m
-Declair.printToConsole
-Declair.bitcoind.host=bitcoind
-Declair.bitcoind.rpcport=43782
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
-Declair.bitcoind.rpcpassword=DwubwWsoo3
-Declair.bitcoind.zmq=tcp://bitcoind:29000
-Declair.chain=regtest
-Declair.api.binding-ip=0.0.0.0
links:
- bitcoind
ports:
- "30993:8080" # api port
expose:
- "9735" # server port
- "8080" # api port
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" # RPC
expose:
- "43782"
- "39388"
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
postgres:
image: postgres:9.6.5
ports:
- "39372:5432"
expose:
- "5432"

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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; }
}
}

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer
{
public class BTCPayNetworkProvider
{
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
public BTCPayNetworkProvider(Network network)
{
if(network == Network.Main)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.Main,
UriScheme = "bitcoin"
});
}
if (network == Network.TestNet)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.TestNet,
UriScheme = "bitcoin"
});
}
if (network == Network.RegTest)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.RegTest,
UriScheme = "bitcoin"
});
}
}
public void Add(BTCPayNetwork network)
{
_Networks.Add(network.CryptoCode, network);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
return network;
}
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.37</Version>
<Version>1.0.0.52</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="NBitcoin" Version="4.0.0.50" />
<PackageReference Include="NBitpayClient" Version="1.0.0.13" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.18" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.22" />
<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>

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class CompositeDisposable : IDisposable
{
List<IDisposable> _Disposables = new List<IDisposable>();
public void Add(IDisposable disposable) { _Disposables.Add(disposable); }
public void Dispose()
{
foreach (var d in _Disposables)
d.Dispose();
_Disposables.Clear();
}
}
}

View File

@ -55,14 +55,9 @@ namespace BTCPayServer.Configuration
Explorer = conf.GetOrDefault<Uri>("explorer.url", networkInfo.DefaultExplorerUrl);
CookieFile = conf.GetOrDefault<string>("explorer.cookiefile", networkInfo.DefaultExplorerCookieFile);
RequireHttps = conf.GetOrDefault<bool>("requirehttps", false);
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
}
public bool RequireHttps
{
get; set;
InternalUrl = conf.GetOrDefault<Uri>("internalurl", null);
}
public string PostgresConnectionString
{
@ -74,5 +69,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri InternalUrl { get; private set; }
}
}

View File

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

View File

@ -26,11 +26,11 @@ 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);
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);
app.Option("--internalurl", $"The expected internal url of this service, this set NBXplorer callback addresses (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
return app;
}
@ -77,7 +77,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();

View File

@ -15,6 +15,11 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Events;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Hosting.Server;
namespace BTCPayServer.Controllers
{
@ -29,18 +34,23 @@ namespace BTCPayServer.Controllers
}
SettingsRepository _Settings;
Network _Network;
InvoiceWatcher _Watcher;
ExplorerClient _Explorer;
BTCPayServerOptions _Options;
EventAggregator _EventAggregator;
IServer _Server;
public CallbackController(SettingsRepository repo,
ExplorerClient explorer,
InvoiceWatcher watcher,
Network network)
EventAggregator eventAggregator,
BTCPayServerOptions options,
IServer server,
BTCPayNetworkProvider networkProvider)
{
_Settings = repo;
_Network = network;
_Watcher = watcher;
_Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork;
_Explorer = explorer;
_Options = options;
_EventAggregator = eventAggregator;
_Server = server;
}
[Route("callbacks/transactions")]
@ -48,7 +58,6 @@ namespace BTCPayServer.Controllers
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();
@ -56,7 +65,10 @@ namespace BTCPayServer.Controllers
foreach (var output in match.Outputs)
{
await _Watcher.NotifyReceived(output.ScriptPubKey);
var evt = new TxOutReceivedEvent();
evt.ScriptPubKey = output.ScriptPubKey;
evt.Address = output.ScriptPubKey.GetDestinationAddress(_Network);
_EventAggregator.Publish(evt);
}
}
@ -65,8 +77,7 @@ namespace BTCPayServer.Controllers
public async Task NewBlock(string token)
{
await AssertToken(token);
Logs.PayServer.LogInformation("New block callback");
await _Watcher.NotifyBlock();
_EventAggregator.Publish(new Events.NewBlockEvent());
}
private async Task AssertToken(string token)
@ -76,15 +87,15 @@ namespace BTCPayServer.Controllers
throw new BTCPayServer.BitpayHttpException(400, "invalid-callback-token");
}
public async Task<Uri> GetCallbackUriAsync(HttpRequest request)
public async Task<Uri> GetCallbackUriAsync()
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/transactions?token=" + token);
return BuildCallbackUri("callbacks/transactions?token=" + token);
}
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme, HttpRequest request)
public async Task RegisterCallbackUriAsync(DerivationStrategyBase derivationScheme)
{
var uri = await GetCallbackUriAsync(request);
var uri = await GetCallbackUriAsync();
await _Explorer.SubscribeToWalletAsync(uri, derivationScheme);
}
@ -100,15 +111,31 @@ namespace BTCPayServer.Controllers
return token;
}
public async Task<Uri> GetCallbackBlockUriAsync(HttpRequest request)
public async Task<Uri> GetCallbackBlockUriAsync()
{
string token = await GetToken();
return new Uri(request.GetAbsoluteRoot() + "/callbacks/blocks?token=" + token);
return BuildCallbackUri("callbacks/blocks?token=" + token);
}
public async Task<Uri> RegisterCallbackBlockUriAsync(HttpRequest request)
private Uri BuildCallbackUri(string callbackPath)
{
var address = _Server.Features.Get<IServerAddressesFeature>().Addresses
.Select(c => new Uri(TransformToRoutable(c)))
.First();
var baseUrl = _Options.InternalUrl == null ? address.AbsoluteUri : _Options.InternalUrl.AbsoluteUri;
baseUrl = baseUrl.WithTrailingSlash();
return new Uri(baseUrl + callbackPath);
}
private string TransformToRoutable(string host)
{
if (host.StartsWith("http://0.0.0.0"))
host = host.Replace("http://0.0.0.0", "http://127.0.0.1");
return host;
}
public async Task<Uri> RegisterCallbackBlockUriAsync(Uri uri)
{
var uri = await GetCallbackBlockUriAsync(request);
await _Explorer.SubscribeToBlocksAsync(uri);
return uri;
}

View File

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

View File

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

View File

@ -16,15 +16,17 @@ 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;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpPost]
[Route("invoices/{invoiceId}")]
public IActionResult Invoice(string invoiceId, string command)
public IActionResult Invoice(string invoiceId, string command, string cryptoCode = null)
{
if (command == "refresh")
{
@ -39,8 +41,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("invoices/{invoiceId}")]
public async Task<IActionResult> Invoice(string invoiceId)
public async Task<IActionResult> Invoice(string invoiceId, string cryptoCode = null)
{
if (cryptoCode == null)
cryptoCode = "BTC";
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
@ -49,8 +53,17 @@ namespace BTCPayServer.Controllers
if (invoice == null)
return NotFound();
var dto = invoice.EntityToDTO();
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || !invoice.Support(network))
return NotFound();
var cryptoData = invoice.GetCryptoData(network);
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var store = await _StoreRepository.FindStore(invoice.StoreId);
var accounting = cryptoData.Calculate();
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
@ -62,16 +75,16 @@ namespace BTCPayServer.Controllers
ExpirationDate = invoice.ExpirationTime,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Rate = invoice.Rate,
Rate = cryptoData.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",
BTC = accounting.TotalDue.ToString() + $" {network.CryptoCode}",
BTCDue = accounting.Due.ToString() + $" {network.CryptoCode}",
BTCPaid = accounting.Paid.ToString() + $" {network.CryptoCode}",
NetworkFee = accounting.NetworkFee.ToString() + $" {network.CryptoCode}",
NotificationUrl = invoice.NotificationURL,
ProductInformation = invoice.ProductInformation,
BitcoinAddress = invoice.DepositAddress,
PaymentUrl = dto.PaymentUrls.BIP72
BitcoinAddress = BitcoinAddress.Create(cryptoInfo.Address, network.NBitcoinNetwork),
PaymentUrl = cryptoInfo.PaymentUrls.BIP72
};
var payments = invoice
@ -79,11 +92,11 @@ namespace BTCPayServer.Controllers
.Select(async payment =>
{
var m = new InvoiceDetailsModel.Payment();
m.DepositAddress = payment.Output.ScriptPubKey.GetDestinationAddress(_Network);
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(network.NBitcoinNetwork);
m.Confirmations = (await _Explorer.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = _Network == Network.Main ? $"https://www.smartbit.com.au/tx/{m.TransactionId}" : $"https://testnet.smartbit.com.au/tx/{m.TransactionId}";
m.TransactionLink = string.Format(network.BlockExplorerLink, m.TransactionId);
return m;
})
.ToArray();
@ -98,48 +111,57 @@ namespace BTCPayServer.Controllers
[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)
{
if (cryptoCode == null)
cryptoCode = "BTC";
//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)
{
if (cryptoCode == null)
throw new ArgumentNullException(nameof(cryptoCode));
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null)
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || network == null || !invoice.Support(network))
return null;
var cryptoData = invoice.GetCryptoData(network);
var store = await _StoreRepository.FindStore(invoice.StoreId);
var dto = invoice.EntityToDTO();
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()
{
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,
BtcAmount = (accounting.TotalDue - cryptoData.TxFee).ToString(),
BtcTotalDue = accounting.TotalDue.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(currency)) + $" ({currency})",
Rate = cryptoData.Rate.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})",
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = invoice.TxFee.ToString(),
InvoiceBitcoinUrl = dto.PaymentUrls.BIP72,
TxCount = invoice.GetTxCount(),
BtcPaid = invoice.GetTotalPaid().ToString(),
TxFees = cryptoData.TxFee.ToString(),
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP72,
TxCount = accounting.TxCount,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status
};
@ -161,14 +183,76 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId)
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
{
var model = await GetInvoiceModel(invoiceId);
if (cryptoCode == null)
cryptoCode = "BTC";
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
{
_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId));
_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId));
_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)
@ -235,9 +319,9 @@ 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());

View File

@ -48,46 +48,47 @@ namespace BTCPayServer.Controllers
IRateProvider _RateProvider;
private InvoiceWatcher _Watcher;
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;
public InvoiceController(InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
InvoiceWatcher watcher,
EventAggregator eventAggregator,
InvoiceWatcherAccessor watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
BTCPayNetworkProvider networkProvider,
IFeeProviderFactory feeProviderFactory)
{
_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));
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
_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)
{
//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 entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow,
DerivationStrategy = derivationStrategy ?? throw new BitpayHttpException(400, "This store has not configured the derivation strategy")
};
var storeBlob = store.GetStoreBlob(_Network);
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;
@ -112,16 +113,45 @@ 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 = storeBlob.NetworkFeeDisabled ? Money.Zero : (await getFeeRate).GetFee(100); // assume price for 100 bytes
entity.Rate = (double)await getRate;
var queries = storeBlob.GetSupportedCryptoCurrencies()
.Select(n => _NetworkProvider.GetNetwork(n))
.Where(n => n != null)
.Select(network =>
{
return new
{
network = network,
getFeeRate = _FeeProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
getRate = _RateProvider.GetRateAsync(invoice.Currency),
getAddress = _Wallet.ReserveAddressAsync(ParseDerivationStrategy(derivationStrategy, network))
};
});
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.CryptoCode == "BTC")
{
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);
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_Watcher.Watch(entity.Id);
var resp = entity.EntityToDTO();
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
@ -153,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)

View File

@ -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;
@ -33,7 +34,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
Network network,
BTCPayNetworkProvider networkProvider,
IHostingEnvironment env)
{
_Repo = repo;
@ -42,7 +43,7 @@ namespace BTCPayServer.Controllers
_TokenController = tokenController;
_Wallet = wallet;
_Env = env;
_Network = network;
_Network = networkProvider.GetNetwork("BTC").NBitcoinNetwork;
_CallbackController = callbackController;
}
Network _Network;
@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy))).ToArray();
var balances = stores.Select(async s => string.IsNullOrEmpty(s.DerivationStrategy) ? Money.Zero : await _Wallet.GetBalance(ParseDerivationStrategy(s.DerivationStrategy, null))).ToArray();
for (int i = 0; i < stores.Length; i++)
{
@ -144,7 +145,7 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob(_Network);
var storeBlob = store.GetStoreBlob();
var vm = new StoreViewModel();
vm.Id = store.Id;
vm.StoreName = store.StoreName;
@ -195,9 +196,10 @@ namespace BTCPayServer.Controllers
{
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
var strategy = ParseDerivationStrategy(model.DerivationScheme);
var strategy = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
await _Wallet.TrackAsync(strategy);
await _CallbackController.RegisterCallbackUriAsync(strategy, Request);
await _CallbackController.RegisterCallbackUriAsync(strategy);
model.DerivationScheme = strategy.ToString();
}
store.DerivationStrategy = model.DerivationScheme;
}
@ -208,11 +210,11 @@ namespace BTCPayServer.Controllers
}
}
var blob = store.GetStoreBlob(_Network);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
if (store.SetStoreBlob(blob, _Network))
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
@ -230,21 +232,62 @@ namespace BTCPayServer.Controllers
}
else
{
var facto = new DerivationStrategyFactory(_Network);
var scheme = facto.Parse(model.DerivationScheme);
var line = scheme.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
if (!string.IsNullOrEmpty(model.DerivationScheme))
{
var address = line.Derive((uint)i);
model.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(_Network).ToString()));
try
{
var scheme = ParseDerivationStrategy(model.DerivationScheme, model.DerivationSchemeFormat);
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()));
}
}
catch
{
ModelState.AddModelError(nameof(model.DerivationScheme), "Invalid Derivation Scheme");
}
}
return View(model);
}
}
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme)
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format)
{
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 == Network.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), _Network).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategyFactory(_Network).Parse(derivationScheme);
}

View File

@ -17,6 +17,17 @@ namespace BTCPayServer.Data
get; set;
}
[Obsolete("Use GetCryptoCode instead")]
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
}
#pragma warning restore CS0618
public DateTimeOffset Assigned
{
get; set;

View File

@ -62,15 +62,17 @@ namespace BTCPayServer.Data
set;
}
public StoreBlob GetStoreBlob(Network network)
static Network Dummy = Network.Main;
public StoreBlob GetStoreBlob()
{
return StoreBlob == null ? new StoreBlob() : new Serializer(network).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
return StoreBlob == null ? new StoreBlob() : new Serializer(Dummy).ToObject<StoreBlob>(Encoding.UTF8.GetString(StoreBlob));
}
public bool SetStoreBlob(StoreBlob storeBlob, Network network)
public bool SetStoreBlob(StoreBlob storeBlob)
{
var original = new Serializer(network).ToString(GetStoreBlob(network));
var newBlob = 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);
@ -95,5 +97,19 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetSupportedCryptoCurrencies() instead")]
public string[] SupportedCryptoCurrencies { get; set; }
public string[] GetSupportedCryptoCurrencies()
{
#pragma warning disable CS0618
if(SupportedCryptoCurrencies == null)
{
return new string[] { "BTC" };
}
return SupportedCryptoCurrencies;
#pragma warning restore CS0618
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,147 @@
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
{
if (evt == null)
throw new ArgumentNullException(nameof(evt));
List<Action<object>> actionList = new List<Action<object>>();
lock (_Subscriptions)
{
if (_Subscriptions.TryGetValue(typeof(T), 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();
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 Script ScriptPubKey { get; set; }
public BitcoinAddress Address { get; set; }
public override string ToString()
{
String address = Address?.ToString() ?? ScriptPubKey.ToHex();
return $"{address} received a transaction";
}
}
}

View File

@ -71,10 +71,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 + "');");
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using System;
using System.Collections.Generic;
@ -34,6 +35,7 @@ using System.Threading;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Authentication;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
namespace BTCPayServer.Hosting
{
@ -83,19 +85,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,31 +95,60 @@ 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<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProvider>(o => new NBXplorerFeeProvider()
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClient>())
{
Fallback = new FeeRate(100, 1),
BlockTarget = 20,
ExplorerClient = o.GetRequiredService<ExplorerClient>()
BlockTarget = 20
});
services.TryAddSingleton<NBXplorerWaiterAccessor>();
services.AddSingleton<IHostedService, NBXplorerWaiter>();
services.TryAddSingleton<ExplorerClient>(o =>
{
var runtime = o.GetRequiredService<BTCPayServerRuntime>();
return runtime.Explorer;
var opts = o.GetRequiredService<BTCPayServerOptions>();
var explorer = new ExplorerClient(opts.Network, opts.Explorer);
if (!explorer.SetCookieAuth(opts.CookieFile))
explorer.SetNoAuth();
return explorer;
});
services.TryAddSingleton<Bitpay>(o =>
{
@ -141,11 +159,16 @@ namespace BTCPayServer.Hosting
});
services.TryAddSingleton<IRateProvider>(o =>
{
return new CachedRateProvider(new CoinAverageRateProvider(), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
var coinaverage = new CoinAverageRateProvider();
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
return new CachedRateProvider(new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay }), o.GetRequiredService<IMemoryCache>()) { CacheSpan = TimeSpan.FromMinutes(1.0) };
});
services.TryAddSingleton<InvoiceWatcher>();
services.TryAddSingleton<InvoiceNotificationManager>();
services.TryAddSingleton<IHostedService>(o => o.GetRequiredService<InvoiceWatcher>());
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.TryAddSingleton<InvoiceWatcherAccessor>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddSingleton<Initializer>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationHandler, OwnStoreHandler>();
services.AddTransient<AccessTokenController>();
@ -172,12 +195,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 +203,11 @@ namespace BTCPayServer.Hosting
scope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
});
}
var initialize = app.ApplicationServices.GetService<Initializer>();
initialize.Init();
app.UseMiddleware<BTCPayMiddleware>();
return app;
}

View File

@ -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,53 @@ namespace BTCPayServer.Hosting
}
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
// 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);
}
// 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 (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
httpContext.Request.Scheme = scheme;
if (scheme == "http")
p = 80;
if (scheme == "https")
p = 443;
}
}
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
p = pp;
}
}
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;

View File

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

View File

@ -0,0 +1,45 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Logging;
using BTCPayServer.Events;
namespace BTCPayServer
{
public class Initializer
{
EventAggregator _Aggregator;
CallbackController _CallbackController;
public Initializer(EventAggregator aggregator,
CallbackController callbackController
)
{
_Aggregator = aggregator;
_CallbackController = callbackController;
}
public void Init()
{
_Aggregator.Subscribe<NBXplorerStateChangedEvent>(async (s, evt) =>
{
if (evt.NewState == NBXplorerState.Ready)
{
s.Unsubscribe();
try
{
var callback = await _CallbackController.GetCallbackBlockUriAsync();
await _CallbackController.RegisterCallbackBlockUriAsync(callback);
Logs.PayServer.LogInformation($"Registering block callback to " + callback);
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Could not register block callback");
s.Resubscribe();
}
}
});
}
}
}

View File

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

View File

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

View File

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

View File

@ -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");
@ -382,7 +384,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany()
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});

View File

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

View File

@ -92,7 +92,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public double Rate
public decimal Rate
{
get;
internal set;

View File

@ -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,21 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoreViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public StoreViewModel()
{
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 Id { get; set; }
[Display(Name = "Store Name")]
[Required]
@ -29,12 +45,20 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[DerivationStrategyValidator]
public string DerivationScheme
{
get; set;
}
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
public SelectList DerivationSchemeFormats { get; set; }
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration

View File

@ -0,0 +1,175 @@
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
{
public class NBXplorerWaiterAccessor
{
public NBXplorerWaiter Instance { get; set; }
}
public enum NBXplorerState
{
NotConnected,
Synching,
Ready
}
public class NBXplorerWaiter : IHostedService
{
public NBXplorerWaiter(ExplorerClient client, EventAggregator aggregator, NBXplorerWaiterAccessor accessor)
{
_Client = client;
_Aggregator = aggregator;
accessor.Instance = this;
}
EventAggregator _Aggregator;
ExplorerClient _Client;
Timer _Timer;
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
public Task StartAsync(CancellationToken cancellationToken)
{
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
return Task.CompletedTask;
}
void Callback(object state)
{
if (!_Idle.IsSet)
return;
try
{
_Idle.Reset();
CheckStatus().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
}
finally
{
_Idle.Set();
}
}
async Task CheckStatus()
{
while (await StepAsync())
{
}
}
private async Task<bool> StepAsync()
{
var oldState = State;
StatusResult status = null;
switch (State)
{
case NBXplorerState.NotConnected:
status = await GetStatusWithTimeout();
if (status != null)
{
if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
else
{
State = NBXplorerState.Synching;
}
}
break;
case NBXplorerState.Synching:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
break;
case NBXplorerState.Ready:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (!status.IsFullySynched)
{
State = NBXplorerState.Synching;
}
break;
}
LastStatus = status;
if (oldState != State)
{
if (State == NBXplorerState.Synching)
{
SetInterval(TimeSpan.FromSeconds(10));
}
else
{
SetInterval(TimeSpan.FromMinutes(1));
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(oldState, State));
}
return oldState != State;
}
private void SetInterval(TimeSpan interval)
{
try
{
_Timer.Change(0, (int)interval.TotalMilliseconds);
}
catch { }
}
private async Task<StatusResult> GetStatusWithTimeout()
{
CancellationTokenSource cts = new CancellationTokenSource();
using (cts)
{
var cancellation = cts.Token;
while (!cancellation.IsCancellationRequested)
{
try
{
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
return status;
}
catch (OperationCanceledException) { }
catch { }
}
}
return null;
}
public NBXplorerState State { get; private set; }
public StatusResult LastStatus { get; private set; }
public Task StopAsync(CancellationToken cancellationToken)
{
_Timer.Dispose();
_Timer = null;
_Idle.Wait();
return Task.CompletedTask;
}
}
}

View File

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

View File

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

View File

@ -8,29 +8,49 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Fees
{
public class NBXplorerFeeProvider : IFeeProvider
public class NBXplorerFeeProviderFactory : IFeeProviderFactory
{
public NBXplorerFeeProviderFactory(ExplorerClient explorerClient)
{
if (explorerClient == null)
throw new ArgumentNullException(nameof(explorerClient));
_ExplorerClient = explorerClient;
}
private readonly ExplorerClient _ExplorerClient;
public ExplorerClient ExplorerClient
{
get; set;
get
{
return _ExplorerClient;
}
}
public FeeRate Fallback
public FeeRate Fallback { get; set; }
public int BlockTarget { get; set; }
public IFeeProvider CreateFeeProvider(BTCPayNetwork network)
{
get; set;
return new NBXplorerFeeProvider(this);
}
public int BlockTarget
}
public class NBXplorerFeeProvider : IFeeProvider
{
public NBXplorerFeeProvider(NBXplorerFeeProviderFactory factory)
{
get; set;
if (factory == null)
throw new ArgumentNullException(nameof(factory));
_Factory = factory;
}
private readonly NBXplorerFeeProviderFactory _Factory;
public async Task<FeeRate> GetFeeRateAsync()
{
try
{
return (await ExplorerClient.GetFeeRateAsync(BlockTarget).ConfigureAwait(false)).FeeRate;
return (await _Factory.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;
}
}
}

View File

@ -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;
}
@ -231,6 +186,8 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use GetCryptoData(network).TxFee instead")]
public Money TxFee
{
get;
@ -251,6 +208,10 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
[Obsolete("Use Set/GetCryptoData() instead")]
public JObject CryptoData { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
{
@ -275,7 +236,7 @@ namespace BTCPayServer.Services.Invoices
}
public InvoiceResponse EntityToDTO()
public InvoiceResponse EntityToDTO(BTCPayNetworkProvider networkProvider)
{
ServerUrl = ServerUrl ?? "";
InvoiceResponse dto = new InvoiceResponse
@ -286,33 +247,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;
}
@ -323,11 +319,152 @@ 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) + TxFee;
var paid = Money.Zero;
var cryptoPaid = Money.Zero;
int txCount = 1;
var payments =
ParentEntity.Payments
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.Select(_ =>
{
paid += _.GetValue(cryptoData, CryptoCode);
if (CryptoCode == _.GetCryptoCode())
cryptoPaid += _.GetValue();
return _;
})
.TakeWhile(_ =>
{
var paidEnough = totalDue <= paid;
if (!paidEnough && _.GetCryptoCode() == CryptoCode)
{
txCount++;
totalDue += TxFee;
}
return !paidEnough;
})
.ToArray();
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 = TxFee * txCount;
return accounting;
}
}
public class AccountedPaymentEntity
@ -351,13 +488,59 @@ 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)
{
#pragma warning disable CS0618
var to = cryptoCode;
var from = GetCryptoCode();
if (to == from)
return Output.Value;
var fromRate = cryptoData[from].Rate;
var toRate = cryptoData[to].Rate;
var fiatValue = fromRate * Output.Value.ToDecimal(MoneyUnit.BTC);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return Money.Coins(otherCurrencyValue);
#pragma warning restore CS0618
}
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
}

View File

@ -15,10 +15,12 @@ using NBitpayClient;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using Microsoft.Extensions.Hosting;
using BTCPayServer.Events;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceNotificationManager
public class InvoiceNotificationManager : IHostedService
{
public static HttpClient _Client = new HttpClient();
@ -41,16 +43,35 @@ namespace BTCPayServer.Services.Invoices
}
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;
}
public void Notify(InvoiceEntity invoice)
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);
@ -70,31 +91,7 @@ namespace BTCPayServer.Services.Invoices
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);
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
}
@ -116,11 +113,82 @@ namespace BTCPayServer.Services.Invoices
}
}
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), Encoding.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;
}
}
}

View File

@ -45,7 +45,7 @@ namespace BTCPayServer.Services.Invoices
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
@ -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,52 +122,77 @@ 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()
{
Address = BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash.ToString(),
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
});
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Address = cryptoData.DepositAddress,
#pragma warning disable CS0618
CryptoCode = cryptoData.CryptoCode,
#pragma warning restore CS0618
Assigned = DateTimeOffset.UtcNow
});
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);
}
currencyData.DepositAddress = bitcoinAddress.ToString();
#pragma warning disable CS0618
if (network.CryptoCode == "BTC")
{
invoiceEntity.DepositAddress = currencyData.DepositAddress;
}
#pragma warning disable CS0618
invoiceEntity.SetCryptoData(cryptoData);
invoice.Blob = ToBytes(invoiceEntity);
context.AddressInvoices.Add(new AddressInvoiceData() { Address = bitcoinAddress.ScriptPubKey.Hash.ToString(), InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow });
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
@ -181,14 +207,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.Address = address.Value.DepositAddress;
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
}
}
public async Task UnaffectAddress(string invoiceId)
@ -199,9 +230,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 +252,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 +313,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;

View File

@ -13,21 +13,30 @@ using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
namespace BTCPayServer.Services.Invoices
{
public class InvoiceWatcherAccessor
{
public InvoiceWatcher Instance { get; set; }
}
public class InvoiceWatcher : IHostedService
{
InvoiceRepository _InvoiceRepository;
ExplorerClient _ExplorerClient;
DerivationStrategyFactory _DerivationFactory;
InvoiceNotificationManager _NotificationManager;
EventAggregator _EventAggregator;
BTCPayWallet _Wallet;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(ExplorerClient explorerClient,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWallet wallet,
InvoiceNotificationManager notificationManager)
InvoiceWatcherAccessor accessor)
{
LongPollingMode = explorerClient.Network == Network.RegTest;
PollInterval = explorerClient.Network == Network.RegTest ? TimeSpan.FromSeconds(10.0) : TimeSpan.FromMinutes(1.0);
@ -35,22 +44,25 @@ namespace BTCPayServer.Services.Invoices
_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));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
accessor.Instance = this;
}
CompositeDisposable leases = new CompositeDisposable();
public bool LongPollingMode
{
get; set;
}
public async Task NotifyReceived(Script scriptPubKey)
async Task NotifyReceived(Script scriptPubKey)
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey);
if (invoice != null)
_WatchRequests.Add(invoice);
}
public async Task NotifyBlock()
async Task NotifyBlock()
{
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{
@ -69,17 +81,22 @@ namespace BTCPayServer.Services.Invoices
if (invoice == null)
break;
var stateBefore = invoice.Status;
var result = await UpdateInvoice(changes, invoice).ConfigureAwait(false);
var postSaveActions = new List<Action>();
var result = await UpdateInvoice(changes, invoice, postSaveActions).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}");
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
foreach(var saveAction in postSaveActions)
{
saveAction();
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
@ -104,18 +121,19 @@ namespace BTCPayServer.Services.Invoices
}
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice)
private async Task<(bool NeedSave, UTXOChanges Changes)> UpdateInvoice(UTXOChanges changes, InvoiceEntity invoice, List<Action> postSaveActions)
{
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));
if (invoice.AvailableAddressHashes.Contains(received.ScriptPubKey.Hash.ToString()))
receivedCoins.Add(received.AsCoin());
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
bool dirtyAddress = false;
@ -123,33 +141,32 @@ namespace BTCPayServer.Services.Invoices
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin).ConfigureAwait(false);
invoice.Payments.Add(payment);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoicePaymentEvent(invoice.Id)));
dirtyAddress = true;
}
//////
var network = _NetworkProvider.GetNetwork("BTC");
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
needSave = true;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "expired")));
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())
var totalPaid = (await GetPaymentsWithTransaction(invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
{
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "paid")));
invoice.Status = "paid";
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
@ -161,23 +178,23 @@ namespace BTCPayServer.Services.Invoices
}
}
if (totalPaid > invoice.GetTotalCryptoDue() && invoice.ExceptionStatus != "paidOver")
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
needSave = true;
}
if (totalPaid < invoice.GetTotalCryptoDue() && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
Logs.PayServer.LogInformation("Paid to " + invoice.DepositAddress);
Logs.PayServer.LogInformation("Paid to " + cryptoData.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);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
@ -199,30 +216,27 @@ namespace BTCPayServer.Services.Invoices
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.Output.Value).Sum();
var chainTotalConfirmed = chainConfirmedTransactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(chainTotalConfirmed < invoice.GetTotalCryptoDue()))
(chainTotalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "invalid")));
invoice.Status = "invalid";
needSave = true;
if (invoice.FullNotifications)
{
_NotificationManager.Notify(invoice);
}
}
else
{
var totalConfirmed = transactions.Select(t => t.Payment.Output.Value).Sum();
if (totalConfirmed >= invoice.GetTotalCryptoDue())
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "confirmed")));
invoice.Status = "confirmed";
_NotificationManager.Notify(invoice);
needSave = true;
}
}
@ -232,12 +246,11 @@ namespace BTCPayServer.Services.Invoices
{
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())
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
postSaveActions.Add(() => _EventAggregator.Publish(new InvoiceStatusChangedEvent(invoice, "complete")));
invoice.Status = "complete";
if (invoice.FullNotifications)
_NotificationManager.Notify(invoice);
needSave = true;
}
}
@ -349,6 +362,10 @@ namespace BTCPayServer.Services.Invoices
_WatchRequests.Add(pending);
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey); }));
return Task.CompletedTask;
}
@ -395,6 +412,7 @@ namespace BTCPayServer.Services.Invoices
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_UpdatePendingInvoices.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));

View File

@ -17,61 +17,6 @@ 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 string Market
@ -80,13 +25,20 @@ namespace BTCPayServer.Services.Rates
} = "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 +49,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("BTC", StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => p.Name.Substring(3, 3), 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();
}
}

View File

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

View File

@ -53,8 +53,8 @@ namespace BTCPayServer.Services.Wallets
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
{
var result = await _Client.SyncAsync(derivationStrategy, null, true);
return result.Confirmed.UTXOs.Select(u => u.Output.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Output.Value))
return result.Confirmed.UTXOs.Select(u => u.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Value))
.Sum();
}
}

View File

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

View File

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

View File

@ -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,7 +105,7 @@
</div>
<!---->
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price clickable" id="buyerTotalBtcAmount">
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} BTC</span>
</div>
<!---->
@ -122,6 +114,9 @@
</div>
<!---->
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<!---->
<line-items>

View File

@ -2,6 +2,13 @@
@inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@inject BTCPayServer.NBXplorerWaiterAccessor waiter
@{
var waiterState = waiter.Instance.State;
var lastStatus = waiter.Instance.LastStatus;
var verificationProgress = waiter.Instance.LastStatus?.BitcoinStatus != null ? waiter.Instance.LastStatus.BitcoinStatus.VerificationProgress * 100 : 0.0;
}
<!DOCTYPE html>
<html lang="en">
@ -78,6 +85,86 @@
</nav>
@RenderBody()
@if(waiterState == NBXplorerState.NotConnected)
{
<!-- Modal -->
<div id="no-nbxplorer" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">NBXplorer is not running</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>NBXplorer is not running, BTCPay Server will not be functional.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
@if(waiterState == NBXplorerState.Synching && lastStatus.BitcoinStatus != null)
{
<!-- 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">Bitcoin Core node is synching...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>Bitcoin Core is synching (Chain height: @lastStatus.BitcoinStatus.Headers, Block height: @lastStatus.BitcoinStatus.Blocks)</p>
<p>BTCPay Server will not work correctly until it is over.</p>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)verificationProgress)"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)verificationProgress)%">
@((int)verificationProgress)%
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
else if(waiterState == NBXplorerState.Synching && lastStatus.BitcoinStatus == null)
{
<!-- 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">Bitcoin Core node is starting...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>Satoshi is rolling the blocks forward... Time to drink a cup of tea?</p>
</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 +182,23 @@
<!-- Custom scripts for this template -->
<script src="~/js/creative.js"></script>
@if(waiterState == NBXplorerState.NotConnected)
{
<script type="text/javascript">
$(function () {
$("#no-nbxplorer").modal();
});
</script>
}
@if(waiterState == NBXplorerState.Synching)
{
<script type="text/javascript">
$(function () {
$("#synching-modal").modal();
});
</script>
}
@RenderSection("Scripts", required: false)
</body>

View File

@ -56,72 +56,77 @@
<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. 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">
<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)
{
<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>
}
{
<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>

View File

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

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 1018 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -191,7 +191,7 @@ function onDataCallback(jsonData) {
checkoutCtrl.srvModel = jsonData;
}
var watcher = setInterval(function () {
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status";
$.ajax({
url: path,
@ -201,6 +201,26 @@ var watcher = setInterval(function () {
}).fail(function (jqXHR, textStatus, errorThrown) {
});
}
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
path = path.replace("https://", "wss://");
path = path.replace("http://", "ws://");
try {
var socket = new WebSocket(path);
socket.onmessage = function (e) {
fetchStatus();
};
}
catch (e) {
console.error("Error while connecting to websocket for invoice notifictions");
}
}
var watcher = setInterval(function () {
fetchStatus();
}, 2000);
$(".menu__item").click(function () {
@ -218,8 +238,9 @@ function validateEmail(email) {
}
// Expand Line-Items
$("#buyerTotalBtcAmount").click(function () {
$(".buyerTotalLine").click(function () {
$("line-items").toggleClass("expanded");
$(".buyerTotalLine").toggleClass("expanded");
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
});

View File

@ -6,7 +6,7 @@ RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/aspnetcore:2.0.0
FROM microsoft/aspnetcore:2.0.3
WORKDIR /app
RUN mkdir /datadir
@ -14,4 +14,4 @@ ENV BTCPAY_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]

View File

@ -1,22 +1,25 @@
![BTCPay Server](BTCPayServer/wwwroot/img/btc_pay_BG_twitter.png)
[![Docker Automated build](https://img.shields.io/docker/automated/jrottenberg/ffmpeg.svg)](https://hub.docker.com/r/nicolasdorier/btcpayserver/)
[![Deploy to Azure](https://azuredeploy.net/deploybutton.svg)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fbtcpayserver%2Fbtcpayserver-azure%2Fmaster%2Fazuredeploy.json)
# BTCPay Server
## Introduction
If you:
BTCPay Server is an Open Source payment processor conforms to the invoice API of Bitpay.
This allows easy migration of your code base to your own, self-hosted payment processor.
* Currently depend on BitPay and want to keep using Bitcoin for your payments after November
* Do not want to give custody or control of your funds to a third party
* Have been rejected by BitPay for KYC/AML reasons after developing your solution
* Are a service provider who wants to offer Bitcoin payments to your customer with a different pricing model than BitPay
* Want to propose services similar to BitPay for an alt-currency.
* Want features BitPay is not willing to consider (Multi-sig + SegWit support soon)
This solution is for you if:
Then head out to the [documentation](https://github.com/btcpayserver/btcpayserver-doc), this project is for you.
* You currently use Bitpay as a payment processor but are worry about their commitment to Bitcoin in the future
* You want to be in control of your own funds
* Bitpay compliance team decided to reject your application
* You want lower fee (we support Segwit)
* You want to become a payment processor yourself and offer BTCPay hosted solution to merchants
* You want to a way support other currency than those offered by Bitpay
If you want to help development, go to [Local Development](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Local-Development.md).
## Documentation
## Additional resources
[Introduction of BTCPay on youtube](https://www.youtube.com/watch?v=npFMOu6tTpA)
Please check out our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) for more details.