Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
145e3bec83 | |||
563e931468 | |||
3113097c4f | |||
cdbbad1694 | |||
c9c2730409 | |||
310a9a6d59 | |||
1a1078782e | |||
73cb3dc4ee | |||
9eb36a8b40 | |||
6307aa8665 | |||
b9e8408db5 | |||
0879895678 | |||
a4ecf070b0 | |||
162d76206e | |||
5af14ef2ec | |||
7210eebeca | |||
25dbf6445f | |||
0828c60537 | |||
34deb17f3d | |||
06b02b8691 | |||
b7abc08c27 | |||
399ae2cd9e | |||
63fe0f6612 | |||
42d60ef84b | |||
1784c30787 | |||
ac8feceaf2 | |||
3d8c5195ae | |||
9a5259510b | |||
caecb26420 | |||
ecc8b3d9ed | |||
d313395751 | |||
9e698a8004 | |||
3c4c99ee42 | |||
d34ffc0d9a | |||
039303bfaa | |||
273cf1adc9 | |||
5feb520843 | |||
17e914778d | |||
db24ab792f | |||
42475ec7b7 | |||
4972f0ab7b | |||
07e13747cf | |||
2465eb7e36 | |||
4ddcd7a4c8 | |||
89d9658e82 | |||
66ecb32538 | |||
a22576da0a | |||
69bd888bab | |||
9b540273fc | |||
cfd083bed5 | |||
55c9314cdd | |||
448cc06a11 | |||
0780df4fd7 | |||
04174b7431 | |||
b7c58c2083 | |||
cd75fd6842 | |||
370951a3bd | |||
2c08b0137b | |||
1eee31e9f1 | |||
01cf579530 | |||
f72705935a | |||
a29ab6b6b0 | |||
4784518235 | |||
f8c88bd44f | |||
0d1d0d57f4 | |||
2bd1238668 | |||
d1fb51b412 | |||
279de1b869 | |||
ce9189caf8 | |||
431147784e | |||
0697b8bf86 | |||
d293bc3947 | |||
e634700913 | |||
ce81136c88 | |||
093ae39e61 | |||
cac58808f0 | |||
a063f10778 | |||
6cefd9c3e7 | |||
a0243fa569 | |||
789b9168ad | |||
7c29cb62ef | |||
f97173e9e7 | |||
8fc1b0c856 | |||
cabd7c4e64 | |||
f8540dc78c | |||
b03d271f85 | |||
cfbcf0947a | |||
fcfba7f5e1 | |||
f4f9fabfd3 | |||
75f4a39ef2 | |||
f9f4d93191 | |||
69050f7a56 | |||
1743919cd4 | |||
131328b42c | |||
ad3b605148 | |||
f32e225fa6 |
@ -5,6 +5,7 @@
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
@ -15,7 +16,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = new ChargeClient(new Uri(url), network);
|
||||
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
|
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
public class LndMockTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
226
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
226
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
// this depends for now on `docker-compose up devlnd`
|
||||
public class UnitTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public UnitTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
initializeEnvironment();
|
||||
|
||||
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
||||
|
||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
||||
}
|
||||
|
||||
private LndSwaggerClient MerchantLnd { get; set; }
|
||||
private LndInvoiceClient InvoiceClient { get; set; }
|
||||
|
||||
private LndSwaggerClient CustomerLnd { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task GetInfo()
|
||||
{
|
||||
var res = await InvoiceClient.GetInfo();
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice()
|
||||
{
|
||||
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice()
|
||||
{
|
||||
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
|
||||
|
||||
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play()
|
||||
{
|
||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
||||
var seq2 = seq.Slice(3);
|
||||
var pos = seq2.GetPosition(0);
|
||||
}
|
||||
|
||||
// integration tests
|
||||
[Fact]
|
||||
public async Task TestWaitListenInvoice()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
var waitToken = default(CancellationToken);
|
||||
var listener = await InvoiceClient.Listen(waitToken);
|
||||
var waitTask = listener.WaitInvoice(waitToken);
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await waitTask;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
||||
|
||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice2.BOLT11
|
||||
});
|
||||
|
||||
invoice = await waitTask2;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
||||
await Task.Delay(100);
|
||||
listener.Dispose();
|
||||
Assert.Throws<TaskCanceledException>(()=> waitTask3.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLndInvoiceAndPay()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
}
|
||||
|
||||
|
||||
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var merchantNodeAddress = new LnrpcLightningAddress
|
||||
{
|
||||
Pubkey = merchantInfo.NodeId,
|
||||
Host = "merchant_lnd:9735"
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if channel is pending generate blocks until confirmed
|
||||
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
|
||||
if (pendingResponse.Pending_open_channels?
|
||||
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
|
||||
{
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if channel is established
|
||||
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
|
||||
LnrpcChannel channelToMerchant = null;
|
||||
if (chanResponse != null && chanResponse.Channels != null)
|
||||
{
|
||||
channelToMerchant = chanResponse.Channels
|
||||
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (channelToMerchant == null)
|
||||
{
|
||||
// create new channel
|
||||
var isConnected = await CustomerLnd.ListPeersAsync();
|
||||
if (isConnected.Peers == null ||
|
||||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
|
||||
{
|
||||
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
|
||||
{
|
||||
Addr = merchantNodeAddress
|
||||
});
|
||||
}
|
||||
|
||||
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
|
||||
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
|
||||
var channelReq = new LnrpcOpenChannelRequest
|
||||
{
|
||||
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
|
||||
Node_pubkey_string = merchantInfo.NodeId
|
||||
};
|
||||
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// channel exists, return it
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
return channelToMerchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await InvoiceClient.GetInfo();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
private void initializeEnvironment()
|
||||
{
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -47,10 +49,12 @@ namespace BTCPayServer.Tests
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
|
||||
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
|
||||
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=https://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
@ -78,41 +82,52 @@ namespace BTCPayServer.Tests
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
||||
if (lndBackend == LightningConnectionType.LndREST)
|
||||
client = MerchantLnd.Client;
|
||||
|
||||
PrepareLightningAsync(client).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] SKIPPED_STATES =
|
||||
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PrepareLightningAsync()
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
|
||||
var peers = await CustomerLightningD.ListPeersAsync();
|
||||
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
|
||||
var channel = filteringToTargetedPeers
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !skippedStates.Contains(c.State ?? ""))
|
||||
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
case null:
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
|
||||
await CustomerLightningD.ConnectAsync(clightning);
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||
|
||||
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
|
||||
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
|
||||
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
@ -122,23 +137,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetInfoResponse> WaitLNSynched()
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
|
||||
await Task.WhenAll(synching);
|
||||
if (synching.All(c => c.Result != null))
|
||||
return synching[0].Result;
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
@ -152,8 +173,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
|
@ -117,7 +117,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
@ -126,15 +126,24 @@ namespace BTCPayServer.Tests
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
|
||||
: throw new NotSupportedException(connectionType.ToString()),
|
||||
ConnectionString = connectionString,
|
||||
SkipPortTest = true
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -321,7 +322,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
foreach(var test in new[]
|
||||
foreach (var test in new[]
|
||||
{
|
||||
(0.0005m, "$0.0005 (USD)"),
|
||||
(0.001m, "$0.001 (USD)"),
|
||||
@ -343,6 +344,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
@ -408,14 +410,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "test", "BTC").GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(storeController.ModelState.IsValid);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
// Make sure old connection string format does not work
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||
@ -427,106 +435,148 @@ namespace BTCPayServer.Tests
|
||||
public void CanParseLightningURL()
|
||||
{
|
||||
LightningConnectionString conn = null;
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
|
||||
Assert.Equal("tcp://test/a", conn.ToString());
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
|
||||
Assert.Equal("aaa", conn.Username);
|
||||
Assert.Equal("bbb", conn.Password);
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPayment2()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
tester.Start();
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.01m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||
});
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal(i == 0, conn.IsLegacy);
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("tcp://test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=tcp://test/a", conn.ToString());
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://aaa:bbb@test/a", conn.ToString());
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
|
||||
Assert.Equal("aaa", conn.Username);
|
||||
Assert.Equal("bbb", conn.Password);
|
||||
}
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
|
||||
}
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
|
||||
|
||||
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
|
||||
|
||||
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
|
||||
|
||||
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
|
||||
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
|
||||
|
||||
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
|
||||
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
|
||||
|
||||
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
|
||||
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
|
||||
Assert.Equal(conn2.ToString(), conn.ToString());
|
||||
Assert.Equal(lndUri, conn.ToString());
|
||||
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
|
||||
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
|
||||
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
|
||||
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
|
||||
|
||||
// AllowInsecure can be set to allow http
|
||||
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPayment()
|
||||
public void CanSendLightningPaymentCLightning()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentCharge()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
void ProcessLightningPayment(LightningConnectionType type)
|
||||
{
|
||||
// For easier debugging and testing
|
||||
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
||||
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
tester.PrepareLightning(type);
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.01m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||
});
|
||||
|
||||
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
|
||||
|
||||
Task.WaitAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||
@ -536,7 +586,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
|
||||
// TODO: If this parameter is less than 1 second we start having concurrency problems
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000));
|
||||
//
|
||||
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Price = 0.01m,
|
||||
@ -770,8 +823,30 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
|
||||
// Test request pairing code client side
|
||||
var storeController = user.GetController<StoresController>();
|
||||
storeController.CreateToken(new CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "test2",
|
||||
StoreId = user.StoreId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.NotNull(storeController.GeneratedPairingCode);
|
||||
|
||||
|
||||
var k = new Key();
|
||||
var bitpay = new Bitpay(k, tester.PayTester.ServerUri);
|
||||
bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait();
|
||||
Assert.True(bitpay.TestAccess(Facade.Merchant));
|
||||
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
|
||||
// Same with new instance
|
||||
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
|
||||
Assert.True(bitpay.TestAccess(Facade.Merchant));
|
||||
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
|
||||
|
||||
// Can generate API Key
|
||||
var repo = tester.PayTester.GetService<TokenRepository>();
|
||||
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
@ -1286,8 +1361,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.PointOfSale.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
|
48
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
48
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -17,9 +17,10 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
@ -44,6 +45,22 @@ services:
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.8
|
||||
@ -80,8 +97,13 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawtx=tcp://0.0.0.0:28332
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtxlock=tcp://0.0.0.0:28332
|
||||
zmqpubhashblock=tcp://0.0.0.0:28332
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
@ -89,7 +111,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.16-dev
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -112,7 +134,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.9
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -131,7 +153,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.14-dev
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -177,8 +199,59 @@ services:
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
customer_lnd_datadir:
|
||||
merchant_lnd_datadir:
|
||||
|
@ -44,6 +44,7 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
|
@ -27,7 +27,8 @@ namespace BTCPayServer
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
34
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "feathercoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FTC_X = FTC_BTC * BTC_X",
|
||||
"FTC_BTC = bittrex(FTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/feathercoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
33
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "ufo",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"UFO_X = UFO_BTC * BTC_X",
|
||||
"UFO_BTC = coinexchange(UFO_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/ufo.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -51,6 +51,9 @@ namespace BTCPayServer
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.31</Version>
|
||||
<Version>1.0.2.40</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -40,10 +40,10 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.10" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.26" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.20" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.10" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.12" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
|
||||
|
@ -77,11 +77,16 @@ namespace BTCPayServer.Configuration
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if(lightning.Length != 0)
|
||||
{
|
||||
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
|
||||
if(!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
|
||||
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
|
||||
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaron=abf239...;certthumbprint=2abdf302...'");
|
||||
}
|
||||
if(connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
@ -53,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
|
@ -134,7 +134,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdatePointOfSale));
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
@ -102,9 +102,9 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var appData = new AppData() { Id = id };
|
||||
appData.StoreDataId = selectedStore;
|
||||
appData.Name = vm.Name;
|
||||
@ -113,6 +113,9 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key)]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
|
@ -356,7 +356,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -535,8 +535,11 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -115,10 +115,11 @@ namespace BTCPayServer.Controllers
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
|
||||
await UpdateCLightningConnectionStringIfNeeded(store);
|
||||
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
@ -144,32 +145,26 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<string> invoiceLogs = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
foreach(var pair in fetchingByCurrencyPair)
|
||||
foreach (var pair in fetchingByCurrencyPair)
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
bool hasError = false;
|
||||
if(rateResult.Errors.Count != 0)
|
||||
invoiceLogs.Add($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
invoiceLogs.Add($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
hasError = true;
|
||||
invoiceLogs.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
if(rateResult.ExchangeExceptions.Count != 0)
|
||||
if (rateResult.ExchangeExceptions.Count != 0)
|
||||
{
|
||||
foreach(var ex in rateResult.ExchangeExceptions)
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
invoiceLogs.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
hasError = true;
|
||||
}
|
||||
if(hasError)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
|
||||
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,11 +180,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +192,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach (var error in paymentMethodErrors)
|
||||
foreach (var error in invoiceLogs)
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
}
|
||||
@ -207,13 +202,29 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, invoiceLogs, _NetworkProvider);
|
||||
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task UpdateCLightningConnectionStringIfNeeded(StoreData store)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
{
|
||||
var lightning = method.GetLightningUrl();
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
needUpdate = true;
|
||||
}
|
||||
}
|
||||
if(needUpdate)
|
||||
await _StoreRepository.UpdateStore(store);
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
@ -288,7 +288,7 @@ namespace BTCPayServer.Controllers
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
@ -320,7 +320,19 @@ namespace BTCPayServer.Controllers
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
@ -335,15 +347,15 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if(!strategy.Segwit)
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = _ExplorerProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach(var getTransactionAsync in getTransactionAsyncs)
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if(tx == null)
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
}
|
||||
@ -356,7 +368,7 @@ namespace BTCPayServer.Controllers
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
@ -377,7 +389,7 @@ namespace BTCPayServer.Controllers
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
|
||||
finally { hw.Dispose(); }
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
|
@ -26,16 +26,15 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
@ -74,33 +73,51 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.Url))
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
|
||||
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
var internalDomain = internalLightning.BaseUri?.DnsSafeHost;
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
isLocal;
|
||||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
|
||||
if (!isInternalNode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if(connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if(!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -110,6 +127,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
|
||||
if (command == "save")
|
||||
{
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
@ -121,7 +139,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
}
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
@ -135,7 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
|
||||
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -313,7 +313,7 @@ namespace BTCPayServer.Controllers
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
|
@ -121,7 +121,10 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
|
@ -6,6 +6,11 @@ using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -31,12 +36,56 @@ namespace BTCPayServer.Data
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
|
||||
{
|
||||
builder
|
||||
.Append("CREATE DATABASE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
|
||||
|
||||
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
|
||||
builder
|
||||
.Append(" TEMPLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
|
||||
|
||||
builder
|
||||
.Append(" LC_CTYPE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" LC_COLLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" ENCODING ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
|
||||
|
||||
if (operation.Tablespace != null)
|
||||
{
|
||||
builder
|
||||
.Append(" TABLESPACE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
|
||||
}
|
||||
|
||||
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
|
||||
|
||||
EndStatement(builder, suppressTransaction: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfigureBuilder(DbContextOptionsBuilder builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseSqlite(_ConnectionString);
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UseNpgsql(_ConnectionString);
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
|
@ -72,7 +72,7 @@ namespace BTCPayServer
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Encoders.Base58Check.DecodeData(parts[i]);
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
@ -80,7 +80,7 @@ namespace BTCPayServer
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
|
@ -8,24 +8,20 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ namespace BTCPayServer
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
@ -141,7 +141,7 @@ namespace BTCPayServer
|
||||
|
||||
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
|
||||
{
|
||||
foreach(var item in items)
|
||||
foreach (var item in items)
|
||||
{
|
||||
hashSet.Add(item);
|
||||
}
|
||||
@ -157,6 +157,15 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
var waiting = Task.Delay(-1, cancellationToken);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
|
@ -308,7 +308,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
// Awaiting this later help make sure invoices should arrive in order
|
||||
|
@ -66,10 +66,10 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
if(invoice.ExceptionStatus == "paidPartial")
|
||||
context.Events.Add(new InvoiceEvent(invoice, 2000, "invoice_expiredPaidPartial"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@ -84,7 +84,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@ -93,7 +93,7 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@ -139,14 +139,14 @@ namespace BTCPayServer.HostedServices
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -157,7 +157,7 @@ namespace BTCPayServer.HostedServices
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -249,13 +249,13 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
|
@ -117,7 +117,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
@ -137,6 +136,7 @@ namespace BTCPayServer.Hosting
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
|
@ -15,9 +15,14 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
public static void Configure(ILoggerFactory factory)
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
if (factory == null)
|
||||
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
|
||||
else
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
}
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
|
@ -9,8 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class LightningNodeViewModel
|
||||
{
|
||||
[Display(Name = "Lightning charge url")]
|
||||
public string Url
|
||||
[Display(Name = "Connection string")]
|
||||
public string ConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -161,7 +161,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
if(payment != null)
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
if (payment != null)
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
@ -346,10 +346,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, InvoiceEntity invoice, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
{
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
|
||||
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
@ -358,13 +358,13 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address.ToString();
|
||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
return invoice;
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
@ -4,6 +4,8 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -12,16 +14,38 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var uri = supportedPaymentMethod.GetLightningUrl();
|
||||
if (uri.ConnectionType == LightningConnectionType.Charge)
|
||||
return CreateClient(uri, network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
|
||||
{
|
||||
if (connString.ConnectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
|
||||
return new ChargeClient(connString.ToUri(true), network);
|
||||
}
|
||||
else if (uri.ConnectionType == LightningConnectionType.CLightning)
|
||||
else if (connString.ConnectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
|
||||
return new CLightningRPCClient(connString.ToUri(false), network);
|
||||
}
|
||||
else if (connString.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri)
|
||||
{
|
||||
Macaroon = connString.Macaroon,
|
||||
MacaroonFilePath = connString.MacaroonFilePath,
|
||||
CertificateThumbprint = connString.CertificateThumbprint,
|
||||
AllowInsecure = connString.AllowInsecure,
|
||||
}));
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)
|
||||
{
|
||||
if (!Payments.Lightning.LightningConnectionString.TryParse(connectionString, false, out var conn, out string error))
|
||||
throw new FormatException($"Invalid format ({error})");
|
||||
return Payments.Lightning.LightningClientFactory.CreateClient(conn, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public enum LightningConnectionType
|
||||
{
|
||||
Charge,
|
||||
CLightning
|
||||
CLightning,
|
||||
LndREST
|
||||
}
|
||||
public class LightningConnectionString
|
||||
{
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString)
|
||||
static Dictionary<string, LightningConnectionType> typeMapping;
|
||||
static Dictionary<LightningConnectionType, string> typeMappingReverse;
|
||||
static LightningConnectionString()
|
||||
{
|
||||
return TryParse(str, out connectionString, out var error);
|
||||
typeMapping = new Dictionary<string, LightningConnectionType>();
|
||||
typeMapping.Add("clightning", LightningConnectionType.CLightning);
|
||||
typeMapping.Add("charge", LightningConnectionType.Charge);
|
||||
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
|
||||
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
|
||||
foreach (var kv in typeMapping)
|
||||
{
|
||||
typeMappingReverse.Add(kv.Value, kv.Key);
|
||||
}
|
||||
}
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
|
||||
{
|
||||
return TryParse(str, supportLegacy, out connectionString, out var error);
|
||||
}
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
if (supportLegacy)
|
||||
{
|
||||
var parsed = TryParseLegacy(str, out connectionString, out error);
|
||||
if (!parsed)
|
||||
{
|
||||
parsed = TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
connectionString = null;
|
||||
error = null;
|
||||
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Dictionary<string, string> keyValues = new Dictionary<string, string>();
|
||||
foreach (var part in parts.Select(p => p.Trim()))
|
||||
{
|
||||
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
|
||||
if (idx == -1)
|
||||
{
|
||||
error = "The format of the connectionString should a list of key=value delimited by semicolon";
|
||||
return false;
|
||||
}
|
||||
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
|
||||
var value = part.Substring(idx + 1).Trim();
|
||||
if (keyValues.ContainsKey(key))
|
||||
{
|
||||
error = $"Duplicate key {key}";
|
||||
return false;
|
||||
}
|
||||
keyValues.Add(key, value);
|
||||
}
|
||||
|
||||
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
|
||||
|
||||
LightningConnectionString result = new LightningConnectionString();
|
||||
var type = Take(keyValues, "type");
|
||||
if (type == null)
|
||||
{
|
||||
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
|
||||
{
|
||||
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
result.ConnectionType = connectionType;
|
||||
|
||||
switch (connectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
var apiToken = Take(keyValues, "api-token");
|
||||
if (apiToken == null)
|
||||
{
|
||||
error = "The key 'api-token' is not found";
|
||||
return false;
|
||||
}
|
||||
result.Username = "api-token";
|
||||
result.Password = apiToken;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:" + str;
|
||||
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:/" + str;
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for lnd connection strings";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
|
||||
var macaroon = Take(keyValues, "macaroon");
|
||||
if (macaroon != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Macaroon = Encoder.DecodeData(macaroon);
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'macaroon' format should be in hex";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
|
||||
if (macaroonFilePath != null)
|
||||
{
|
||||
if(macaroon != null)
|
||||
{
|
||||
error = $"The key 'macaroon' is already specified";
|
||||
return false;
|
||||
}
|
||||
if(!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
|
||||
return false;
|
||||
}
|
||||
result.MacaroonFilePath = macaroonFilePath;
|
||||
}
|
||||
|
||||
string securitySet = null;
|
||||
var certthumbprint = Take(keyValues, "certthumbprint");
|
||||
if (certthumbprint != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
if (bytes.Length != 32)
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
|
||||
return false;
|
||||
}
|
||||
result.CertificateThumbprint = bytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
|
||||
return false;
|
||||
}
|
||||
securitySet = "certthumbprint";
|
||||
}
|
||||
|
||||
var allowinsecureStr = Take(keyValues, "allowinsecure");
|
||||
|
||||
if (allowinsecureStr != null)
|
||||
{
|
||||
var allowedValues = new[] { "true", "false" };
|
||||
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = $"The key 'allowinsecure' should be true or false";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
if (securitySet != null && allowInsecure)
|
||||
{
|
||||
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
|
||||
return false;
|
||||
}
|
||||
result.AllowInsecure = allowInsecure;
|
||||
}
|
||||
|
||||
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
|
||||
{
|
||||
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
}
|
||||
|
||||
if (keyValues.Count != 0)
|
||||
{
|
||||
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
private static string Take(Dictionary<string, string> keyValues, string key)
|
||||
{
|
||||
if (keyValues.TryGetValue(key, out var v))
|
||||
keyValues.Remove(key);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str.StartsWith('/'))
|
||||
str = "unix:" + str;
|
||||
var result = new LightningConnectionString();
|
||||
@ -27,7 +290,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
error = null;
|
||||
|
||||
Uri uri;
|
||||
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
{
|
||||
error = "Invalid URL";
|
||||
return false;
|
||||
@ -40,7 +303,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
error = $"The url support the following protocols {protocols}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "unix")
|
||||
{
|
||||
str = uri.AbsoluteUri.Substring("unix:".Length);
|
||||
@ -49,8 +311,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
str = str.Substring(1);
|
||||
}
|
||||
uri = new Uri("unix://" + str, UriKind.Absolute);
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "tcp")
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
|
||||
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||||
{
|
||||
var parts = uri.UserInfo.Split(':');
|
||||
@ -61,6 +327,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
result.ConnectionType = LightningConnectionType.Charge;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
@ -68,6 +335,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
result.IsLegacy = true;
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
@ -80,15 +348,17 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public Uri BaseUri { get; set; }
|
||||
public bool IsLegacy { get; private set; }
|
||||
|
||||
public LightningConnectionType ConnectionType
|
||||
{
|
||||
get
|
||||
{
|
||||
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
|
||||
: LightningConnectionType.CLightning;
|
||||
}
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
|
||||
public Uri ToUri(bool withCredentials)
|
||||
{
|
||||
@ -101,10 +371,57 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return BaseUri;
|
||||
}
|
||||
}
|
||||
|
||||
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
|
||||
public override string ToString()
|
||||
{
|
||||
return ToUri(true).AbsoluteUri;
|
||||
var type = typeMappingReverse[ConnectionType];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append($"type={type}");
|
||||
switch (ConnectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
if (Username == null || Username == "api-token")
|
||||
{
|
||||
builder.Append($";server={BaseUri};api-token={Password}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
builder.Append($";server={BaseUri}");
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
if (Username == null)
|
||||
{
|
||||
builder.Append($";server={BaseUri}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
if (Macaroon != null)
|
||||
{
|
||||
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
|
||||
}
|
||||
if (MacaroonFilePath != null)
|
||||
{
|
||||
builder.Append($";macaroonfilepath={MacaroonFilePath}");
|
||||
}
|
||||
if (CertificateThumbprint != null)
|
||||
{
|
||||
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
|
||||
}
|
||||
if (AllowInsecure)
|
||||
{
|
||||
builder.Append($";allowinsecure=true");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(type);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
|
||||
{
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningLikePaymentHandler(
|
||||
@ -41,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
using (var cts = new CancellationTokenSource(5000))
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -70,7 +72,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
||||
using (var cts = new CancellationTokenSource(5000))
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
LightningNodeInformation info = null;
|
||||
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
await EnsureListening(inv.InvoiceId, false);
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
}
|
||||
}));
|
||||
|
||||
@ -189,8 +189,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
BOLT11 = notification.BOLT11,
|
||||
Amount = notification.Amount
|
||||
}, network.CryptoCode, accounted: true);
|
||||
if(payment != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
||||
if(invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> _ListeningLightning = new List<Task>();
|
||||
|
@ -8,18 +8,41 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Username { get; set; }
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Password { get; set; }
|
||||
|
||||
// This property MUST be after CryptoCode or else JSON serialization fails
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string LightningChargeUrl { get; set; }
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string LightningConnectionString { get; set; }
|
||||
|
||||
public LightningConnectionString GetLightningUrl()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
|
||||
if (!string.IsNullOrEmpty(LightningConnectionString))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
return connectionString;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
@ -29,16 +52,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Username = connectionString.Username;
|
||||
Password = connectionString.Password;
|
||||
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
|
||||
LightningConnectionString = connectionString.ToString();
|
||||
Username = null;
|
||||
Password = null;
|
||||
LightningChargeUrl = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Username { get; set; }
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Password { get; set; }
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||
}
|
||||
}
|
||||
|
67
BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs
Normal file
67
BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public abstract class LndAuthentication
|
||||
{
|
||||
public class FixedMacaroonAuthentication : LndAuthentication
|
||||
{
|
||||
public FixedMacaroonAuthentication(byte[] macaroon)
|
||||
{
|
||||
if (macaroon == null)
|
||||
throw new ArgumentNullException(nameof(macaroon));
|
||||
Macaroon = macaroon;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon));
|
||||
}
|
||||
}
|
||||
public class NullAuthentication : LndAuthentication
|
||||
{
|
||||
public static NullAuthentication Instance { get; } = new NullAuthentication();
|
||||
|
||||
private NullAuthentication()
|
||||
{
|
||||
|
||||
}
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class MacaroonFileAuthentication : LndAuthentication
|
||||
{
|
||||
public MacaroonFileAuthentication(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
// Because this dump the whole file, let's make sure it is indeed the macaroon
|
||||
if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath));
|
||||
FilePath = filePath;
|
||||
}
|
||||
public string FilePath { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(FilePath);
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void AddAuthentication(HttpRequestMessage httpRequest);
|
||||
}
|
||||
}
|
227
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
227
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndInvoiceClient : ILightningInvoiceClient
|
||||
{
|
||||
class LndInvoiceClientSession : ILightningListenInvoiceSession
|
||||
{
|
||||
private LndSwaggerClient _Parent;
|
||||
Channel<LightningInvoice> _Invoices = Channel.CreateBounded<LightningInvoice>(50);
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
ManualResetEventSlim _Stopped = new ManualResetEventSlim(false);
|
||||
|
||||
public LndInvoiceClientSession(LndSwaggerClient parent)
|
||||
{
|
||||
_Parent = parent;
|
||||
}
|
||||
|
||||
public async void StartListening()
|
||||
{
|
||||
var urlBuilder = new StringBuilder();
|
||||
urlBuilder.Append(_Parent.BaseUrl).Append("/v1/invoices/subscribe");
|
||||
try
|
||||
{
|
||||
using (var client = _Parent.CreateHttpClient())
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
|
||||
|
||||
using (var response = await client.SendAsync(
|
||||
request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token))
|
||||
{
|
||||
using (var body = await response.Content.ReadAsStreamAsync())
|
||||
using (var reader = new StreamReader(body))
|
||||
{
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
string line = await reader.ReadLineAsync().WithCancellation(_Cts.Token);
|
||||
if (line != null && line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var invoiceString = JObject.Parse(line)["result"].ToString();
|
||||
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
|
||||
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Stopped.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _Invoices.Reader.ReadAsync(cancellation);
|
||||
}
|
||||
catch (ChannelClosedException)
|
||||
{
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cts.Cancel();
|
||||
_Stopped.Wait();
|
||||
_Invoices.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LndSwaggerClient _rpcClient;
|
||||
|
||||
public LndInvoiceClient(LndSwaggerClient swaggerClient)
|
||||
{
|
||||
if (swaggerClient == null)
|
||||
throw new ArgumentNullException(nameof(swaggerClient));
|
||||
_rpcClient = swaggerClient;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var strAmount = ConvertInv.ToString(amount.ToUnit(LightMoneyUnit.Satoshi));
|
||||
var strExpiry = ConvertInv.ToString(Math.Round(expiry.TotalSeconds, 0));
|
||||
// lnd requires numbers sent as strings. don't ask
|
||||
var resp = await _rpcClient.AddInvoiceAsync(new LnrpcInvoice
|
||||
{
|
||||
Value = strAmount,
|
||||
Memo = description,
|
||||
Expiry = strExpiry
|
||||
});
|
||||
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = amount,
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.GetInfoAsync(cancellation);
|
||||
|
||||
var nodeInfo = new LightningNodeInformation
|
||||
{
|
||||
BlockHeight = (int?)resp.Block_height ?? 0,
|
||||
NodeId = resp.Identity_pubkey
|
||||
};
|
||||
|
||||
|
||||
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
|
||||
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
|
||||
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
|
||||
|
||||
var firstNodeInfo = node.Node.Addresses.First();
|
||||
var externalHostPort = firstNodeInfo.Addr.Split(':');
|
||||
|
||||
nodeInfo.Address = externalHostPort[0];
|
||||
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
|
||||
return ConvertLndInvoice(resp);
|
||||
}
|
||||
|
||||
public Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var session = new LndInvoiceClientSession(this._rpcClient);
|
||||
session.StartListening();
|
||||
return Task.FromResult<ILightningListenInvoiceSession>(session);
|
||||
}
|
||||
|
||||
internal static LightningInvoice ConvertLndInvoice(LnrpcInvoice resp)
|
||||
{
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
// TODO: Verify id corresponds to R_hash
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = new LightMoney(ConvertInv.ToInt64(resp.Value), LightMoneyUnit.Satoshi),
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
|
||||
if (resp.Settle_date != null)
|
||||
{
|
||||
invoice.PaidAt = DateTimeOffset.FromUnixTimeSeconds(ConvertInv.ToInt64(resp.Settle_date));
|
||||
invoice.Status = "paid";
|
||||
}
|
||||
else
|
||||
{
|
||||
var invoiceExpiry = ConvertInv.ToInt64(resp.Creation_date) + ConvertInv.ToInt64(resp.Expiry);
|
||||
if (DateTimeOffset.FromUnixTimeSeconds(invoiceExpiry) > DateTimeOffset.UtcNow)
|
||||
{
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
|
||||
// utility static methods... maybe move to separate class
|
||||
private static string BitString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes)
|
||||
.Replace("-", "", StringComparison.InvariantCulture)
|
||||
.ToLower(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Invariant culture conversion
|
||||
public static class ConvertInv
|
||||
{
|
||||
public static int ToInt32(string str)
|
||||
{
|
||||
return Convert.ToInt32(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static long ToInt64(string str)
|
||||
{
|
||||
return Convert.ToInt64(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static string ToString(decimal d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string ToString(double d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs
Normal file
36
BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndRestSettings
|
||||
{
|
||||
public LndRestSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public LndRestSettings(Uri uri)
|
||||
{
|
||||
Uri = uri;
|
||||
}
|
||||
public Uri Uri { get; set; }
|
||||
/// <summary>
|
||||
/// The SHA256 of the PEM certificate
|
||||
/// </summary>
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
|
||||
public LndAuthentication CreateLndAuthentication()
|
||||
{
|
||||
if (Macaroon != null)
|
||||
return new LndAuthentication.FixedMacaroonAuthentication(Macaroon);
|
||||
if (!string.IsNullOrEmpty(MacaroonFilePath))
|
||||
return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath);
|
||||
return LndAuthentication.NullAuthentication.Instance;
|
||||
}
|
||||
}
|
||||
}
|
8713
BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.cs
Normal file
8713
BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public partial class LndSwaggerClient
|
||||
{
|
||||
public LndSwaggerClient(LndRestSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
_LndSettings = settings;
|
||||
_Authentication = settings.CreateLndAuthentication();
|
||||
BaseUrl = settings.Uri.AbsoluteUri.TrimEnd('/');
|
||||
_httpClient = CreateHttpClient(settings);
|
||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
|
||||
{
|
||||
var json = new Newtonsoft.Json.JsonSerializerSettings();
|
||||
UpdateJsonSerializerSettings(json);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
LndRestSettings _LndSettings;
|
||||
LndAuthentication _Authentication;
|
||||
|
||||
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
|
||||
{
|
||||
_Authentication.AddAuthentication(request);
|
||||
}
|
||||
|
||||
internal static HttpClient CreateHttpClient(LndRestSettings settings)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
SslProtocols = SslProtocols.Tls12
|
||||
};
|
||||
|
||||
var expectedThumbprint = settings.CertificateThumbprint?.ToArray();
|
||||
if (expectedThumbprint != null)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
|
||||
{
|
||||
var actualCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
|
||||
var hash = actualCert.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
return hash.SequenceEqual(expectedThumbprint);
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.AllowInsecure)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (settings.Uri.Scheme == "http")
|
||||
throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https");
|
||||
}
|
||||
return new HttpClient(handler);
|
||||
}
|
||||
|
||||
internal HttpClient CreateHttpClient()
|
||||
{
|
||||
return LndSwaggerClient.CreateHttpClient(_LndSettings);
|
||||
}
|
||||
|
||||
internal T Deserialize<T>(string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(str, _settings.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,9 +31,14 @@ namespace BTCPayServer
|
||||
var logger = loggerFactory.CreateLogger("Configuration");
|
||||
try
|
||||
{
|
||||
// This is the only way toat LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
|
||||
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
|
||||
if (conf == null)
|
||||
return;
|
||||
Logs.Configure(loggerFactory);
|
||||
new BTCPayServerOptions().LoadArgs(conf);
|
||||
Logs.Configure(null);
|
||||
/////
|
||||
|
||||
host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
@ -64,6 +69,8 @@ namespace BTCPayServer
|
||||
finally
|
||||
{
|
||||
processor.Dispose();
|
||||
if(host == null)
|
||||
Logs.Configuration.LogError("Configuration error");
|
||||
if (host != null)
|
||||
host.Dispose();
|
||||
loggerProvider.Dispose();
|
||||
|
247
BTCPayServer/Security/BitpayAuthentication.cs
Normal file
247
BTCPayServer/Security/BitpayAuthentication.cs
Normal file
@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayAuthentication
|
||||
{
|
||||
public class BitpayAuthOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
|
||||
}
|
||||
class BitpayAuthHandler : AuthenticationHandler<BitpayAuthOptions>
|
||||
{
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
public BitpayAuthHandler(
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
IOptionsMonitor<BitpayAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
_TokenRepository = tokenRepository;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (Context.Request.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
// Careful, those are not the opposite. failedAuth says if a the tentative failed.
|
||||
// successAuth, ensure that at least one succeed.
|
||||
var failedAuth = false;
|
||||
var successAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
|
||||
storeId = result.StoreId;
|
||||
failedAuth = !result.SuccessAuth;
|
||||
successAuth = result.SuccessAuth;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
successAuth = storeId != null;
|
||||
}
|
||||
|
||||
if (failedAuth)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
}
|
||||
|
||||
if (successAuth)
|
||||
{
|
||||
if (storeId != null)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
}
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List<Claim> claims)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
claims.Add(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return (null, false);
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return (storeId, true);
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||
{
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) => { });
|
||||
services.AddAuthentication().AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
|
||||
public BitpayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_TokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BitpayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var bitpayAuth = context.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
|
||||
{
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
context.HttpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Invalid credentials");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return storeId;
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string BitpayAuthentication = "Bitpay.Auth";
|
||||
public const string CookieAuthentication = "Identity.Application";
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
|
@ -20,9 +20,9 @@ namespace BTCPayServer.Services
|
||||
public HardwareWalletException(string message) : base(message) { }
|
||||
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
public class HardwareWalletService
|
||||
public class HardwareWalletService : IDisposable
|
||||
{
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
||||
{
|
||||
private readonly WebSocket webSocket;
|
||||
|
||||
@ -33,26 +33,40 @@ namespace BTCPayServer.Services
|
||||
this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public async Task<byte[][]> Exchange(byte[][] apdus)
|
||||
{
|
||||
await _Semaphore.WaitAsync();
|
||||
List<byte[]> responses = new List<byte[]>();
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||
try
|
||||
{
|
||||
foreach (var apdu in apdus)
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Semaphore.Release();
|
||||
}
|
||||
return responses.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LedgerClient _Ledger;
|
||||
@ -121,7 +135,7 @@ namespace BTCPayServer.Services
|
||||
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||
{
|
||||
List<KeyPath> derivations = new List<KeyPath>();
|
||||
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
derivations.Add(new KeyPath("49'"));
|
||||
derivations.Add(new KeyPath("44'"));
|
||||
KeyPath foundKeyPath = null;
|
||||
@ -155,6 +169,12 @@ namespace BTCPayServer.Services
|
||||
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
||||
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Transport != null)
|
||||
_Transport.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class LedgerTestResult
|
||||
|
@ -85,7 +85,8 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
JToken bid = p.Value["bid"];
|
||||
JToken ask = p.Value["ask"];
|
||||
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
|
||||
if (bid == null || ask == null ||
|
||||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
|
||||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
|
||||
v1 > v2 ||
|
||||
v1 <= 0 || v2 <= 0)
|
||||
|
@ -10,8 +10,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Some of your nodes are still synching...<br />
|
||||
BTCPay Server will not work correctly until it is over.
|
||||
Some of your nodes are still synchronizing...<br />
|
||||
BTCPay Server will not create invoices with those cryptocurrencies.
|
||||
</p>
|
||||
@foreach (var line in dashboard.GetAll())
|
||||
{
|
||||
@ -47,12 +47,12 @@
|
||||
}
|
||||
else if (line.Status.BitcoinStatus.IsSynched)
|
||||
{
|
||||
<li>The node is synched (Height: @line.Status.BitcoinStatus.Headers)</li>
|
||||
<li>The node is synchronized (Height: @line.Status.BitcoinStatus.Headers)</li>
|
||||
@if (line.Status.BitcoinStatus.IsSynched &&
|
||||
line.Status.SyncHeight.HasValue &&
|
||||
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
|
||||
{
|
||||
<li>NBXplorer is synching... (Height: @line.Status.SyncHeight.Value)</li>
|
||||
<li>NBXplorer is synchronizing... (Height: @line.Status.SyncHeight.Value)</li>
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -36,9 +36,9 @@
|
||||
<span>This URL should point to an installed lightning charge server for @Model.CryptoCode</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Url"></label>
|
||||
<input id="lightningurl" asp-for="Url" class="form-control" />
|
||||
<span asp-validation-for="Url" class="text-danger"></span>
|
||||
<label asp-for="ConnectionString"></label>
|
||||
<input id="lightningurl" asp-for="ConnectionString" class="form-control" />
|
||||
<span asp-validation-for="ConnectionString" class="text-danger"></span>
|
||||
@if(Model.InternalLightningNode != null)
|
||||
{
|
||||
<p class="form-text text-muted">
|
||||
|
@ -17,7 +17,7 @@
|
||||
If your Ledger wallet is not detected:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Make sure you are running the Ledger Bitcoin or Litecoin app with version superior or equal to 1.2.4</li>
|
||||
<li>Make sure you are running the Ledger app with version superior or equal to 1.2.4</li>
|
||||
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
|
||||
</ul>
|
||||
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||
|
@ -30,6 +30,7 @@ function onDataCallback(jsonData) {
|
||||
newStatus === "paid") {
|
||||
if ($(".modal-dialog").hasClass("expired")) {
|
||||
$(".modal-dialog").removeClass("expired");
|
||||
$("#expired").removeClass("active");
|
||||
}
|
||||
|
||||
$(".modal-dialog").addClass("paid");
|
||||
@ -39,6 +40,11 @@ function onDataCallback(jsonData) {
|
||||
}
|
||||
|
||||
if (newStatus === "expired" || newStatus === "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
|
||||
if ($(".modal-dialog").hasClass("paid")) {
|
||||
$(".modal-dialog").removeClass("paid");
|
||||
$("#paid").removeClass("active");
|
||||
}
|
||||
|
||||
$(".timer-row").removeClass("expiring-soon");
|
||||
$(".timer-row__spinner").html("");
|
||||
$("#emailAddressView").removeClass("active");
|
||||
|
@ -35,8 +35,8 @@ const locales_de = {
|
||||
"What happened?": "Was ist passiert?",
|
||||
"InvoiceExpired_Body_1": "Diese Rechnung ist abgelaufen. Eine Rechnung ist nur für {{maxTimeMinutes}} Minuten gültig. \
|
||||
Sie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden möchten.",
|
||||
"InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Bitcoin-Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.",
|
||||
"InvoiceExpired_Body_3": "Wenn die Transaktion vom Bitcoin-Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Wallet verfügbar. Abhängig von Ihrer Wallet, kann dies 48-72 Stunden dauern.",
|
||||
"InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.",
|
||||
"InvoiceExpired_Body_3": "Wenn die Transaktion vom Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Wallet verfügbar. Abhängig von Ihrer Wallet, kann dies 48-72 Stunden dauern.",
|
||||
"Invoice ID": "Rechnungs ID",
|
||||
"Order ID": "Auftrag ID",
|
||||
"Return to StoreName": "Zurück zu {{storeName}}",
|
||||
|
@ -35,8 +35,8 @@ const locales_en = {
|
||||
"What happened?": "What happened?",
|
||||
"InvoiceExpired_Body_1": "This invoice has expired. An invoice is only valid for {{maxTimeMinutes}} minutes. \
|
||||
You can return to {{storeName}} if you would like to submit your payment again.",
|
||||
"InvoiceExpired_Body_2": "If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.",
|
||||
"InvoiceExpired_Body_3": "If the transaction is not accepted by the Bitcoin network, the funds will be spendable again in your wallet. Depending on your wallet, this may take 48-72 hours.",
|
||||
"InvoiceExpired_Body_2": "If you tried to send a payment, it has not yet been accepted by the network. We have not yet received your funds.",
|
||||
"InvoiceExpired_Body_3": "If the transaction is not accepted by the network, the funds will be spendable again in your wallet. Depending on your wallet, this may take 48-72 hours.",
|
||||
"Invoice ID": "Invoice ID",
|
||||
"Order ID": "Order ID",
|
||||
"Return to StoreName": "Return to {{storeName}}",
|
||||
|
@ -26,7 +26,7 @@ const locales_is = {
|
||||
"Copied": "Afritað",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Þú getur borgað {{btcDue}} {{cryptoCode}} með altcoins.",
|
||||
"ConversionTab_BodyDesc": "Þessi þjónusta er veitt af þriðja aðila. Mundu að við höfum ekki stjórn á því hvað þeir gera við peningana. Reikningurinn verður aðeins móttekinn þegar {{cryptoCode}} greiðslan hefur verið staðfest á Bitcoin netinu.",
|
||||
"ConversionTab_BodyDesc": "Þessi þjónusta er veitt af þriðja aðila. Mundu að við höfum ekki stjórn á því hvað þeir gera við peningana. Reikningurinn verður aðeins móttekinn þegar {{cryptoCode}} greiðslan hefur verið staðfest á netinu.",
|
||||
"Shapeshift_Button_Text": "Borga með Altcoins",
|
||||
"ConversionTab_Lightning": "Engir viðskiptaveitendur eru í boði fyrir Lightning Network greiðslur.",
|
||||
// Invoice expired
|
||||
@ -36,7 +36,7 @@ const locales_is = {
|
||||
"InvoiceExpired_Body_1": "Þessi reikningur er útrunnin. Reikningurinn er aðeins gildur í {{maxTimeMinutes}} mínútur. \
|
||||
Þú getur farið aftur á {{storeName}} ef þú vilt reyna aftur.",
|
||||
"InvoiceExpired_Body_2": "Ef þú reyndir að senda greiðslu, þá hefur hún ekki verið samþykkt.",
|
||||
"InvoiceExpired_Body_3": "Ef viðskiptin eru ekki samþykkt af Bitcoin netinu verða fjármunirnir aðgengilegar aftur í veskinu þínu. Það fer eftir veskinu þínu og getur tekið 48-72 klukkustundir.",
|
||||
"InvoiceExpired_Body_3": "Ef viðskiptin eru ekki samþykkt af netinu verða fjármunirnir aðgengilegar aftur í veskinu þínu. Það fer eftir veskinu þínu og getur tekið 48-72 klukkustundir.",
|
||||
"Invoice ID": "Innheimtu ID",
|
||||
"Order ID": "Pöntun ID",
|
||||
"Return to StoreName": "Fara aftur á {{storeName}}",
|
||||
|
BIN
BTCPayServer/wwwroot/imlegacy/feathercoin.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/feathercoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
BIN
BTCPayServer/wwwroot/imlegacy/groestlcoin.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/groestlcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
BIN
BTCPayServer/wwwroot/imlegacy/ufo.png
Normal file
BIN
BTCPayServer/wwwroot/imlegacy/ufo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
15
README.md
15
README.md
@ -20,6 +20,19 @@ This solution is for you if:
|
||||
* You want to become a payment processor yourself and offer a BTCPay hosted solution to merchants
|
||||
* You want a way to support currencies other than those offered by Bitpay
|
||||
|
||||
## We support altcoins!
|
||||
|
||||
In addition to Bitcoin, we support the following crypto currencies:
|
||||
|
||||
* BGold
|
||||
* Dogecoin
|
||||
* Feathercoin
|
||||
* Groestlcoin
|
||||
* Litecoin
|
||||
* Monacoin
|
||||
* Polis
|
||||
* UFO
|
||||
|
||||
## Documentation
|
||||
|
||||
Please check out our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) for more details.
|
||||
@ -30,7 +43,7 @@ You can also checkout [The Merchants Guide to accepting Bitcoin directly with no
|
||||
|
||||
While the documentation advise using docker-compose, you may want to build yourself outside of development purpose.
|
||||
|
||||
First install .NET Core SDK 2.1 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/sdk-2.1.300-rc1).
|
||||
First install .NET Core SDK 2.1 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core).
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
|
Reference in New Issue
Block a user