Compare commits

..

63 Commits

Author SHA1 Message Date
b750663a1f update lnd/clightning 2018-09-28 17:08:51 +09:00
4c4b76e995 Remove Bitpay direct provider 2018-09-28 17:08:51 +09:00
da19d2c1a7 Bugfixing display of custom amount entry in POS (#302)
Ref: #293
2018-09-28 13:31:59 +09:00
fb15c5b354 Increase timeout for wallet, update references 2018-09-28 10:15:35 +09:00
6ffe1cfcab bump LedgerWallet lib 2018-09-27 16:31:33 +09:00
87678c58ac Fix error message for ledger signing 2018-09-27 15:43:34 +09:00
feab4cc48a update ledgerwebsocket 2018-09-27 15:21:26 +09:00
712946f512 bump 2018-09-22 05:16:24 -05:00
a7bfceae05 Reverting to 2.1.0 until we update docker images 2018-09-22 05:14:36 -05:00
8a26cd549a bump 2018-09-22 01:11:14 -05:00
1cf3ce0617 Footer with server version now visible only for logged in users
Per #282
2018-09-22 01:11:14 -05:00
73c65fada2 Fixing display on Lockout page 2018-09-22 01:11:14 -05:00
92ea923c03 Updating reference to Bitcoin docker image 2018-09-22 01:11:14 -05:00
e7db453717 Removing network fee line item if fee is 0
Per discussion in #265
2018-09-22 01:11:14 -05:00
10ee09f052 Bugfixing broken link in footer
Div overlay caused it, fixes #269
2018-09-22 01:11:14 -05:00
be7d91a138 VueJs cloak until loading is finished
Addressing #255
2018-09-22 01:11:14 -05:00
3278c80d3f Fixing problem with View dynamic recompiling
The type 'RazorViewAttribute' exists in both 'Microsoft.AspNetCore.Mvc.Razor' error resolved
2018-09-22 01:11:14 -05:00
65e1edb0b8 Merge pull request #274 from Kukks/feature/lockout
enable account lockout
2018-09-22 00:42:33 -05:00
e05c88370f enable account lockout 2018-09-12 13:36:44 +02:00
15c29f8419 Update dependencies 2018-09-09 23:04:16 +09:00
fc722731d3 bump 2018-09-08 14:54:23 +09:00
1c9c564e90 Merge branch 'rockstardev-master' 2018-09-08 14:54:06 +09:00
872b60f8ea Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-08 14:53:53 +09:00
0d3364b3da Change button path to api/v1/invoices 2018-09-08 14:53:42 +09:00
fed53661b3 Add btcpay.store.cancreateinvoice claim, and use that for the store 2018-09-08 14:53:41 +09:00
e86b4d89ca remove paybuttontest 2018-09-08 14:53:41 +09:00
c5cb32f6dd Providing option to disable Pay Button at later date 2018-09-08 14:53:41 +09:00
deb56e16ec Confirmation page for enabling Pay Button 2018-09-08 14:53:41 +09:00
b5626ef01c Validating that Store has Pay Button enabled 2018-09-08 14:53:41 +09:00
e39d9067f2 Updating Unit tests 2018-09-08 14:53:41 +09:00
43d34d5d35 Now requiring Authorization on AppsController 2018-09-08 14:53:41 +09:00
7341be76bb Extracting public portion of app controller 2018-09-08 14:53:41 +09:00
0abd62dfe8 Moving PayButton handler to public controller 2018-09-08 14:53:41 +09:00
735012e3d7 Refactoring Invoice to cleanup unused code 2018-09-08 14:53:41 +09:00
b5efb8d2e6 Add exchange name to expired rate 2018-09-08 14:53:40 +09:00
1dbeabb716 Update broken link of third-party host (#271) 2018-09-08 14:05:37 +09:00
671f9e56e2 Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-09-04 19:04:47 +09:00
dc6c189948 Update broken link (#264)
Current link goes to a 404 page, updated to current .NET Core SDK 2.1 page
2018-09-03 22:04:14 +09:00
4501824f3f Updating Readme (#262)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

reduced merchant guide link for better readability

* Update README.md

* Update README.md

* Update README.md
2018-09-03 22:03:50 +09:00
4568d2a98e Add exchange name to expired rate 2018-08-31 10:45:21 +09:00
f5d81334f8 Remove Lightning Specific logic from BTCPay, and use BTCPayServer.Lightning packages instead 2018-08-30 12:24:00 +09:00
f3ed90399b Merge pull request #257 from dalijolijo/master
Bring supported coins in alphabetic order
2018-08-30 10:45:01 +09:00
fada01cec9 Bring supported coins in alphabetic order 2018-08-30 01:07:30 +02:00
1b4b9fb4cc bump 2018-08-29 00:31:39 +09:00
6eeef8a866 Remove XFrame on the checkout page 2018-08-29 00:31:23 +09:00
24979a0af2 add lnd trickledelay 2018-08-28 18:13:28 +09:00
be1a44f018 Fix bug in LND client 2018-08-28 18:06:07 +09:00
9fcc2903fc Revert "Disable color in logs"
This reverts commit 06df63b2835d77a83c8897368f91491e008f57b5.
2018-08-28 16:13:26 +09:00
06df63b283 Disable color in logs 2018-08-28 09:56:17 +09:00
0f1efc16f5 fx build 2018-08-27 18:45:05 +09:00
da8a06952c Fix bug about btcpay showing wrong port 0 as public lightning port for charge and clightning 2018-08-27 18:36:08 +09:00
38d810cef7 Fix bundling 2018-08-25 23:08:46 +09:00
393a3a2b8f Remove obsolete code in checkout 2018-08-25 22:50:17 +09:00
aaddc580d1 bump 2018-08-25 21:49:41 +09:00
957d478865 Fixing coinaverage, ValidityTime was faster than the refresh rate 2018-08-25 21:49:20 +09:00
023913a852 Rate limit per IP the number of login attempt 2018-08-25 20:28:46 +09:00
6c51d83f61 Fix tests 2018-08-25 15:49:04 +09:00
0edaedb6ab Report if BackgroundFetcherRateProvider has expired entry 2018-08-25 15:09:42 +09:00
13f21aa0d6 bump 2018-08-25 14:48:06 +09:00
929a0c37bd Better handle errors on BackgroundFetcherRateProvider 2018-08-25 14:44:56 +09:00
058ccf56d0 Fix uncaught exception on when getting rates of invoice 2018-08-25 14:44:55 +09:00
162ac572da Merge pull request #252 from Onurrr/patch-1
Update Checkout.cshtml
2018-08-24 18:37:07 +09:00
c7f3fdb46d Update Checkout.cshtml 2018-08-23 13:11:07 +02:00
84 changed files with 785 additions and 12443 deletions

View File

@ -9,9 +9,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>

View File

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.Charge;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests

View File

@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
@ -13,10 +13,10 @@ namespace BTCPayServer.Tests
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
{
this.parent = parent;
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
RPC = new CLightningClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
}
public CLightningRPCClient RPC { get; }
public CLightningClient RPC { get; }
public string P2PHost { get; }
}

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Lnd;
using BTCPayServer.Lightning.LND;
using NBitcoin;
namespace BTCPayServer.Tests.Lnd
@ -16,12 +16,12 @@ namespace BTCPayServer.Tests.Lnd
var url = serverTester.GetEnvironment(environmentName, defaultValue);
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
Client = new LndInvoiceClient(Swagger);
Client = new LndClient(Swagger, network);
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public LndSwaggerClient Swagger { get; set; }
public LndInvoiceClient Client { get; set; }
public LndClient Client { get; set; }
public string P2PHost { get; }
}
}

View File

@ -1,246 +0,0 @@
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;
using Xunit.Sdk;
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();
await EventuallyAsync(async () =>
{
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
{
Payment_request = merchantInvoice.BOLT11
});
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
Assert.True(invoice.PaidAt.HasValue);
});
}
private async Task EventuallyAsync(Func<Task> act)
{
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try
{
await act();
break;
}
catch (XunitException) when (!cts.Token.IsCancellationRequested)
{
await Task.Delay(500);
}
}
}
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;
}
}
}

View File

