Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
bb46294a6d | |||
f8aad6ac80 | |||
658d1f1df0 | |||
ee3144f34a | |||
9a34fe46fb | |||
766d96c02d | |||
7445c89773 | |||
28ac5608a5 | |||
44c925a4ba | |||
51cd89f177 | |||
ab188ad54f | |||
513835ed0f | |||
a863812f90 | |||
a37fdde214 | |||
d5ef36fe50 | |||
7430ceb23d | |||
395b550c21 | |||
774565b121 | |||
72d1344002 | |||
b4ee5dcb0d | |||
a0f0ff0bf1 | |||
db2cc8f951 | |||
24007f1515 | |||
3d7445f359 | |||
34760afe77 | |||
417209b057 | |||
9026378b86 | |||
9b3dca1683 | |||
cde593a935 | |||
f0755260a6 | |||
582e1eb4f8 | |||
aaadda3e0f | |||
9d7f5b5b6e | |||
99040597dc | |||
d9794216dd | |||
84bb6056d3 | |||
dfed2daa8e | |||
1521ec8071 | |||
bf7ae178ef | |||
dc7f96c6da | |||
c6959bb0bc | |||
d4dd6c84bc | |||
e59678360c | |||
1b6fa0c7d8 | |||
95a5936daf | |||
477d4117ce | |||
444f119e50 | |||
fa13a2874e | |||
24ce325e31 |
@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170720-02" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -106,19 +106,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>()
|
||||
{
|
||||
|
37
BTCPayServer.Tests/EclairTester.cs
Normal file
37
BTCPayServer.Tests/EclairTester.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
# How to run the tests
|
||||
# How to be started for development
|
||||
|
||||
The tests depends on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
BTCPay Server tests depend on having a proper environment running with Postgres, Bitcoind, NBxplorer configured.
|
||||
You can however use the `docker-compose.yml` of this folder to get it running.
|
||||
|
||||
In addition, when you run a debug session of BTCPay (Hitting F5 on Visual Studio Code or Visual Studio 2017), it will run the launch profile called `Docker-Regtest`. This launch profile depends on this `docker-compose` running.
|
||||
|
||||
This is running a bitcoind instance on regtest, a private bitcoin blockchain for testing on which you can generate blocks yourself.
|
||||
|
||||
```
|
||||
docker-compose up nbxplorer
|
||||
docker-compose up dev
|
||||
```
|
||||
|
||||
You can run the tests while it is running through your favorite IDE, or with
|
||||
@ -43,4 +45,4 @@ docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitc
|
||||
If you are using Powershell:
|
||||
```
|
||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
||||
```
|
||||
```
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -16,6 +17,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Eclair;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
16
BTCPayServer/BTCPayNetwork.cs
Normal file
16
BTCPayServer/BTCPayNetwork.cs
Normal 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; }
|
||||
}
|
||||
}
|
59
BTCPayServer/BTCPayNetworkProvider.cs
Normal file
59
BTCPayServer/BTCPayNetworkProvider.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
19
BTCPayServer/CompositeDisposable.cs
Normal file
19
BTCPayServer/CompositeDisposable.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -1,110 +0,0 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using BTCPayServer.Logging;
|
||||
using DBreeze;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class BTCPayServerRuntime : IDisposable
|
||||
{
|
||||
public ExplorerClient Explorer
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public void Configure(BTCPayServerOptions opts)
|
||||
{
|
||||
ConfigureAsync(opts).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task ConfigureAsync(BTCPayServerOptions opts)
|
||||
{
|
||||
Network = opts.Network;
|
||||
Explorer = new ExplorerClient(opts.Network, opts.Explorer);
|
||||
|
||||
if (!Explorer.SetCookieAuth(opts.CookieFile))
|
||||
Explorer.SetNoAuth();
|
||||
|
||||
CancellationTokenSource cts = new CancellationTokenSource(30000);
|
||||
try
|
||||
{
|
||||
Logs.Configuration.LogInformation("Trying to connect to explorer " + Explorer.Address.AbsoluteUri);
|
||||
await Explorer.WaitServerStartedAsync(cts.Token).ConfigureAwait(false);
|
||||
Logs.Configuration.LogInformation("Connection successfull");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ConfigException($"Could not connect to NBXplorer, {ex.Message}");
|
||||
}
|
||||
|
||||
ApplicationDbContextFactory dbContext = null;
|
||||
if (opts.PostgresConnectionString == null)
|
||||
{
|
||||
var connStr = "Data Source=" + Path.Combine(opts.DataDir, "sqllite.db");
|
||||
Logs.Configuration.LogInformation($"SQLite DB used ({connStr})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Sqlite, connStr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logs.Configuration.LogInformation($"Postgres DB used ({opts.PostgresConnectionString})");
|
||||
dbContext = new ApplicationDbContextFactory(DatabaseType.Postgres, opts.PostgresConnectionString);
|
||||
}
|
||||
DBFactory = dbContext;
|
||||
|
||||
InvoiceRepository = new InvoiceRepository(dbContext, CreateDBPath(opts, "InvoiceDB"), Network);
|
||||
_Resources.Add(InvoiceRepository);
|
||||
}
|
||||
|
||||
private static string CreateDBPath(BTCPayServerOptions opts, string name)
|
||||
{
|
||||
var dbpath = Path.Combine(opts.DataDir, name);
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return dbpath;
|
||||
}
|
||||
|
||||
List<IDisposable> _Resources = new List<IDisposable>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_Resources)
|
||||
{
|
||||
foreach (var r in _Resources)
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
_Resources.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public ApplicationDbContextFactory DBFactory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -26,11 +26,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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -24,16 +24,19 @@ namespace BTCPayServer.Controllers
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -56,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO();
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
|
||||
@ -90,7 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO()).ToArray();
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
@ -17,21 +17,25 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId)
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.IsExpired())
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
};
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = _Network;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = dto.BTCDue, Script = BitcoinAddress.Create(dto.BitcoinAddress, _Network).ScriptPubKey });
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
|
@ -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());
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
BTCPayServer/Eclair/AllChannelResponse.cs
Normal file
14
BTCPayServer/Eclair/AllChannelResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
21
BTCPayServer/Eclair/ChannelResponse.cs
Normal file
21
BTCPayServer/Eclair/ChannelResponse.cs
Normal 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";
|
||||
}
|
||||
}
|
230
BTCPayServer/Eclair/EclairRPCClient.cs
Normal file
230
BTCPayServer/Eclair/EclairRPCClient.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
18
BTCPayServer/Eclair/GetInfoResponse.cs
Normal file
18
BTCPayServer/Eclair/GetInfoResponse.cs
Normal 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; }
|
||||
}
|
||||
}
|
569
BTCPayServer/Eclair/LightMoney.cs
Normal file
569
BTCPayServer/Eclair/LightMoney.cs
Normal 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
|
||||
}
|
||||
}
|
24
BTCPayServer/Eclair/NodeInfo.cs
Normal file
24
BTCPayServer/Eclair/NodeInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
147
BTCPayServer/EventAggregator.cs
Normal file
147
BTCPayServer/EventAggregator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal file
17
BTCPayServer/Events/InvoiceDataChangedEvent.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Events/InvoicePaymentEvent.cs
Normal file
24
BTCPayServer/Events/InvoicePaymentEvent.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
30
BTCPayServer/Events/InvoiceStatusChangedEvent.cs
Normal file
30
BTCPayServer/Events/InvoiceStatusChangedEvent.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal file
24
BTCPayServer/Events/NBXplorerStateChangedEvent.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
15
BTCPayServer/Events/NewBlockEvent.cs
Normal file
15
BTCPayServer/Events/NewBlockEvent.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
20
BTCPayServer/Events/TxOutReceivedEvent.cs
Normal file
20
BTCPayServer/Events/TxOutReceivedEvent.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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 + "');");
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
45
BTCPayServer/Initializer.cs
Normal file
45
BTCPayServer/Initializer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
481
BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs
generated
Normal file
481
BTCPayServer/Migrations/20171221054550_AltcoinSupport.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
24
BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs
Normal file
24
BTCPayServer/Migrations/20171221054550_AltcoinSupport.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -92,7 +92,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public double Rate
|
||||
public decimal Rate
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
|
@ -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
|
||||
|
175
BTCPayServer/NBXplorerWaiter.cs
Normal file
175
BTCPayServer/NBXplorerWaiter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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}");
|
||||
}
|
||||
|
12
BTCPayServer/Services/Fees/IFeeProviderFactory.cs
Normal file
12
BTCPayServer/Services/Fees/IFeeProviderFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
43
BTCPayServer/Services/Rates/FallbackRateProvider.cs
Normal file
43
BTCPayServer/Services/Rates/FallbackRateProvider.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Validations
|
||||
{
|
||||
public class DerivationStrategyValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
var network = (Network)validationContext.GetService(typeof(Network));
|
||||
if (network == null)
|
||||
return new ValidationResult("No Network specified");
|
||||
try
|
||||
{
|
||||
new DerivationStrategyFactory(network).Parse((string)value);
|
||||
return ValidationResult.Success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ValidationResult(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -57,10 +57,10 @@
|
||||
<h2>Video tutorials</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/npFMOu6tTpA" frameborder="0" allowfullscreen></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/6rd8ZpLrz-4" frameborder="0" allowfullscreen></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,10 +122,18 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 ml-auto text-center">
|
||||
<a href="http://13.79.159.103:3000/">
|
||||
<a href="http://52.191.212.129:3000/">
|
||||
<img src="~/img/slack.png" height="100" />
|
||||
</a>
|
||||
<p><a href="http://13.79.159.103:3000/">On Slack</a></p>
|
||||
<p><a href="http://52.191.212.129:3000/">On Slack</a></p>
|
||||
</div>
|
||||
<div class="col-lg-4 mr-auto text-center">
|
||||
<a href="https://twitter.com/BtcpayServer">
|
||||
<img src="~/img/twitter.png" height="100" />
|
||||
</a>
|
||||
<p>
|
||||
<a href="https://twitter.com/BtcpayServer">On Twitter</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-4 mr-auto text-center">
|
||||
<a href="https://github.com/btcpayserver/btcpayserver">
|
||||
|
@ -12,21 +12,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>BTCPay Invoice</title>
|
||||
|
||||
<environment include="Development">
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
|
||||
</environment>
|
||||
<environment exclude="Development">
|
||||
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
|
||||
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
|
||||
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
|
||||
</environment>
|
||||
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.css" rel="stylesheet" />
|
||||
<link href="~/css/css.css" rel="stylesheet" type="text/css">
|
||||
<link href="~/css/normalizer.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript">
|
||||
@Model.ToSrvModel()
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
|
||||
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
|
||||
@ -102,7 +94,7 @@
|
||||
</div>
|
||||
<div class="order-details">
|
||||
<!---->
|
||||
<div class="single-item-order">
|
||||
<div class="single-item-order buyerTotalLine">
|
||||
<div class="single-item-order__left">
|
||||
<div class="single-item-order__left__name">
|
||||
{{ srvModel.storeName }}
|
||||
@ -113,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>
|
||||
|
@ -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">×</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">×</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">×</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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 |
BIN
BTCPayServer/wwwroot/img/btc_pay_BG_twitter.png
Normal file
BIN
BTCPayServer/wwwroot/img/btc_pay_BG_twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
BTCPayServer/wwwroot/img/twitter.png
Normal file
BIN
BTCPayServer/wwwroot/img/twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
@ -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");
|
||||
});
|
||||
|
||||
|
@ -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"]
|
||||
|
27
README.md
27
README.md
@ -1,22 +1,25 @@
|
||||
|
||||

|
||||
|
||||
[](https://hub.docker.com/r/nicolasdorier/btcpayserver/)
|
||||
[](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.
|
||||
|
Reference in New Issue
Block a user