@ -18,10 +18,10 @@ using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Tests.Lnd;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Lightning;
namespace BTCPayServer.Tests
{
@ -49,8 +49,8 @@ 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 = (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);
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
@ -78,88 +78,24 @@ namespace BTCPayServer.Tests
PayTester.Start();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning(LightningConnectionType lndBackend)
{
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>
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
public Task EnsureConnectedToDestinations()
{
bool awaitingLocking = false;
while (true)
{
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 => !SKIPPED_STATES.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
case null:
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
ExplorerNode.Generate(1);
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
await Task.Delay(1000);
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(awaitingLocking ? 1 : 10);
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
awaitingLocking = true;
break;
case "CHANNELD_NORMAL":
return;
default:
throw new NotSupportedException(channel?.State ?? "");
}
}
return BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients());
}
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
private IEnumerable<ILightningClient> GetLightningSenderClients()
{
while (true)
{
var blockCount = await ExplorerNode.GetBlockCountAsync();
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);
}
yield return CustomerLightningD;
}
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
private IEnumerable<ILightningClient> GetLightningDestClients()
{
var merchantInfo = await client.GetInfo();
if (merchantInfo.BlockHeight == blockCount)
{
return merchantInfo;
}
return null;
yield return MerchantLightningD;
yield return MerchantLnd.Client;
}
public void SendLightningPayment(Invoice invoice)
@ -171,12 +107,12 @@ namespace BTCPayServer.Tests
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerLightningD.SendAsync(bolt11);
await CustomerLightningD.Pay(bolt11);
}
public CLightningRPCClient CustomerLightningD { get; set; }
public ILightningClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ILightningClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
public LndMockTester MerchantLnd { get; set; }
@ -218,7 +154,7 @@ namespace BTCPayServer.Tests
public void Dispose()
{
foreach(var store in Stores)
foreach (var store in Stores)
{
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
}

View File

@ -15,6 +15,8 @@ using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
namespace BTCPayServer.Tests
{
@ -133,7 +135,7 @@ namespace BTCPayServer.Tests
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;
connectionString = "type=clightning;server=" + ((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
else if (connectionType == LightningConnectionType.LndREST)
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
else

View File

@ -40,6 +40,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
namespace BTCPayServer.Tests
{
@ -387,18 +388,6 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanUseLightMoney()
{
var light = LightMoney.MilliSatoshis(1);
Assert.Equal("0.00000000001", light.ToString());
light = LightMoney.MilliSatoshis(200000);
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSetLightningServer()
{
using (var tester = ServerTester.Create())
@ -435,139 +424,24 @@ namespace BTCPayServer.Tests
}
[Fact]
public void CanParseLightningURL()
public async Task CanSendLightningPaymentCLightning()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/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(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));
await ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCLightning()
public async Task CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.CLightning);
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCharge()
public async Task CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.Charge);
await ProcessLightningPayment(LightningConnectionType.LndREST);
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.LndREST);
}
void ProcessLightningPayment(LightningConnectionType type)
async Task ProcessLightningPayment(LightningConnectionType type)
{
// For easier debugging and testing
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
@ -580,11 +454,11 @@ namespace BTCPayServer.Tests
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
await tester.EnsureConnectedToDestinations();
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
await CanSendLightningPaymentCore(tester, user);
Task.WaitAll(Enumerable.Range(0, 5)
await Task.WhenAll(Enumerable.Range(0, 5)
.Select(_ => CanSendLightningPaymentCore(tester, user))
.ToArray());
}
@ -1444,14 +1318,16 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal("hello", vmpos.Title);
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.ViewPointOfSale(appId).Result).Model);
var publicApps = user.GetController<AppsPublicController>();
var vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
Assert.Equal("hello", vmview.Title);
Assert.Equal(2, vmview.Items.Length);
Assert.Equal("good apple", vmview.Items[0].Title);
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
@ -1783,6 +1659,7 @@ namespace BTCPayServer.Tests
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();

View File

@ -34,7 +34,7 @@ services:
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
@ -49,7 +49,7 @@ services:
- merchant_lnd
devlnd:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
regtest=1
@ -87,7 +87,7 @@ services:
- litecoind
bitcoind:
image: nicolasdorier/docker-bitcoin:0.16.0
image: nicolasdorier/docker-bitcoin:0.16.3
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
@ -97,10 +97,8 @@ 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
zmqpubrawtx=tcp://0.0.0.0:28333
ports:
- "43782:43782"
- "28332:28332"
@ -111,7 +109,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:v0.6.1-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -122,6 +120,7 @@ services:
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
expose:
@ -134,7 +133,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.15
image: shesek/lightning-charge:0.4.3
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -153,7 +152,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:v0.6.1-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -200,7 +199,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:0.4.2.0
image: btcpayserver/lnd:0.5-beta
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -208,11 +207,13 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=merchant_lnd:9735
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53280:8080"
expose:
@ -224,7 +225,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:0.4.2.0
image: btcpayserver/lnd:0.5-beta
environment:
LND_CHAIN: "btc"
LND_ENVIRONMENT: "regtest"
@ -232,11 +233,13 @@ services:
restlisten=0.0.0.0:8080
bitcoin.node=bitcoind
bitcoind.rpchost=bitcoind:43782
bitcoind.zmqpath=tcp://bitcoind:28332
bitcoind.zmqpubrawblock=tcp://bitcoind:28332
bitcoind.zmqpubrawtx=tcp://bitcoind:28333
externalip=customer_lnd:10009
no-macaroons=1
debuglevel=debug
noencryptwallet=1
noseedbackup=1
trickledelay=1000
ports:
- "53281:8080"
expose:

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.96</Version>
<Version>1.0.2.106</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,23 +33,25 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.1" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0.1" />
<PackageReference Include="LedgerWallet" Version="2.0.0.2" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<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.45" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="NBitcoin" Version="4.1.1.48" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.18" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.19" />
<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.17" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
@ -119,16 +121,20 @@
</ItemGroup>
<ItemGroup>
<Content Update="Views\Apps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonEnable.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonTest.cshtml">
<Content Update="Views\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">

View File

@ -14,6 +14,7 @@ using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration
{

View File

@ -18,6 +18,7 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
using BTCPayServer.Security;
using System.Globalization;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
@ -70,6 +71,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
ViewData["ReturnUrl"] = returnUrl;
@ -88,7 +90,7 @@ namespace BTCPayServer.Controllers
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");

View File

@ -1,25 +1,11 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using Newtonsoft.Json;
using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BTCPayServer.Controllers
{
@ -87,7 +73,7 @@ namespace BTCPayServer.Controllers
}
try
{
var items = Parse(settings.Template, settings.Currency);
var items = _AppsHelper.Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
@ -109,11 +95,11 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
if (_AppsHelper.GetCurrencyData(vm.Currency, false) == null)
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
try
{
Parse(vm.Template, vm.Currency);
_AppsHelper.Parse(vm.Template, vm.Currency);
}
catch
{
@ -138,131 +124,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListApps));
}
[HttpGet]
[Route("{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = Parse(settings.Template, settings.Currency)
});
}
private async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
private ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
[HttpPost]
[Route("{appId}/pos")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())

View File

@ -5,8 +5,9 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
@ -15,32 +16,31 @@ using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class AppsController : Controller
{
ApplicationDbContextFactory _ContextFactory;
UserManager<ApplicationUser> _UserManager;
CurrencyNameTable _Currencies;
InvoiceController _InvoiceController;
BTCPayNetworkProvider _NetworkProvider;
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
BTCPayNetworkProvider networkProvider,
AppsHelper appsHelper)
{
_UserManager = userManager;
_ContextFactory = contextFactory;
_NetworkProvider = networkProvider;
_AppsHelper = appsHelper;
}
private UserManager<ApplicationUser> _UserManager;
private ApplicationDbContextFactory _ContextFactory;
private BTCPayNetworkProvider _NetworkProvider;
private AppsHelper _AppsHelper;
[TempData]
public string StatusMessage { get; set; }
public AppsController(
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
CurrencyNameTable currencies,
InvoiceController invoiceController,
BTCPayNetworkProvider networkProvider)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
_NetworkProvider = networkProvider;
}
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();
@ -201,13 +201,5 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
}
}

View File

@ -0,0 +1,185 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
namespace BTCPayServer.Controllers
{
public class AppsPublicController : Controller
{
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
{
_AppsHelper = appsHelper;
_InvoiceController = invoiceController;
}
private AppsHelper _AppsHelper;
private InvoiceController _InvoiceController;
[HttpGet]
[Route("/apps/{appId}/pos")]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _AppsHelper.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ShowCustomAmount = settings.ShowCustomAmount,
Items = _AppsHelper.Parse(settings.Template, settings.Currency)
});
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
string title = null;
var price = 0.0m;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
price = choice.Price.Value;
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await _AppsHelper.GetStore(app);
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
}
public class AppsHelper
{
ApplicationDbContextFactory _ContextFactory;
CurrencyNameTable _Currencies;
public AppsHelper(ApplicationDbContextFactory contextFactory, CurrencyNameTable currencies)
{
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<AppData> GetApp(string appId, AppType appType)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
}
public async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
public ViewPointOfSaleViewModel.Item[] Parse(string template, string currency)
{
var input = new StringReader(template);
YamlStream stream = new YamlStream();
stream.Load(input);
var root = (YamlMappingNode)stream.Documents[0].RootNode;
return root
.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlMappingNode })
.Where(kv => kv.Value != null)
.Select(c => new ViewPointOfSaleViewModel.Item()
{
Id = c.Key,
Title = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "title")
.FirstOrDefault()?.Value?.Value ?? c.Key,
Price = c.Value.Children
.Select(kv => new { Key = (kv.Key as YamlScalarNode)?.Value, Value = kv.Value as YamlScalarNode })
.Where(kv => kv.Value != null)
.Where(cc => cc.Key == "price")
.Select(cc => new ViewPointOfSaleViewModel.Item.ItemPrice()
{
Value = decimal.Parse(cc.Value.Value, CultureInfo.InvariantCulture),
Formatted = FormatCurrency(cc.Value.Value, currency)
})
.Single()
})
.ToArray();
}
public string FormatCurrency(string price, string currency)
{
return decimal.Parse(price, CultureInfo.InvariantCulture).ToString("C", _Currencies.GetCurrencyProvider(currency));
}
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
}
}

View File

@ -1,26 +1,20 @@
using BTCPayServer.Authentication;
using Microsoft.Extensions.Logging;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Cors;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;

View File

@ -1,16 +1,14 @@
using BTCPayServer.Filters;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Filters;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Payment;
namespace BTCPayServer.Controllers
{
@ -84,4 +82,36 @@ namespace BTCPayServer.Controllers
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View File

@ -1,28 +1,25 @@
using BTCPayServer.Data;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Filters;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers
{
@ -502,7 +499,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
StatusMessage = null;
if (!store.HasClaim(Policies.CanModifyStoreSettings.Key))
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
{
ModelState.AddModelError(nameof(model.StoreId), "You need to be owner of this store to create an invoice");
return View(model);

View File

@ -1,47 +1,25 @@
using BTCPayServer.Authentication;
using System.Reflection;
using System.Linq;
using Microsoft.Extensions.Logging;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Newtonsoft.Json;
using System.Globalization;
using NBitcoin;
using NBitcoin.DataEncoders;
using BTCPayServer.Filters;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Net;
using Microsoft.AspNetCore.Identity;
using Newtonsoft.Json.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin.Payment;
using BTCPayServer.Data;
using BTCPayServer.Models.InvoicingModels;
using System.Security.Claims;
using BTCPayServer.Services;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBitpayClient;
using Newtonsoft.Json;
namespace BTCPayServer.Controllers
{
@ -84,6 +62,8 @@ namespace BTCPayServer.Controllers
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
if (!store.HasClaim(Policies.CanCreateInvoice.Key))
throw new UnauthorizedAccessException();
InvoiceLogs logs = new InvoiceLogs();
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity

View File

@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using NBitcoin.Payment;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
public class PaymentRequestActionResult : IActionResult
{
PaymentRequest req;
public PaymentRequestActionResult(PaymentRequest req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentrequest";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
public class PaymentAckActionResult : IActionResult
{
PaymentACK req;
public PaymentAckActionResult(PaymentACK req)
{
this.req = req;
}
public Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.Headers["Content-Transfer-Encoding"] = "binary";
context.HttpContext.Response.ContentType = "application/bitcoin-paymentack";
req.WriteTo(context.HttpContext.Response.Body);
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public class PublicController : Controller
{
public PublicController(InvoiceController invoiceController,
StoreRepository storeRepository)
{
_InvoiceController = invoiceController;
_StoreRepository = storeRepository;
}
private InvoiceController _InvoiceController;
private StoreRepository _StoreRepository;
[HttpPost]
[Route("api/v1/invoices")]
[MediaTypeAcceptConstraintAttribute("text/html")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle([FromForm]PayButtonViewModel model)
{
var store = await _StoreRepository.FindStore(model.StoreId);
if (store == null)
ModelState.AddModelError("Store", "Invalid store");
else
{
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
ModelState.AddModelError("Store", "Store has not enabled Pay Button");
}
if (model == null || model.Price <= 0)
ModelState.AddModelError("Price", "Price must be greater than 0");
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
BuyerEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
}
}

View File

@ -24,6 +24,7 @@ using System.Net.Mail;
using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
namespace BTCPayServer.Controllers
{

View File

@ -5,12 +5,12 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
using System.Threading;
using BTCPayServer.Lightning;
namespace BTCPayServer.Controllers
{

View File

@ -48,8 +48,7 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env,
InvoiceController invoiceController)
IHostingEnvironment env)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -65,7 +64,6 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_InvoiceController = invoiceController;
}
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -80,7 +78,6 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
IHostingEnvironment _Env;
InvoiceController _InvoiceController;
[TempData]
public string StatusMessage
@ -402,6 +399,7 @@ namespace BTCPayServer.Controllers
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, storeBlob, vm);
@ -473,6 +471,7 @@ namespace BTCPayServer.Controllers
}
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
@ -779,6 +778,12 @@ namespace BTCPayServer.Controllers
{
var store = StoreData;
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
{
return View("PayButtonEnable", null);
}
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
@ -793,39 +798,22 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("{storeId}/pay")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle(string storeId, [FromForm]PayButtonViewModel model)
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton(bool enableStore)
{
var store = StoreData;
// TODO: extract validation to model
if (model.Price <= 0)
ModelState.AddModelError("Price", "Price must be greater than 0");
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = enableStore;
if (StoreData.SetStoreBlob(blob))
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
BuyerEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(PayButton), new
{
storeId = StoreData.Id
});
[HttpGet]
[Route("{storeId}/paybuttontest")]
public IActionResult PayButtonTest(string storeId)
{
return View();
}
}
}

View File

@ -166,24 +166,25 @@ namespace BTCPayServer.Data
public Claim[] GetClaims()
{
List<Claim> claims = new List<Claim>();
claims.AddRange(AdditionalClaims);
#pragma warning disable CS0612 // Type or member is obsolete
var role = Role;
#pragma warning restore CS0612 // Type or member is obsolete
if (role == StoreRoles.Owner)
{
claims.Add(new Claim(Policies.CanModifyStoreSettings.Key, Id));
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
}
if (role == StoreRoles.Guest)
if(role == StoreRoles.Owner || role == StoreRoles.Guest || GetStoreBlob().AnyoneCanInvoice)
{
claims.Add(new Claim(Policies.CanUseStore.Key, Id));
claims.Add(new Claim(Policies.CanCreateInvoice.Key, Id));
}
return claims.ToArray();
}
public bool HasClaim(string claim)
{
return GetClaims().Any(c => c.Type == claim);
return GetClaims().Any(c => c.Type == claim && c.Value == Id);
}
public byte[] StoreBlob
@ -196,6 +197,9 @@ namespace BTCPayServer.Data
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
[NotMapped]
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
{
@ -302,6 +306,8 @@ namespace BTCPayServer.Data
public string RateScript { get; set; }
public bool AnyoneCanInvoice { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate

View File

@ -28,6 +28,28 @@ namespace BTCPayServer.Filters
}
}
public class MediaTypeAcceptConstraintAttribute : Attribute, IActionConstraint
{
public MediaTypeAcceptConstraintAttribute(string mediaType)
{
MediaType = mediaType ?? throw new ArgumentNullException(nameof(mediaType));
}
public string MediaType
{
get; set;
}
public int Order => 100;
public bool Accept(ActionConstraintContext context)
{
if (!context.RouteContext.HttpContext.Request.Headers.ContainsKey("Accept"))
return false;
return context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().StartsWith(MediaType, StringComparison.Ordinal);
}
}
public class BitpayAPIConstraintAttribute : Attribute, IActionConstraint
{
public BitpayAPIConstraintAttribute(bool isBitpayAPI = true)

View File

@ -23,7 +23,10 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
if (context.IsEffectivePolicy<XFrameOptionsAttribute>(this))
{
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
}
}

View File

@ -41,6 +41,7 @@ using System.Security.Claims;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Hosting
{
@ -87,7 +88,6 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
@ -95,6 +95,8 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
@ -156,7 +158,7 @@ namespace BTCPayServer.Hosting
{
var opts = provider.GetRequiredService<BTCPayServerOptions>();
var bundle = new BundleOptions();
bundle.UseMinifiedFiles = opts.BundleJsCss;
bundle.UseBundles = opts.BundleJsCss;
bundle.AppendVersion = true;
return bundle;
});
@ -165,6 +167,10 @@ namespace BTCPayServer.Hosting
{
options.AddPolicy(CorsPolicies.All, p=>p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
});
var rateLimits = new RateLimitService();
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
services.AddSingleton(rateLimits);
return services;
}

View File

@ -100,6 +100,9 @@ namespace BTCPayServer.Hosting
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
@ -166,6 +169,7 @@ namespace BTCPayServer.Hosting
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseWebSockets();
app.UseStatusCodePages();
app.UseMvc(routes =>
{
routes.MapRoute(

View File

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Reflection;
using BTCPayServer.Payments.Lightning;
using NBitcoin.JsonConverters;
using System.Globalization;
namespace BTCPayServer.JsonConverters
{
public class LightMoneyJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
Type longType = typeof(long).GetTypeInfo();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
reader.TokenType == JsonToken.Integer ?
longType.IsAssignableFrom(reader.ValueType) ? new LightMoney((long)reader.Value)
: new LightMoney(long.MaxValue) :
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
: null;
}
catch (InvalidCastException)
{
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((LightMoney)value).MilliSatoshi);
}
}
}

View File

@ -9,6 +9,7 @@ namespace BTCPayServer.Models.StoreViewModels
public class PayButtonViewModel
{
public decimal Price { get; set; }
public string InvoiceId { get; set; }
[Required]
public string Currency { get; set; }
public string CheckoutDesc { get; set; }

View File

@ -47,6 +47,9 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Allow anyone to create invoice")]
public bool AnyoneCanCreateInvoice { get; set; }
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]

View File

@ -1,35 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class CLightningInvoice
{
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
[JsonProperty("payment_hash")]
public uint256 PaymentHash { get; set; }
[JsonProperty("msatoshi")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MilliSatoshi { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expiry_time")]
public DateTimeOffset ExpiryTime { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
[JsonProperty("expires_at")]
public DateTimeOffset ExpiryAt { get; set; }
[JsonProperty("bolt11")]
public string BOLT11 { get; set; }
[JsonProperty("pay_index")]
public int? PayIndex { get; set; }
public string Label { get; set; }
public string Status { get; set; }
[JsonProperty("paid_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
}
}

View File

@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Mono.Unix;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class LightningRPCException : Exception
{
public LightningRPCException(string message) : base(message)
{
}
}
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
{
public Network Network { get; private set; }
public Uri Address { get; private set; }
public CLightningRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
if(address.Scheme == "file")
{
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
}
Address = address;
Network = network;
}
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
}
public Task SendAsync(string bolt11)
{
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
}
public async Task<PeerInfo[]> ListPeersAsync()
{
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
foreach (var peer in peers)
{
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
}
return peers;
}
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
{
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
}
public Task ConnectAsync(NodeInfo nodeInfo)
{
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
}
static Encoding UTF8 = new UTF8Encoding(false);
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false, CancellationToken cancellation = default(CancellationToken))
{
parameters = parameters ?? Array.Empty<string>();
using (Socket socket = await Connect())
{
using (var networkStream = new NetworkStream(socket))
{
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
{
using (var jsonWriter = new JsonTextWriter(textWriter))
{
var req = new JObject();
req.Add("id", 0);
req.Add("method", command);
req.Add("params", new JArray(parameters));
await req.WriteToAsync(jsonWriter, cancellation);
await jsonWriter.FlushAsync(cancellation);
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync(cancellation);
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var resultAsync = JObject.LoadAsync(jsonReader, cancellation);
// without this hack resultAsync is blocking even if cancellation happen
using (cancellation.Register(() => { socket.Dispose(); }))
{
var result = await resultAsync;
var error = result.Property("error");
if (error != null)
{
throw new LightningRPCException(error.Value["message"].Value<string>());
}
if (noReturn)
return default(T);
if (isArray)
{
return result["result"].Children().First().Children().First().ToObject<T>();
}
return result["result"].ToObject<T>();
}
}
}
}
}
}
private async Task<Socket> Connect()
{
Socket socket = null;
EndPoint endpoint = null;
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
{
var domain = Address.DnsSafeHost;
if (!IPAddress.TryParse(domain, out IPAddress address))
{
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
if (address == null)
throw new Exception("Host not found");
}
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
endpoint = new IPEndPoint(address, Address.Port);
}
else if (Address.Scheme == "unix")
{
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
if (!path.StartsWith('/'))
path = "/" + path;
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
{
path = path.Remove(0, 1);
}
if (path.Length < 2)
throw new FormatException("Invalid unix url");
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
endpoint = new UnixEndPoint(path);
}
else
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
await socket.ConnectAsync(endpoint);
return socket;
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
if (invoices.Length == 0)
return null;
return ToLightningInvoice(invoices[0]);
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, description ?? "", Math.Max(0, (int)expiry.TotalSeconds) }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";
return ToLightningInvoice(invoice);
}
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
{
return new LightningInvoice()
{
Id = invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.BOLT11,
Status = invoice.Status,
PaidAt = invoice.PaidAt
};
}
Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
{
return Task.FromResult<ILightningListenInvoiceSession>(this);
}
long lastInvoiceIndex = 99999999999;
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
{
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = invoice.PayIndex.Value;
return ToLightningInvoice(invoice);
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
{
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
return new LightningNodeInformation()
{
NodeId = info.Id,
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
void IDisposable.Dispose()
{
}
}
}

View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class NodeInfo
{
public NodeInfo(string nodeId, string host, int port)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (nodeId == null)
throw new ArgumentNullException(nameof(nodeId));
Port = port;
Host = host;
NodeId = nodeId;
}
public string NodeId { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
public override string ToString()
{
return $"{NodeId}@{Host}:{Port}";
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class ChannelInfo
{
public string State { get; set; }
public string Owner { get; set; }
[JsonProperty("funding_txid")]
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 FundingTxId { get; set; }
[JsonProperty("msatoshi_to_us")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney ToUs { get; set; }
[JsonProperty("msatoshi_total")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney Total { get; set; }
[JsonProperty("dust_limit_satoshis")]
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
public Money DustLimit { get; set; }
[JsonProperty("max_htlc_value_in_flight_msat")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MaxHTLCValueInFlight { get; set; }
[JsonProperty("channel_reserve_satoshis")]
[JsonConverter(typeof(NBitcoin.JsonConverters.MoneyJsonConverter))]
public Money ChannelReserve { get; set; }
[JsonProperty("htlc_minimum_msat")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney HTLCMinimum { get; set; }
[JsonProperty("to_self_delay")]
public int ToSelfDelay { get; set; }
[JsonProperty("max_accepted_htlcs")]
public int MaxAcceptedHTLCS { get; set; }
public string[] Status { get; set; }
}
public class PeerInfo
{
public string State { get; set; }
public string Id { get; set; }
[JsonProperty("netaddr")]
public string[] NetworkAddresses { get; set; }
public bool Connected { get; set; }
public string Owner { get; set; }
public ChannelInfo[] Channels { get; set; }
}
}

View File

@ -1,140 +0,0 @@
//
// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets.
//
// Authors:
// Gonzalo Paniagua Javier (gonzalo@ximian.com)
//
// (C) 2003 Ximian, Inc (http://www.ximian.com)
//
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Mono.Unix
{
[Serializable]
public class UnixEndPoint : EndPoint
{
string filename;
public UnixEndPoint(string filename)
{
if (filename == null)
throw new ArgumentNullException("filename");
if (filename.Length == 0)
throw new ArgumentException("Cannot be empty.", "filename");
this.filename = filename;
}
public string Filename
{
get
{
return (filename);
}
set
{
filename = value;
}
}
public override AddressFamily AddressFamily
{
get { return AddressFamily.Unix; }
}
public override EndPoint Create(SocketAddress socketAddress)
{
/*
* Should also check this
*
int addr = (int) AddressFamily.Unix;
if (socketAddress [0] != (addr & 0xFF))
throw new ArgumentException ("socketAddress is not a unix socket address.");
if (socketAddress [1] != ((addr & 0xFF00) >> 8))
throw new ArgumentException ("socketAddress is not a unix socket address.");
*/
if (socketAddress.Size == 2)
{
// Empty filename.
// Probably from RemoteEndPoint which on linux does not return the file name.
UnixEndPoint uep = new UnixEndPoint("a");
uep.filename = "";
return uep;
}
int size = socketAddress.Size - 2;
byte[] bytes = new byte[size];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = socketAddress[i + 2];
// There may be junk after the null terminator, so ignore it all.
if (bytes[i] == 0)
{
size = i;
break;
}
}
string name = Encoding.Default.GetString(bytes, 0, size);
return new UnixEndPoint(name);
}
public override SocketAddress Serialize()
{
byte[] bytes = Encoding.Default.GetBytes(filename);
SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1);
// sa [0] -> family low byte, sa [1] -> family high byte
for (int i = 0; i < bytes.Length; i++)
sa[2 + i] = bytes[i];
//NULL suffix for non-abstract path
sa[2 + bytes.Length] = 0;
return sa;
}
public override string ToString()
{
return (filename);
}
public override int GetHashCode()
{
return filename.GetHashCode(StringComparison.Ordinal);
}
public override bool Equals(object o)
{
UnixEndPoint other = o as UnixEndPoint;
if (other == null)
return false;
return (other.filename == filename);
}
}
}

View File

@ -1,177 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeClient : ILightningInvoiceClient
{
private Uri _Uri;
public Uri Uri
{
get
{
return _Uri;
}
}
private Network _Network;
static HttpClient _Client = new HttpClient();
public ChargeClient(Uri uri, Network network)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (network == null)
throw new ArgumentNullException(nameof(network));
this._Uri = uri;
this._Network = network;
if (uri.UserInfo == null)
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
var userInfo = uri.UserInfo.Split(':');
if (userInfo.Length != 2)
throw new ArgumentException(paramName: nameof(uri), message: "User information not present in uri");
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
}
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
{
var message = CreateMessage(HttpMethod.Post, "invoice");
Dictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("msatoshi", request.Amount.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
if(request.Description != null)
parameters.Add("description", request.Description);
message.Content = new FormUrlEncodedContent(parameters);
var result = await _Client.SendAsync(message, cancellation);
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
}
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
{
var socket = new ClientWebSocket();
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
if (!uri.EndsWith('/'))
uri += "/";
uri += "ws";
uri = ToWebsocketUri(uri);
await socket.ConnectAsync(new Uri(uri), cancellation);
return new ChargeSession(socket);
}
private static string ToWebsocketUri(string uri)
{
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
return uri;
}
public NetworkCredential Credentials { get; set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public async Task<ChargeInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
{
var request = CreateMessage(HttpMethod.Get, $"invoice/{invoiceId}");
var message = await _Client.SendAsync(request, cancellation);
if (message.StatusCode == HttpStatusCode.NotFound)
return null;
message.EnsureSuccessStatusCode();
var content = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<ChargeInvoice>(content);
}
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
var request = CreateMessage(HttpMethod.Get, "info");
var message = await _Client.SendAsync(request, cancellation);
message.EnsureSuccessStatusCode();
var content = await message.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
}
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
{
var uri = GetFullUri(path);
var request = new HttpRequestMessage(method, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
return request;
}
private string GetBase64Creds()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
}
private Uri GetFullUri(string partialUrl)
{
var uri = _Uri.AbsoluteUri;
if (!uri.EndsWith("/", StringComparison.InvariantCultureIgnoreCase))
uri += "/";
return new Uri(uri + partialUrl);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoice = await GetInvoice(invoiceId, cancellation);
if (invoice == null)
return null;
return ChargeClient.ToLightningInvoice(invoice);
}
async Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
{
return await Listen(cancellation);
}
internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice)
{
return new LightningInvoice()
{
Id = invoice.Id ?? invoice.Label,
Amount = invoice.MilliSatoshi,
BOLT11 = invoice.PaymentRequest,
PaidAt = invoice.PaidAt,
Status = invoice.Status
};
}
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
}
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
{
var info = await GetInfoAsync(cancellation);
var address = info.Address.Select(a => a.Address).FirstOrDefault();
var port = info.Port;
return new LightningNodeInformation()
{
NodeId = info.Id,
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
}
}

View File

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeInvoice
{
public string Id { get; set; }
[JsonProperty("msatoshi")]
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
public LightMoney MilliSatoshi { get; set; }
[JsonProperty("paid_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? PaidAt { get; set; }
[JsonProperty("expires_at")]
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? ExpiresAt { get; set; }
public string Status { get; set; }
[JsonProperty("payreq")]
public string PaymentRequest { get; set; }
public string Label { get; set; }
}
public class ChargeSession : ILightningListenInvoiceSession
{
private ClientWebSocket socket;
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
public ChargeSession(ClientWebSocket socket)
{
this.socket = socket;
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
}
ArraySegment<byte> _Buffer;
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
{
var buffer = _Buffer;
var array = _Buffer.Array;
var originalSize = _Buffer.Array.Length;
var newSize = _Buffer.Array.Length;
while (true)
{
var message = await socket.ReceiveAsync(buffer, cancellation);
if (message.MessageType == WebSocketMessageType.Close)
{
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
break;
}
if (message.MessageType != WebSocketMessageType.Text)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
break;
}
if (message.EndOfMessage)
{
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
try
{
var o = ParseMessage(buffer);
if (newSize != originalSize)
{
Array.Resize(ref array, originalSize);
}
return o;
}
catch (Exception ex)
{
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
}
}
else
{
if (buffer.Count - message.Count <= 0)
{
newSize *= 2;
if (newSize > MAX_BUFFER_SIZE)
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
Array.Resize(ref array, newSize);
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
}
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
}
}
throw new InvalidOperationException("Should never happen");
}
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
private ChargeInvoice ParseMessage(ArraySegment<byte> buffer)
{
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
return JsonConvert.DeserializeObject<ChargeInvoice>(str, new JsonSerializerSettings());
}
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
{
var array = _Buffer.Array;
if (array.Length != ORIGINAL_BUFFER_SIZE)
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
await socket.CloseSocket(status, description, cancellation);
throw new WebSocketException($"The socket has been closed ({status}: {description})");
}
public async void Dispose()
{
await this.socket.CloseSocket();
}
public async Task DisposeAsync()
{
await this.socket.CloseSocket();
}
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
{
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
}
void IDisposable.Dispose()
{
Dispose();
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{
public LightMoney Amount { get; set; }
public TimeSpan Expiry { get; set; }
public string Description { get; set; }
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceResponse
{
public string PayReq { get; set; }
public string Id { get; set; }
}
}

View File

@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Charge
{
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
public class GetInfoResponse
{
public class GetInfoAddress
{
public string Type { get; set; }
public string Address { get; set; }
public int Port { get; set; }
}
public string Id { get; set; }
public int Port { get; set; }
public GetInfoAddress[] Address { get; set; }
public string Version { get; set; }
public int BlockHeight { get; set; }
public string Network { get; set; }
}
}

View File

@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Payments.Lightning
{
public class LightningInvoice
{
public string Id { get; set; }
public string Status { get; set; }
public string BOLT11 { get; set; }
public DateTimeOffset? PaidAt
{
get; set;
}
public LightMoney Amount { get; set; }
}
public class LightningNodeInformation
{
public string NodeId { get; set; }
public string Address { get; internal set; }
public int P2PPort { get; internal set; }
public int BlockHeight { get; set; }
}
public interface ILightningInvoiceClient
{
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}
public interface ILightningListenInvoiceSession : IDisposable
{
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
}
}

View File

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

View File

@ -1,51 +0,0 @@
using System;
using System.Collections.Generic;
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
{
public class LightningClientFactory
{
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var uri = supportedPaymentMethod.GetLightningUrl();
return CreateClient(uri, network.NBitcoinNetwork);
}
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
{
if (connString.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(connString.ToUri(true), network);
}
else if (connString.ConnectionType == LightningConnectionType.CLightning)
{
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 ({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);
}
}
}

View File

@ -1,438 +0,0 @@
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,
LndREST,
LndGRPC
}
public class LightningConnectionString
{
static Dictionary<string, LightningConnectionType> typeMapping;
static Dictionary<LightningConnectionType, string> typeMappingReverse;
static LightningConnectionString()
{
typeMapping = new Dictionary<string, LightningConnectionType>();
typeMapping.Add("clightning", LightningConnectionType.CLightning);
typeMapping.Add("charge", LightningConnectionType.Charge);
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
typeMapping.Add("lnd-grpc", LightningConnectionType.LndGRPC);
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
foreach (var kv in typeMapping)
{
typeMappingReverse.Add(kv.Value, kv.Key);
}
}
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:
case LightningConnectionType.LndGRPC:
{
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;
}
public LightningConnectionString Clone()
{
LightningConnectionString.TryParse(this.ToString(), false, out var result);
return result;
}
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();
connectionString = null;
error = null;
Uri uri;
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
{
error = "Invalid URL";
return false;
}
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
if (!supportedDomains.Contains(uri.Scheme))
{
var protocols = String.Join(",", supportedDomains);
error = $"The url support the following protocols {protocols}";
return false;
}
if (uri.Scheme == "unix")
{
str = uri.AbsoluteUri.Substring("unix:".Length);
while (str.Length >= 1 && str[0] == '/')
{
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(':');
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
{
error = "The url is missing user and password";
return false;
}
result.Username = parts[0];
result.Password = parts[1];
result.ConnectionType = LightningConnectionType.Charge;
}
else if (!string.IsNullOrEmpty(uri.UserInfo))
{
error = "The url should not have user information";
return false;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
result.IsLegacy = true;
connectionString = result;
return true;
}
public LightningConnectionString()
{
}
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;
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)
{
if (withCredentials)
{
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
}
else
{
return BaseUri;
}
}
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
public override string ToString()
{
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:
case LightningConnectionType.LndGRPC:
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();
}
}
}

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.JsonConverters;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json;

View File

@ -7,8 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Lightning;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Payments.Lightning
@ -18,12 +17,9 @@ namespace BTCPayServer.Payments.Lightning
public static int LIGHTNING_TIMEOUT = 5000;
NBXplorerDashboard _Dashboard;
LightningClientFactory _LightningClientFactory;
public LightningLikePaymentHandler(
LightningClientFactory lightningClientFactory,
NBXplorerDashboard dashboard)
{
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
@ -32,7 +28,7 @@ namespace BTCPayServer.Payments.Lightning
var test = Test(supportedPaymentMethod, network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var client = supportedPaymentMethod.CreateClient(network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
@ -74,7 +70,7 @@ namespace BTCPayServer.Payments.Lightning
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
{
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var client = supportedPaymentMethod.CreateClient(network);
LightningNodeInformation info = null;
try
{
@ -89,7 +85,7 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
}
if (info.Address == null)
if (info.NodeInfo == null)
{
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
}
@ -100,7 +96,7 @@ namespace BTCPayServer.Payments.Lightning
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
}
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
return info.NodeInfo;
}
}

View File

@ -9,6 +9,7 @@ using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using BTCPayServer.Lightning;
namespace BTCPayServer.Payments.Lightning
{
@ -27,16 +28,13 @@ namespace BTCPayServer.Payments.Lightning
EventAggregator _Aggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
LightningClientFactory _LightningClientFactory;
public LightningListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
LightningClientFactory lightningClientFactory,
BTCPayNetworkProvider networkProvider)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_LightningClientFactory = lightningClientFactory;
}
CompositeDisposable leases = new CompositeDisposable();
@ -100,22 +98,22 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var charge = lightningSupportedMethod.CreateClient(network);
LightningInvoice chargeInvoice = null;
try
{
chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
}
catch(Exception ex)
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{lightningSupportedMethod.CryptoCode} (Lightning): Can't connect to the lightning server");
continue;
}
if (chargeInvoice == null)
continue;
if (chargeInvoice.Status == "paid")
if (chargeInvoice.Status == LightningInvoiceStatus.Paid)
await AddPayment(network, chargeInvoice, listenedInvoice);
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
if (chargeInvoice.Status == LightningInvoiceStatus.Paid || chargeInvoice.Status == LightningInvoiceStatus.Expired)
continue;
}
@ -143,11 +141,11 @@ namespace BTCPayServer.Payments.Lightning
CancellationTokenSource _Cts = new CancellationTokenSource();
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
ILightningListenInvoiceSession session = null;
ILightningInvoiceListener session = null;
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var lightningClient = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var lightningClient = supportedPaymentMethod.CreateClient(network);
session = await lightningClient.Listen(_Cts.Token);
while (true)
{
@ -158,13 +156,13 @@ namespace BTCPayServer.Payments.Lightning
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == "paid" && notification.PaidAt.HasValue)
if (notification.Status == LightningInvoiceStatus.Paid && notification.PaidAt.HasValue)
{
await AddPayment(network, notification, listenedInvoice);
if (DoneListening(listenedInvoice))
break;
}
if (notification.Status == "expired")
if (notification.Status == LightningInvoiceStatus.Expired)
{
if (DoneListening(listenedInvoice))
break;
@ -197,7 +195,7 @@ namespace BTCPayServer.Payments.Lightning
if (payment != null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
if(invoice != null)
if (invoice != null)
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
}
}

View File

@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using NBitcoin;
namespace BTCPayServer.Payments.Lightning
{
@ -28,7 +30,7 @@ namespace BTCPayServer.Payments.Lightning
#pragma warning disable CS0618 // Type or member is obsolete
if (!string.IsNullOrEmpty(LightningConnectionString))
{
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
{
throw new FormatException(error);
}
@ -37,7 +39,7 @@ namespace BTCPayServer.Payments.Lightning
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))
if (!BTCPayServer.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
{
throw new FormatException(error);
}
@ -58,5 +60,10 @@ namespace BTCPayServer.Payments.Lightning
LightningChargeUrl = null;
#pragma warning restore CS0618 // Type or member is obsolete
}
public ILightningClient CreateClient(BTCPayNetwork network)
{
return LightningClientFactory.CreateClient(this.GetLightningUrl(), network.NBitcoinNetwork);
}
}
}

View File

@ -1,67 +0,0 @@
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);
}
}

View File

@ -1,279 +0,0 @@
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.Runtime.ExceptionServices;
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();
HttpClient _Client;
HttpResponseMessage _Response;
Stream _Body;
StreamReader _Reader;
Task _ListenLoop;
public LndInvoiceClientSession(LndSwaggerClient parent)
{
_Parent = parent;
}
public Task StartListening()
{
try
{
_Client = _Parent.CreateHttpClient();
_Client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
var request = new HttpRequestMessage(HttpMethod.Get, _Parent.BaseUrl.WithTrailingSlash() + "v1/invoices/subscribe");
_Parent._Authentication.AddAuthentication(request);
_ListenLoop = ListenLoop(request);
}
catch
{
Dispose();
}
return Task.CompletedTask;
}
private async Task ListenLoop(HttpRequestMessage request)
{
try
{
_Response = await _Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token);
_Body = await _Response.Content.ReadAsStreamAsync();
_Reader = new StreamReader(_Body);
while (!_Cts.IsCancellationRequested)
{
string line = await _Reader.ReadLineAsync().WithCancellation(_Cts.Token);
if (line != null)
{
if (line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
{
var invoiceString = JObject.Parse(line)["result"].ToString();
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice), _Cts.Token);
}
else if (line.StartsWith("{\"error\":", StringComparison.OrdinalIgnoreCase))
{
var errorString = JObject.Parse(line)["error"].ToString();
var error = _Parent.Deserialize<LndError>(errorString);
throw new LndException(error);
}
else
{
throw new LndException("Unknown result from LND: " + line);
}
}
}
}
catch when (_Cts.IsCancellationRequested)
{
}
catch (Exception ex)
{
_Invoices.Writer.TryComplete(ex);
}
finally
{
Dispose(false);
}
}
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
{
try
{
return await _Invoices.Reader.ReadAsync(cancellation);
}
catch (ChannelClosedException ex) when (ex.InnerException == null)
{
throw new TaskCanceledException();
}
catch (ChannelClosedException ex)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
throw;
}
}
public void Dispose()
{
Dispose(true);
}
void Dispose(bool waitLoop)
{
if (_Cts.IsCancellationRequested)
return;
_Cts.Cancel();
_Reader?.Dispose();
_Reader = null;
_Body?.Dispose();
_Body = null;
_Response?.Dispose();
_Response = null;
_Client?.Dispose();
_Client = null;
if (waitLoop)
_ListenLoop?.Wait();
_Invoices.Writer.TryComplete();
}
}
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
};
try
{
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;
}
catch (SwaggerException ex) when (!string.IsNullOrEmpty(ex.Response))
{
throw new Exception("LND threw an error: " + ex.Response);
}
}
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
{
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
return ConvertLndInvoice(resp);
}
public async Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
{
var session = new LndInvoiceClientSession(this._rpcClient);
await session.StartListening();
return 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);
}
}
}
}

View File

@ -1,36 +0,0 @@
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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,119 +0,0 @@
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 class LndException : Exception
{
public LndException(string message) : base(message)
{
}
public LndException(LndError error) : base(error.Message)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
_Error = error;
}
private readonly LndError _Error;
public LndError Error
{
get
{
return _Error;
}
}
}
// {"grpc_code":2,"http_code":500,"message":"rpc error: code = Unknown desc = expected 1 macaroon, got 0","http_status":"Internal Server Error"}
public class LndError
{
[JsonProperty("grpc_code")]
public int GRPCCode { get; set; }
[JsonProperty("http_code")]
public int HttpCode { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("http_status")]
public string HttpStatus { get; set; }
}
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;
internal 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);
}
}
}

View File

@ -88,8 +88,9 @@ namespace BTCPayServer.Security
{
if (storeId != null)
{
claims.Add(new Claim(Policies.CanUseStore.Key, storeId));
claims.Add(new Claim(Policies.CanCreateInvoice.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
store.AdditionalClaims.AddRange(claims);
Context.Request.HttpContext.SetStoreData(store);
}
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));

View File

@ -12,9 +12,9 @@ namespace BTCPayServer.Security
public const string CookieAuthentication = "Identity.Application";
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{
AddClaim(options, CanUseStore.Key);
AddClaim(options, CanModifyStoreSettings.Key);
AddClaim(options, CanModifyServerSettings.Key);
AddClaim(options, CanCreateInvoice.Key);
return options;
}
@ -27,13 +27,14 @@ namespace BTCPayServer.Security
{
public const string Key = "btcpay.store.canmodifyserversettings";
}
public class CanUseStore
{
public const string Key = "btcpay.store.canusestore";
}
public class CanModifyStoreSettings
{
public const string Key = "btcpay.store.canmodifystoresettings";
}
public class CanCreateInvoice
{
public const string Key = "btcpay.store.cancreateinvoice";
}
}
}

View File

@ -148,7 +148,7 @@ namespace BTCPayServer.Services.Invoices
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
foreach(var log in creationLogs.ToList())
foreach (var log in creationLogs.ToList())
{
context.InvoiceEvents.Add(new InvoiceEventData()
{
@ -249,7 +249,7 @@ namespace BTCPayServer.Services.Invoices
{
await context.SaveChangesAsync();
}
catch(DbUpdateException) { } // Probably the invoice does not exists anymore
catch (DbUpdateException) { } // Probably the invoice does not exists anymore
}
}
@ -441,7 +441,7 @@ namespace BTCPayServer.Services.Invoices
query = query.Where(i => statusSet.Contains(i.Status));
}
if(queryObject.Unusual != null)
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
@ -554,7 +554,7 @@ namespace BTCPayServer.Services.Invoices
{
await context.SaveChangesAsync().ConfigureAwait(false);
}
catch(DbUpdateException) { return null; } // Already exists
catch (DbUpdateException) { return null; } // Already exists
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
return entity;
}

View File

@ -1,9 +1,11 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Logging;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
@ -13,33 +15,25 @@ namespace BTCPayServer.Services.Rates
public class LatestFetch
{
public ExchangeRates Latest;
public DateTimeOffset Timestamp;
public DateTimeOffset NextRefresh;
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
internal ExchangeRates GetResult()
{
if(Expiration < DateTimeOffset.UtcNow)
if (Expiration <= DateTimeOffset.UtcNow)
{
if(Exception != null)
if (Exception != null)
{
ExceptionDispatchInfo.Capture(Exception).Throw();
}
else
{
throw new InvalidOperationException("The rate has expired");
throw new InvalidOperationException($"The rate has expired ({ExchangeName})");
}
}
return Latest;
}
internal void CopyFrom(LatestFetch previous)
{
Latest = previous.Latest;
Timestamp = previous.Timestamp;
Expiration = previous.Expiration;
Exception = previous.Exception;
}
}
IRateProvider _Inner;
@ -50,20 +44,53 @@ namespace BTCPayServer.Services.Rates
_Inner = inner;
}
public TimeSpan RefreshRate { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan ValidatyTime { get; set; } = TimeSpan.FromMinutes(10);
TimeSpan _RefreshRate = TimeSpan.FromSeconds(30);
public TimeSpan RefreshRate
{
get
{
return _RefreshRate;
}
set
{
var diff = value - _RefreshRate;
var latest = _Latest;
if (latest != null)
latest.NextRefresh += diff;
_RefreshRate = value;
}
}
TimeSpan _ValidatyTime = TimeSpan.FromMinutes(10);
public TimeSpan ValidatyTime
{
get
{
return _ValidatyTime;
}
set
{
var diff = value - _ValidatyTime;
var latest = _Latest;
if (latest != null)
latest.Expiration += diff;
_ValidatyTime = value;
}
}
public DateTimeOffset NextUpdate
{
get
{
var latest = _Latest;
if (latest == null || latest.Exception != null)
if (latest == null)
return DateTimeOffset.UtcNow;
return latest.Timestamp + RefreshRate;
return latest.NextRefresh;
}
}
public bool DoNotAutoFetchIfExpired { get; set; }
public async Task<LatestFetch> UpdateIfNecessary()
{
if (NextUpdate <= DateTimeOffset.UtcNow)
@ -81,28 +108,48 @@ namespace BTCPayServer.Services.Rates
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync()
{
return (_Latest ?? (await Fetch())).GetResult();
var latest = _Latest;
if (!DoNotAutoFetchIfExpired && latest != null && latest.Expiration <= DateTimeOffset.UtcNow + TimeSpan.FromSeconds(1.0))
{
Logs.PayServer.LogWarning($"GetRatesAsync was called on {GetExchangeName()} when the rate is outdated. It should never happen, let BTCPayServer developers know about this.");
latest = null;
}
return (latest ?? (await Fetch())).GetResult();
}
private string GetExchangeName()
{
if (_Inner is IHasExchangeName exchangeName)
return exchangeName.ExchangeName ?? "???";
return "???";
}
private async Task<LatestFetch> Fetch()
{
var previous = _Latest;
var fetch = new LatestFetch();
fetch.ExchangeName = GetExchangeName();
try
{
var rates = await _Inner.GetRatesAsync();
fetch.Latest = rates;
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
fetch.NextRefresh = DateTimeOffset.UtcNow + RefreshRate;
}
catch (Exception ex)
{
if(previous != null)
if (previous != null)
{
fetch.CopyFrom(previous);
fetch.Latest = previous.Latest;
fetch.Expiration = previous.Expiration;
}
else
{
fetch.Expiration = DateTimeOffset.UtcNow;
}
fetch.NextRefresh = DateTimeOffset.UtcNow;
fetch.Exception = ex;
}
fetch.Timestamp = DateTimeOffset.UtcNow;
_Latest = fetch;
fetch.GetResult(); // Will throw if not valid
return fetch;

View File

@ -9,7 +9,7 @@ using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class BitpayRateProvider : IRateProvider
public class BitpayRateProvider : IRateProvider, IHasExchangeName
{
public const string BitpayName = "bitpay";
Bitpay _Bitpay;
@ -20,6 +20,8 @@ namespace BTCPayServer.Services.Rates
_Bitpay = bitpay;
}
public string ExchangeName => BitpayName;
public async Task<ExchangeRates> GetRatesAsync()
{
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))

View File

@ -7,7 +7,7 @@ using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedRateProvider : IRateProvider
public class CachedRateProvider : IRateProvider, IHasExchangeName
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Rates
}
}
public string ExchangeName { get; set; }
public string ExchangeName { get; }
public TimeSpan CacheSpan
{

View File

@ -49,7 +49,7 @@ namespace BTCPayServer.Services.Rates
Task AddHeader(HttpRequestMessage message);
}
public class CoinAverageRateProvider : IRateProvider
public class CoinAverageRateProvider : IRateProvider, IHasExchangeName
{
public const string CoinAverageName = "coinaverage";
public CoinAverageRateProvider()
@ -82,6 +82,8 @@ namespace BTCPayServer.Services.Rates
public ICoinAverageAuthenticator Authenticator { get; set; }
public string ExchangeName => Exchange ?? CoinAverageName;
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
{
bidAsk = null;

View File

@ -10,7 +10,7 @@ using ExchangeSharp;
namespace BTCPayServer.Services.Rates
{
public class ExchangeSharpRateProvider : IRateProvider
public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName
{
readonly ExchangeAPI _ExchangeAPI;
readonly string _ExchangeName;
@ -29,6 +29,8 @@ namespace BTCPayServer.Services.Rates
get; set;
}
public string ExchangeName => _ExchangeName;
public async Task<ExchangeRates> GetRatesAsync()
{
await new SynchronizationContextRemover();

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface IHasExchangeName
{
string ExchangeName { get; }
}
}

View File

@ -13,7 +13,7 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider
public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName
{
public KrakenExchangeRateProvider()
{
@ -31,6 +31,9 @@ namespace BTCPayServer.Services.Rates
_LocalClient = null;
}
}
public string ExchangeName => "kraken";
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();

View File

@ -9,11 +9,13 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class QuadrigacxRateProvider : IRateProvider
public class QuadrigacxRateProvider : IRateProvider, IHasExchangeName
{
public const string QuadrigacxName = "quadrigacx";
static HttpClient _Client = new HttpClient();
public string ExchangeName => QuadrigacxName;
private bool TryToBidAsk(JObject p, out BidAsk v)
{
v = null;

View File

@ -21,17 +21,17 @@ namespace BTCPayServer.Services.Rates
{
_inner = inner;
}
public Task<ExchangeRates> GetRatesAsync()
public async Task<ExchangeRates> GetRatesAsync()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return _inner.GetRatesAsync();
return await _inner.GetRatesAsync();
}
catch (Exception ex)
{
Exception = ex;
return Task.FromResult(new ExchangeRates());
return new ExchangeRates();
}
finally
{
@ -79,7 +79,10 @@ namespace BTCPayServer.Services.Rates
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
{
c.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
}
}
CoinAverageSettings _CoinAverageSettings;
private readonly IHttpClientFactory _httpClientFactory;
@ -102,7 +105,6 @@ namespace BTCPayServer.Services.Rates
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
@ -117,7 +119,16 @@ namespace BTCPayServer.Services.Rates
foreach (var provider in Providers.ToArray())
{
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
prov.RefreshRate = provider.Key == CoinAverageRateProvider.CoinAverageName ? CacheSpan : TimeSpan.FromMinutes(1.0);
if(provider.Key == CoinAverageRateProvider.CoinAverageName)
{
prov.RefreshRate = CacheSpan;
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
}
else
{
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
}
Providers[provider.Key] = prov;
}

View File

@ -1,8 +1,19 @@
@{
ViewData["Title"] = "Locked out";
}
<section>
<div class="container">
<header>
<h2 class="text-danger">@ViewData["Title"]</h2>
<p class="text-danger">This account has been locked out, please try again later.</p>
</header>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
</div>
<div class="col-lg-12 lead">
<p>This account has been locked out because of multiple invalid login attempts. Please try again later.</p>
</div>
</div>
</div>
</section>

View File

@ -53,7 +53,7 @@
{
<a asp-action="@app.UpdateAction" asp-controller="Apps" asp-route-appId="@app.Id">Settings</a><span> - </span>
}
<a asp-action="@app.ViewAction" asp-controller="Apps" asp-route-appId="@app.Id">View</a><span> - </span>
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id">View</a><span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
</td>
</tr>

View File

@ -1,6 +1,6 @@
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model ViewPointOfSaleViewModel
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;
@ -21,7 +21,7 @@
<h1 class="mb-4">@Model.Title</h1>
<form method="post" asp-antiforgery="false">
<div class="row">
@for(int i = 0; i < Model.Items.Length; i++)
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
var item = Model.Items[i];
@ -32,10 +32,11 @@
}
</div>
</form>
@if(Model.ShowCustomAmount)
@if (Model.ShowCustomAmount)
{
<div class="row mt-4">
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
<div class="col-sm-3">&nbsp;</div>
<div class="col-sm-6">
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
@ -44,6 +45,7 @@
</div>
</form>
</div>
<div class="col-sm-3">&nbsp;</div>
</div>
}
</div>

View File

@ -116,7 +116,7 @@
{{srvModel.orderAmountFiat}}
</div>
</div>
<div class="line-items__item">
<div class="line-items__item" v-show="srvModel.networkFee > 0">
<div class="line-items__item__label">
<span>{{$t("Network Cost")}}</span>
</div>
@ -275,7 +275,7 @@
</div>
}
<div class="bp-view pad" id="paid">
<div class="bp-view" id="paid">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
@ -339,268 +339,4 @@
</a>
</div>
</div>
@* Obsolete? Start *@
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
<div class="manual__step-one refund-address-form" novalidate="">
<div class="manual__step-one__header" i18n="">Link Expired</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
</div>
<div class="input-wrapper">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>
</div>
</div>
</div>
<div class="bp-view confirm-contact-email-view">
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
<div class="manual__step-one__header" i18n="">Contact &amp; Refund Email</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
</div>
<div class="input-wrapper">
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email"
style="opacity: 1;" type="email">
<button type="submit" class="action-button" style="margin-top: 15px;">
<span i18n="">Confirm email address</span>
</button>
</div>
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
<span i18n="">Wrong email?</span>
</div>
</form>
</div>
<div class="bp-view wrong-email-view" id="compromised-invoice">
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
<div class="manual__step-one__header">
<span i18n="">There seems to be a problem</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
If you did not submit the email address, it's possible a thief falsely
submitted this address to steal refunds. Please contact the merchant
about this security incident, and try your payment again.
</span>
</div>
<div class="input-wrapper">
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
<span i18n="">Contact {{srvModel.storeName}}</span>
</a>
</div>
<div class="refund-address-form__link">
<span i18n="">I understand, continue to payment&nbsp;→</span>
</div>
</div>
</div>
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
<div><img src="~/imlegacy/mail.svg"></div>
<div class="manual__step-one__header">
<span i18n="">Please confirm your address</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
</div>
<bp-resend-link id="resend-link">
<div class="bp-resend__link">
<span class="link-text">
<span i18n="">Resend email</span>
</span>
<div class="success-text">
<img src="~/imlegacy/circle-check.svg">
<div i18n="">Email resent</div>
</div>
</div>
</bp-resend-link>
</form>
</div>
<div class="bp-view refund-address-view" id="enter-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
<div class="manual__step-one__header">
<span i18n="">Please provide a refund address.</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span i18n="">
To send your refund of {BTC to refund} BTC,
well need a bitcoin address from your wallet. Please open your bitcoin
wallet, copy a receiving address, and paste it below.
</span>
</span>
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
</div>
<div class="input-wrapper">
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
</bp-refund-address>
<bp-loading-button i18n="" id="request-refund-button">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner" />
</div>
</button>
</bp-loading-button>
</div>
<div class="refund-address-form__cancel">
<span i18n="">Cancel</span>
</div>
</form>
</div>
<div class="bp-view" id="refund-pending">
<div class="status-block">
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
<img src="~/imlegacy/refund-pending.svg">
<div class="pending-block__header" i18n="">Processing Refund</div>
<span>
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
</span>
</div>
</div>
<div class="manual-box" style="margin-bottom: 30px;">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label">&nbsp;</span>
<span class="final-label" i18n="">Amount To Be Refunded</span>
</div>
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bp-view expired" id="low-fee">
<div class="expired__body">
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
<div class="expired__text" i18n="">
If the transaction
doesn't confirm, the funds will be spendable again in your wallet.
Depending on the wallet, this may take 48-72 hours.
</div>
<low-fee-timeline>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--complete">
<img src="~/imlegacy/checkmark-small.svg">
</div>
<div class="timeline__item__name" i18n="">Transaction created</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--pending">
<img src="~/imlegacy/pending.svg">
</div>
<div class="timeline__item__name">
<span i18n="">Transaction confirming — funds have not yet moved</span>
</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon"></div>
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
</div>
</div>
</low-fee-timeline>
</div>
<button class="action-button" style="margin-top: .75rem;">
<bp-done-text>
<span i18n="">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
</div>
</div>
</div>
<div class="success-message">
<span>
<span i18n="">Refund Complete</span>
</span>
</div>
</div>
</div>
<div class="manual-box">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label" i18n="">Overpaid By</span>
<span class="final-label">
<span i18n="">Amount Refunded</span>
</span>
</div>
<div class="manual-box__amount__value">{BTC amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<button class="action-button finished" style="margin-top: 23px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
</div>
<div class="footer-button enter-different-address-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</div>
@* Obsolete? End *@
</div>

View File

@ -54,7 +54,7 @@
<![endif]-->
<invoice>
<div class="no-bounce" id="checkoutCtrl">
<div class="no-bounce" id="checkoutCtrl" v-cloak>
<div class="modal page">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<div class="modal-content long">
@ -64,7 +64,8 @@
</div>
</div>
</div>
<div style="margin-top: 10px; text-align: right;">
<div style="margin-top: 10px; text-align: center;">
@* Not working because of nsSeparator: false, keySeparator: false,
{{$t("nested.lang")}} >>
*@
@ -92,7 +93,7 @@
});
</script>
</div>
<div style="margin-top: 10px; text-align: right;" class="form-text small text-muted">
<div style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
</div>
</div>

View File

@ -33,7 +33,7 @@
<body id="page-top">
@{
if(ViewBag.AlwaysShrinkNavBar == null)
if (ViewBag.AlwaysShrinkNavBar == null)
{
ViewBag.AlwaysShrinkNavBar = true;
}
@ -45,7 +45,7 @@
<div class="container">
<a class="navbar-brand js-scroll-trigger" href="~/">
<img src="~/img/logo.png" height="45">
@if(env.NetworkType != NBitcoin.NetworkType.Mainnet)
@if (env.NetworkType != NBitcoin.NetworkType.Mainnet)
{
<span class="badge badge-warning" style="font-size:10px;">@env.NetworkType.ToString()</span>
}
@ -55,9 +55,9 @@
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
@if(SignInManager.IsSignedIn(User))
@if (SignInManager.IsSignedIn(User))
{
@if(User.IsInRole(Roles.ServerAdmin))
@if (User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
@ -73,7 +73,7 @@
</li>}
else
{
@if(themeManager.ShowRegister)
@if (themeManager.ShowRegister)
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
}
@ -88,7 +88,7 @@
<li>Either starts BTCPay with <b>--externalurl @(env.ExpectedProtocol)://@(env.ExpectedHost)/</b></li>
<li>Or, if using docker-compose deployment, setting environment variable <b>BTCPAY_PROTOCOL=@(env.ExpectedProtocol)</b> and <b>BTCPAY_HOST=@(env.ExpectedDomain)</b></li>
</ul>
</div>
</div>
</nav>
@ -96,11 +96,14 @@
@RenderBody()
<footer class="siteFooter bg-dark text-white">
<div class="container text-right">@env.ToString()</div>
</footer>
@if (User.Identity.IsAuthenticated)
{
<footer class="siteFooter bg-dark text-white">
<div class="container text-right">@env.ToString()</div>
</footer>
}
@if(!dashboard.IsFullySynched())
@if (!dashboard.IsFullySynched())
{
<partial name="SyncModal" />
}

View File

@ -1,7 +1,8 @@
@model PayButtonViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Pay Button");
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton);
ViewBag.MainTitle = "Pay Button";
}
<div class="container" id="payButtonCtrl">
@ -120,8 +121,6 @@
Please fix errors shown in order for code generation to successfully execute.
</div>
</div>
<br />
</div>
@section HeadScripts {

View File

@ -0,0 +1,22 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Please confirm you want to allow anyone to create invoices in your store");
ViewBag.MainTitle = "Pay Button";
}
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="form-group">
<p>
To start using Pay Buttons you need to explicitly turn on this feature.
Once you do so, any source will be able to create an invoice on your instance store.
</p>
@Html.Hidden("EnableStore", true)
<button name="command" type="submit" value="save" class="btn btn-primary">Allow anyone to create invoices</button>
</div>
</form>
</div>
</div>

View File

@ -1,14 +0,0 @@

<section>
<div class="container" id="payButtonCtrl">
<div class="row">
<form method="POST" action="http://127.0.0.1:14142/stores/4b1AnMtMNuxwG7io2dUcB3TrT3yUbgajjGfJjVfdhBDu/pay">
<input type="hidden" name="price" value="10" />
<input type="hidden" name="currency" value="USD" />
<input type="image" src="http://127.0.0.1:14142/img/paybutton/pay.png" name="submit" style="width:209px" alt="Pay with BtcPay, Self-Hosted Bitcoin Payment Processor">
</form>
</div>
</div>
</section>

View File

@ -46,6 +46,10 @@
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="AnyoneCanCreateInvoice"></label>
<input asp-for="AnyoneCanCreateInvoice" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration"></label>
<input asp-for="InvoiceExpiration" class="form-control" />

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class ZoneLimits
{
public const string Login = "btcpaylogin";
}
}

View File

@ -64,7 +64,7 @@
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason) {
if (reason.message === "Sign failed")
if (reason.name === "TransportError")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})

View File

@ -61,7 +61,7 @@ $(function () {
confirmButton.prop("disabled", true);
confirmButton.addClass("disabled");
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
bridge.sendCommand('sendtoaddress', args, 60 * 10 /* timeout */)
.catch(function (reason) {
WriteAlert("danger", reason);
confirmButton.prop("disabled", false);
@ -135,7 +135,7 @@ $(function () {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
if (reason.message === "Sign failed")
if (reason.name === "TransportError")
reason = "Are you running the ledger app with version equals or above 1.2.4?";
Write('hw', 'error', reason);
})

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,8 @@ function inputChanges(event, buttonSize) {
srvModel.buttonSize = buttonSize;
}
var html = '<form method="POST" action="' + esc(srvModel.urlRoot + 'stores/'+ srvModel.storeId + '/pay') + '">';
var html = '<form method="POST" action="' + esc(srvModel.urlRoot) + 'api/v1/invoices">';
html += addinput("storeId", srvModel.storeId);
html += addinput("price", srvModel.price);
if (srvModel.currency) {
html += addinput("currency", srvModel.currency);

View File

@ -8,28 +8,45 @@
## Introduction
BTCPay Server is an Open Source payment processor, written in C#, that conforms to the invoice API of [Bitpay](https://bitpay.com/).
This allows easy migration of your code base to your own, self-hosted payment processor.
BTCPay Server is a free and open-source cryptocurrency payment processor which allows you to receive payments in Bitcoin and altcoins directly, with no fees, transaction cost or a middleman.
This solution is for you if:
BTCPay is a non-custodial invoicing system which eliminates the involvement of a third-party. Payments with BTCPay go directly to your wallet, which increases the privacy and security. Your private keys are never uploaded to the server. There is no address re-use, since each invoice generates a new address deriving from your xpubkey.
* You are currently using Bitpay as a payment processor but are worried about their commitment to Bitcoin in the future
* You want to be in control of your own funds
* Bitpay compliance team decided to reject your application
* You want lower fees (we support Segwit)
* 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
The software is built in C# and conforms to the invoice [API of BitPay](https://bitpay.com/api). It allows for your website to be easily migrated from BitPay and configured as a self-hosted payment processor.
## We support altcoins!
You can run BTCPay as a self-hosted solution on your own server, or use a [third-party host](https://github.com/btcpayserver/btcpayserver-doc/blob/master/ThirdPartyHosting.md).
In addition to Bitcoin, we support the following crypto currencies:
The self-hosted solution allows you not only to attach an unlimited number of stores and use the Lightning Network but also become the payment processor for others.
Thanks to the apps built on top of it, you can use BTCPay to receive donations or have an in-store POS system.
## Features
* Direct, P2P Bitcoin payments
* Lightning Network support (LND and c-lightning)
* Altcoin support
* Complete control over private keys
* Fully compatability with BitPay API (easy migration)
* Enchanced privacy
* SegWit support
* Process payments for others
* Payment buttons
* Point of sale
* No transaction fees (other than those for the crypto networks)
* No processing fees
* No middleman
* No KYC
## Supported Altcoins
In addition to Bitcoin, BTCPay supports the following cryptocurrencies:
* BGold
* Dash
* Dogecoin
* Feathercoin
* Groestlcoin
* Litecoin
* Dash
* Monacoin
* Polis
* UFO
@ -39,13 +56,13 @@ In addition to Bitcoin, we support the following crypto currencies:
Please check out our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) for more details.
You can also checkout [The Merchants Guide to accepting Bitcoin directly with no intermediates through BTCPay](https://www.reddit.com/r/Bitcoin/comments/81h1oy/the_merchants_guide_to_accepting_bitcoin_directly/).
You can also read the [BTCPay Merchants Guide](https://www.reddit.com/r/Bitcoin/comments/8f1eqf/the_ultimate_guide_to_btcpay_the_free_and/).
## How to build
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).
First install .NET Core SDK 2.1 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/2.1).
On Powershell:
```