Compare commits

..

103 Commits

Author SHA1 Message Date
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 
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 
2018-09-22 01:11:14 -05:00
10ee09f052 Bugfixing broken link in footer
Div overlay caused it, fixes 
2018-09-22 01:11:14 -05:00
be7d91a138 VueJs cloak until loading is finished
Addressing 
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 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 () 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 ()
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 ()
* 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 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 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
29af07b3f9 Make sure we do not return outdated rates 2018-08-23 13:47:56 +09:00
758436a428 bump 2018-08-23 12:16:43 +09:00
e0cadb4f62 Do not send not found if invoices does not belong to logged on user 2018-08-23 12:13:27 +09:00
013dfa1b61 Properly escape attributes, make the preview an actual form rather than an image 2018-08-23 11:55:29 +09:00
e0f1c50534 Making Currency a textbox instead of dropdown 2018-08-23 11:17:54 +09:00
d50dc2e68e ServerIPN is an email 2018-08-23 11:12:25 +09:00
8b5b18c97e Make sure no trailing slash bug 2018-08-23 11:11:39 +09:00
f7383b4cc8 Fixing error on CheckoutExperience if no crypto is set 2018-08-23 11:08:53 +09:00
1bc32285ba Merge branch 'master' of https://github.com/rockstardev/btcpayserver into rockstardev-master 2018-08-23 11:01:48 +09:00
f12114f9aa Poll and cache rates in parallel 2018-08-23 00:24:33 +09:00
2b6faa8d20 Migrating generator to work on store path 2018-08-22 14:05:12 +02:00
45b7df6ac9 Removing traces of PayButton being an app... shhhh... 2018-08-22 14:04:59 +02:00
5a43ce2719 Transfering Pay Button from App directly to Store 2018-08-22 13:57:54 +02:00
fe31dc8606 Setting titles for new pill navigation 2018-08-22 13:56:55 +02:00
f0615482d9 Refactoring pill navigation to use new subnav code with enums 2018-08-22 13:50:29 +02:00
03c47e6f7d Merge remote-tracking branch 'source/master' 2018-08-22 13:01:34 +02:00
4111b8a5a3 Maintaining AppId reference 2018-08-22 12:59:55 +02:00
5b5a2e8c25 Reorganizing Javascript code and references 2018-08-22 11:10:46 +02:00
9ec0c23c52 Folder for pay buttons, donate button 2018-08-22 10:59:24 +02:00
9a5034c13c URL of image for pay button 2018-08-22 10:52:17 +02:00
b1fcf4524a Separate app for PayButton 2018-08-22 10:26:49 +02:00
87d384dba5 Decouple RateProviderFactory with RateFetcher 2018-08-22 16:53:40 +09:00
4f5a8f7953 Use direct provider for Kraken, update packages 2018-08-21 15:59:57 +09:00
8728356698 Use HttpClientFactory for coinaverage 2018-08-21 14:33:13 +09:00
9c30476fc8 Making BTCPayServer a bit faster when creating invoices 2018-08-21 13:54:52 +09:00
09beb57eaf Fixing csproj 2018-08-17 16:27:37 +02:00
76a36d1829 Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer/BTCPayServer.csproj
2018-08-17 13:35:56 +02:00
0d4036efa2 Dynamically determining site url 2018-08-17 13:33:47 +02:00
cb4562aad5 Model validation attributes added for email and url 2018-08-17 13:26:33 +02:00
0084d4766b Server side validation of PayButton POST 2018-08-17 13:21:00 +02:00
74ddcfa01e Handling payment button post and providing test form 2018-08-17 12:38:03 +02:00
ed36fba0d7 Defining input names for validation errors 2018-08-17 10:30:49 +02:00
af015d435b Integrating Clipboard.js and implemeting copy code 2018-08-16 22:22:15 +02:00
ec59980e6f Validation and conditional rendering of Generated Code section 2018-08-16 21:55:24 +02:00
f0f4247c5d Field validation 2018-08-15 23:58:39 +02:00
b562094956 Currency dropdown as part of page model 2018-08-14 23:47:41 +02:00
4afd55c441 Rendering button code on load from srvModel 2018-08-14 23:47:28 +02:00
d7404f418d Returning intialized model and inputchange on all attribs 2018-08-14 19:40:57 +02:00
893410911c Button width influenced by generator 2018-08-14 18:59:59 +02:00
556b581b6a Updating display of generated HTML 2018-08-14 14:57:46 +02:00
1685ccaca8 Fixing success message for DNS 2018-08-13 17:04:37 +09:00
522abcfdfd Fix setting up DNS name 2018-08-13 16:48:10 +09:00
ed1827ff28 Fix not showing ssh service if no LND 2018-08-13 15:10:59 +09:00
214b2d1c1c Fix SSH fingerprint checking 2018-08-13 09:43:59 +09:00
322518e9dc Ensure the ssh connection is trusted 2018-08-12 23:23:26 +09:00
57daf27fdd Completion of UI for rest of Pay Button gneerator 2018-08-11 13:34:52 +02:00
e698d90e3c Pay Button page foundation 2018-08-10 20:26:51 +02:00
113 changed files with 10052 additions and 12857 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayServer.csproj
Configuration
Controllers
Data
Filters
HostedServices
Hosting
JsonConverters
Models
Payments
Rating
SSH
Security
Services
Views
ZoneLimits.cs
wwwroot
img/paybutton
paybutton
vendor/vuejs-vee-validate
README.md

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

@ -120,36 +120,67 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
if (MockRates)
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
}
}
public string HostName
@ -177,7 +208,7 @@ namespace BTCPayServer.Tests
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
}
if(storeId != null)
if (storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}

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

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

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

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

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

@ -14,6 +14,9 @@ using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
namespace BTCPayServer.Tests
{
@ -132,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

@ -40,6 +40,7 @@ using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
namespace BTCPayServer.Tests
{
@ -386,17 +387,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]
public void CanSetLightningServer()
{
@ -434,136 +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]
public void CanSendLightningPaymentCLightning()
public async Task CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.CLightning);
await ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
public void CanSendLightningPaymentCharge()
public async Task CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.Charge);
await ProcessLightningPayment(LightningConnectionType.LndREST);
}
[Fact]
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;
@ -576,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());
}
@ -849,6 +727,22 @@ namespace BTCPayServer.Tests
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
}
[Fact]
public void CanParseFingerprint()
{
Assert.True(SSH.SSHFingerprint.TryParse("4e343c6fc6cfbf9339c02d06a151e1dd", out var unused));
Assert.Equal("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", unused.ToString());
Assert.True(SSH.SSHFingerprint.TryParse("4e:34:3c:6f:c6:cf:bf:93:39:c0:2d:06:a1:51:e1:dd", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out unused));
Assert.Equal("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", unused.ToString());
Assert.True(SSH.SSHFingerprint.TryParse("Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w=", out var f1));
Assert.True(SSH.SSHFingerprint.TryParse("SHA256:Wl7CdRgT4u5T7yPMsxSrlFP+HIJJWwidGkzphJ8di5w", out var f2));
Assert.Equal(f1.ToString(), f2.ToString());
}
[Fact]
public void TestAccessBitpayAPI()
{
@ -927,8 +821,8 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
@ -939,7 +833,7 @@ namespace BTCPayServer.Tests
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -948,7 +842,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
Currency = "USD",
Currency = currency,
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
@ -962,12 +856,10 @@ namespace BTCPayServer.Tests
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
@ -978,7 +870,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -997,11 +889,9 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
var expectedRate = 5000.0m * 0.6m;
var expectedCoins = invoice2.Price / expectedRate;
Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
}
}
@ -1116,7 +1006,7 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);";
"X_X = coinaverage(X_X);";
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
@ -1428,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);
@ -1650,15 +1542,17 @@ namespace BTCPayServer.Tests
[Fact]
public void CanQueryDirectProviders()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
@ -1671,21 +1565,23 @@ namespace BTCPayServer.Tests
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
}
[Fact]
public void CanGetRateCryptoCurrenciesByDefault()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var pairs =
provider.GetAll()
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = factory.FetchRates(pairs, rules);
var result = fetcher.FetchRates(pairs, rules);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
@ -1693,46 +1589,94 @@ namespace BTCPayServer.Tests
}
}
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
private static RateProviderFactory CreateBTCPayRateFactory()
{
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
{
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
{
Hit = true;
var rates = new ExchangeRates();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates);
}
public void AssertHit()
{
Assert.True(Hit, "Should have hit the provider");
Hit = false;
}
public void AssertNotHit()
{
Assert.False(Hit, "Should have not hit the provider");
Hit = false;
}
}
[Fact]
public void CheckRatesProvider()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var coinAverage = new CoinAverageRateProvider();
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
var spy = new SpyRateProvider();
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.CacheSpan = TimeSpan.FromSeconds(10);
var factory = CreateBTCPayRateFactory();
factory.Providers.Clear();
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetcher = new RateFetcher(factory);
Thread.Sleep(11000);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid);
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
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();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertHit();
fetch.GetRatesAsync().GetAwaiter().GetResult();
Thread.Sleep(1000);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)

@ -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
@ -63,7 +63,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.14
image: nicolasdorier/nbxplorer:1.0.2.31
ports:
- "32838:32838"
expose:
@ -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
@ -111,7 +111,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:634f19a7b230edc686be56ab950b80784e56252c-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -122,6 +122,7 @@ services:
announce-addr=customer_lightningd
log-level=debug
dev-broadcast-interval=1000
dev-bitcoind-poll=1
ports:
- "30992:9835" # api port
expose:
@ -153,7 +154,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:v0.6-dev
image: nicolasdorier/clightning:634f19a7b230edc686be56ab950b80784e56252c-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -213,6 +214,7 @@ services:
no-macaroons=1
debuglevel=debug
noencryptwallet=1
trickledelay=1000
ports:
- "53280:8080"
expose:
@ -237,6 +239,7 @@ services:
no-macaroons=1
debuglevel=debug
noencryptwallet=1
trickledelay=1000
ports:
- "53281:8080"
expose:

@ -2,9 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.88</Version>
<Version>1.0.2.105</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
@ -30,23 +33,25 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.0" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<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" />
<PackageReference Include="LedgerWallet" Version="2.0.0.1" />
<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.32" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="NBitcoin" Version="4.1.1.46" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.18" />
<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.16" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.17" />
<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" />
@ -116,7 +121,20 @@
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\SSHService.cshtml">
<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\Public\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">

@ -12,6 +12,9 @@ using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
using Renci.SshNet;
using NBitcoin.DataEncoders;
using BTCPayServer.SSH;
using BTCPayServer.Lightning;
namespace BTCPayServer.Configuration
{
@ -22,69 +25,6 @@ namespace BTCPayServer.Configuration
public string CookieFile { get; internal set; }
}
public class SSHSettings
{
public string Server { get; set; }
public int Port { get; set; } = 22;
public string KeyFile { get; set; }
public string KeyFilePassword { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public ConnectionInfo CreateConnectionInfo()
{
if (!string.IsNullOrEmpty(KeyFile))
{
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
}
else
{
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
}
}
public static SSHSettings ParseConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
else if(externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
}
public class BTCPayServerOptions
{
public NetworkType NetworkType
@ -181,11 +121,18 @@ namespace BTCPayServer.Configuration
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var sshSettings = SSHSettings.ParseConfiguration(conf);
if (!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile))
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
{
if (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
throw new ConfigException($"sshkeyfile does not exist");
int waitTime = 0;
while (!string.IsNullOrEmpty(sshSettings.KeyFile) && !File.Exists(sshSettings.KeyFile))
{
if(waitTime++ < 5)
System.Threading.Thread.Sleep(1000);
else
throw new ConfigException($"sshkeyfile does not exist");
}
if (sshSettings.Port > ushort.MaxValue ||
sshSettings.Port < ushort.MinValue)
throw new ConfigException($"ssh port is invalid");
@ -199,10 +146,20 @@ namespace BTCPayServer.Configuration
{
throw new ConfigException($"sshkeyfilepassword is invalid");
}
SSHSettings = sshSettings;
}
var fingerPrints = conf.GetOrDefault<string>("sshtrustedfingerprints", "");
if (!string.IsNullOrEmpty(fingerPrints))
{
foreach (var fingerprint in fingerPrints.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
if (!SSHFingerprint.TryParse(fingerprint, out var f))
throw new ConfigException($"Invalid ssh fingerprint format {fingerprint}");
TrustedFingerprints.Add(f);
}
}
RootPath = conf.GetOrDefault<string>("rootpath", "/");
if (!RootPath.StartsWith("/", StringComparison.InvariantCultureIgnoreCase))
RootPath = "/" + RootPath;
@ -210,6 +167,53 @@ namespace BTCPayServer.Configuration
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
{
var externalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var settings = new SSHSettings();
settings.Server = conf.GetOrDefault<string>("sshconnection", null);
if (settings.Server != null)
{
var parts = settings.Server.Split(':');
if (parts.Length == 2 && int.TryParse(parts[1], out int port))
{
settings.Port = port;
settings.Server = parts[0];
}
else
{
settings.Port = 22;
}
parts = settings.Server.Split('@');
if (parts.Length == 2)
{
settings.Username = parts[0];
settings.Server = parts[1];
}
else
{
settings.Username = "root";
}
}
else if (externalUrl != null)
{
settings.Port = 22;
settings.Username = "root";
settings.Server = externalUrl.DnsSafeHost;
}
settings.Password = conf.GetOrDefault<string>("sshpassword", "");
settings.KeyFile = conf.GetOrDefault<string>("sshkeyfile", "");
settings.KeyFilePassword = conf.GetOrDefault<string>("sshkeyfilepassword", "");
return settings;
}
internal bool IsTrustedFingerprint(byte[] fingerPrint, byte[] hostKey)
{
return TrustedFingerprints.Any(f => f.Match(fingerPrint, hostKey));
}
public string RootPath { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public ExternalServices ExternalServicesByCryptoCode { get; set; } = new ExternalServices();
@ -230,6 +234,7 @@ namespace BTCPayServer.Configuration
get;
set;
}
public List<SSHFingerprint> TrustedFingerprints { get; set; } = new List<SSHFingerprint>();
public SSHSettings SSHSettings
{
get;

@ -38,6 +38,7 @@ namespace BTCPayServer.Configuration
app.Option("--sshpassword", "SSH password to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfile", "SSH private key file to manage BTCPay (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshkeyfilepassword", "Password of the SSH keyfile (default: empty)", CommandOptionType.SingleValue);
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
foreach (var network in provider.GetAll())
{
var crypto = network.CryptoCode.ToLowerInvariant();

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

@ -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,139 +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<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())

@ -1,44 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
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;
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)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
}
public async Task<IActionResult> ListApps()
{
var apps = await GetAllApps();

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

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

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

@ -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
{
@ -34,7 +31,6 @@ namespace BTCPayServer.Controllers
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true,
IncludeEvents = true
@ -503,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);

@ -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
{
@ -49,7 +27,7 @@ namespace BTCPayServer.Controllers
{
InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider;
RateFetcher _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -62,7 +40,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
@ -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
@ -195,12 +175,9 @@ namespace BTCPayServer.Controllers
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
if (rateResult.ExchangeExceptions.Count != 0)
foreach (var ex in rateResult.ExchangeExceptions)
{
foreach (var ex in rateResult.ExchangeExceptions)
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
}).ToArray());
}
@ -210,6 +187,7 @@ namespace BTCPayServer.Controllers
try
{
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.BidAsk == null)
{
@ -220,7 +198,7 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);

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

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

@ -15,12 +15,12 @@ namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
BTCPayRateProviderFactory _RateProviderFactory;
RateFetcher _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public RateController(
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)

@ -24,6 +24,7 @@ using System.Net.Mail;
using System.Threading.Tasks;
using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
namespace BTCPayServer.Controllers
{
@ -33,14 +34,14 @@ namespace BTCPayServer.Controllers
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private readonly NBXplorerDashboard _dashBoard;
private BTCPayRateProviderFactory _RateProviderFactory;
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
LightningConfigurationProvider lnConfigProvider,
@ -180,6 +181,8 @@ namespace BTCPayServer.Controllers
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
return View(vm);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
@ -200,12 +203,13 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
if (this.Request.Host.Port != null)
builder.Port = this.Request.Host.Port.Value;
builder.Path = "runid";
builder.Query = $"expected={RunId}";
var response = await client.GetAsync(builder.Uri);
if (!response.IsSuccessStatusCode)
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);
@ -260,6 +264,29 @@ namespace BTCPayServer.Controllers
ssh = $"sudo bash -c '. /etc/profile.d/btcpay-env.sh && nohup {ssh} > /dev/null 2>&1 & disown'";
var sshClient = _Options.SSHSettings == null ? vm.CreateSSHClient(this.Request.Host.Host)
: new SshClient(_Options.SSHSettings.CreateConnectionInfo());
if (_Options.TrustedFingerprints.Count != 0)
{
sshClient.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
{
if (_Options.TrustedFingerprints.Count == 0)
{
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
e.CanTrust = true; // Not a typo, we want the connection to succeed with a warning
}
else
{
e.CanTrust = _Options.IsTrustedFingerprint(e.FingerPrint, e.HostKey);
if(!e.CanTrust)
Logs.Configuration.LogError($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
}
else
{
}
try
{
sshClient.Connect();

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

@ -1,8 +1,13 @@
using BTCPayServer.Authentication;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Security;
@ -11,21 +16,13 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
@ -35,7 +32,7 @@ namespace BTCPayServer.Controllers
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
BTCPayRateProviderFactory _RateFactory;
RateFetcher _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
IServiceProvider serviceProvider,
@ -47,7 +44,7 @@ namespace BTCPayServer.Controllers
AccessTokenController tokenController,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
BTCPayRateProviderFactory rateFactory,
RateFetcher rateFactory,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
@ -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;
@ -521,7 +520,7 @@ namespace BTCPayServer.Controllers
private CoinAverageExchange[] GetSupportedExchanges()
{
return _RateFactory.GetSupportedExchanges()
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -767,5 +766,54 @@ namespace BTCPayServer.Controllers
return null;
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public IActionResult PayButton()
{
var store = StoreData;
var storeBlob = store.GetStoreBlob();
if (!storeBlob.AnyoneCanInvoice)
{
return View("PayButtonEnable", null);
}
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
Price = 10,
Currency = DEFAULT_CURRENCY,
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id
};
return View(model);
}
[HttpPost]
[Route("{storeId}/paybutton")]
public async Task<IActionResult> PayButton(bool enableStore)
{
var blob = StoreData.GetStoreBlob();
blob.AnyoneCanInvoice = enableStore;
if (StoreData.SetStoreBlob(blob))
{
await _Repo.UpdateStore(StoreData);
StatusMessage = "Store successfully updated";
}
return RedirectToAction(nameof(PayButton), new
{
storeId = StoreData.Id
});
}
}
}

@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
BTCPayRateProviderFactory _RateProvider;
RateFetcher _RateProvider;
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)

@ -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,10 +197,13 @@ 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)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
@ -302,6 +306,8 @@ namespace BTCPayServer.Data
public string RateScript { get; set; }
public bool AnyoneCanInvoice { get; set; }
string _LightningDescriptionTemplate;
public string LightningDescriptionTemplate

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

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

@ -10,6 +10,7 @@ using Microsoft.Extensions.Hosting;
using System.Threading;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using NBitcoin.DataEncoders;
namespace BTCPayServer.HostedServices
{
@ -30,6 +31,14 @@ namespace BTCPayServer.HostedServices
{
Logs.Configuration.LogInformation($"SSH settings detected, testing connection to {_options.SSHSettings.Username}@{_options.SSHSettings.Server} on port {_options.SSHSettings.Port} ...");
var connection = new Renci.SshNet.SshClient(_options.SSHSettings.CreateConnectionInfo());
connection.HostKeyReceived += (object sender, Renci.SshNet.Common.HostKeyEventArgs e) =>
{
e.CanTrust = true;
if (!_options.IsTrustedFingerprint(e.FingerPrint, e.HostKey))
{
Logs.Configuration.LogWarning($"SSH host fingerprint for {e.HostKeyName} is untrusted, start BTCPay with -sshtrustedfingerprints \"{Encoders.Hex.EncodeData(e.FingerPrint)}\"");
}
};
try
{
connection.Connect();

@ -18,9 +18,9 @@ namespace BTCPayServer.HostedServices
{
private SettingsRepository _SettingsRepository;
private CoinAverageSettings _coinAverageSettings;
BTCPayRateProviderFactory _RateProviderFactory;
RateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo,
BTCPayRateProviderFactory rateProviderFactory,
RateProviderFactory rateProviderFactory,
CoinAverageSettings coinAverageSettings)
{
this._SettingsRepository = repo;
@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices
return new[]
{
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings)
CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates)
};
}
async Task RefreshRates()
{
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
try
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
{
if (t.Result.Exception != null)
{
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
}
}, TaskScheduler.Default))
.ToArray()).WithCancellation(timeout.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
}
}
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
}
async Task RefreshCoinAverageSupportedExchanges()
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach(var item in tickers
foreach (var item in tickers
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{
@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSettings()
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))

@ -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
{
@ -53,6 +54,7 @@ namespace BTCPayServer.Hosting
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o);
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -86,7 +88,6 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
@ -94,6 +95,8 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<AppsHelper>();
services.TryAddSingleton<LightningConfigurationProvider>();
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
@ -137,7 +140,8 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<BTCPayRateProviderFactory>();
services.TryAddSingleton<RateProviderFactory>();
services.TryAddSingleton<RateFetcher>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();
@ -154,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;
});
@ -163,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;
}

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

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

@ -15,6 +15,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
}

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.SSH;
using Renci.SshNet;
namespace BTCPayServer.Models.ServerViewModels

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
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; }
public string OrderId { get; set; }
public int ButtonSize { get; set; }
[Url]
public string ServerIpn { get; set; }
[Url]
public string BrowserRedirect { get; set; }
[EmailAddress]
public string NotifyEmail { get; set; }
public string StoreId { get; set; }
// Data that influences Pay Button UI, but not invoice creation
public string UrlRoot { get; set; }
public List<string> CurrencyDropdown { get; set; }
public string PayButtonImageUrl { get; set; }
}
}

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

@ -27,16 +27,30 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
class Prepare
{
public Task<FeeRate> GetFeeRate;
public Task<BitcoinAddress> ReserveAddress;
}
public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
return new Prepare()
{
GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase)
};
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
var prepare = (Prepare)preparePaymentObject;
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = await getFeeRate;
onchainMethod.FeeRate = await prepare.GetFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
onchainMethod.DepositAddress = (await getAddress).ToString();
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
return onchainMethod;
}
}

@ -20,23 +20,46 @@ namespace BTCPayServer.Payments
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
/// <summary>
/// This method called before the rate have been fetched
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
object PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network);
}
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
public virtual object PreparePayment(T supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
return null;
}
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, store, network);
return PreparePayment(method, store, network);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, store, network, preparePaymentObject);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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,21 +17,18 @@ 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)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
var storeBlob = store.GetStoreBlob();
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;
}
}

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

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

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

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

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

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

@ -221,6 +221,16 @@ namespace BTCPayServer.Rating
}
public class ExchangeRate
{
public ExchangeRate()
{
}
public ExchangeRate(string exchange, CurrencyPair currencyPair, BidAsk bidAsk)
{
this.Exchange = exchange;
this.CurrencyPair = currencyPair;
this.BidAsk = bidAsk;
}
public string Exchange { get; set; }
public CurrencyPair CurrencyPair { get; set; }
public BidAsk BidAsk { get; set; }

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.SSH
{
public class SSHFingerprint
{
public static bool TryParse(string str, out SSHFingerprint fingerPrint)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
fingerPrint = null;
str = str.Trim();
try
{
var shortFingerprint = str.Replace(":", "", StringComparison.OrdinalIgnoreCase);
if (HexEncoder.IsWellFormed(shortFingerprint))
{
var hash = Encoders.Hex.DecodeData(shortFingerprint);
if (hash.Length == 16)
{
fingerPrint = new SSHFingerprint(hash);
return true;
}
return false;
}
}
catch
{
}
if (str.StartsWith("SHA256:", StringComparison.OrdinalIgnoreCase))
str = str.Substring("SHA256:".Length).Trim();
if (str.Contains(':', StringComparison.OrdinalIgnoreCase))
return false;
if (!str.EndsWith('='))
str = str + "=";
try
{
var hash = Encoders.Base64.DecodeData(str);
if (hash.Length == 32)
{
fingerPrint = new SSHFingerprint(hash);
return true;
}
}
catch
{
}
return false;
}
public SSHFingerprint(byte[] hash)
{
if (hash.Length == 16)
{
_ShortFingerprint = hash;
_Original = string.Join(':', hash.Select(b => b.ToString("x2", CultureInfo.InvariantCulture))
.ToArray());
}
else if (hash.Length == 32)
{
_FullHash = hash;
_Original = "SHA256:" + Encoders.Base64.EncodeData(hash);
if (_Original.EndsWith("=", StringComparison.OrdinalIgnoreCase))
_Original = _Original.Substring(0, _Original.Length - 1);
}
else
throw new ArgumentException(paramName:nameof(hash), message: "Invalid length, expected 16 or 32");
}
byte[] _ShortFingerprint;
byte[] _FullHash;
public bool Match(byte[] shortFingerprint, byte[] hostKey)
{
if (shortFingerprint == null)
throw new ArgumentNullException(nameof(shortFingerprint));
if (hostKey == null)
throw new ArgumentNullException(nameof(hostKey));
if (_ShortFingerprint != null)
return Utils.ArrayEqual(shortFingerprint, _ShortFingerprint);
return Utils.ArrayEqual(_FullHash, NBitcoin.Crypto.Hashes.SHA256(hostKey));
}
string _Original;
public override string ToString()
{
return _Original;
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Renci.SshNet;
namespace BTCPayServer.SSH
{
public class SSHSettings
{
public string Server { get; set; }
public int Port { get; set; } = 22;
public string KeyFile { get; set; }
public string KeyFilePassword { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public ConnectionInfo CreateConnectionInfo()
{
if (!string.IsNullOrEmpty(KeyFile))
{
return new ConnectionInfo(Server, Port, Username, new[] { new PrivateKeyAuthenticationMethod(Username, new PrivateKeyFile(KeyFile, KeyFilePassword)) });
}
else
{
return new ConnectionInfo(Server, Port, Username, new[] { new PasswordAuthenticationMethod(Username, Password) });
}
}
}
}

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

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

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

@ -1,215 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class ExchangeException
{
public Exception Exception { get; set; }
public string ExchangeName { get; set; }
}
public class RateResult
{
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public BidAsk BidAsk { get; set; }
public bool Cached { get; internal set; }
}
public class BTCPayRateProviderFactory
{
class QueryRateResult
{
public bool CachedResult { get; set; }
public List<ExchangeException> Exceptions { get; set; }
public ExchangeRates ExchangeRates { get; set; }
}
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
CoinAverageSettings _CoinAverageSettings;
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
BTCPayNetworkProvider btcpayNetworkProvider,
CoinAverageSettings coinAverageSettings)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_CoinAverageSettings = coinAverageSettings;
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
this.btcpayNetworkProvider = btcpayNetworkProvider;
InitExchanges();
}
public bool UseCoinAverageAsFallback { get; set; } = true;
private void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings });
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
}
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders
{
get
{
return _DirectProviders;
}
}
BTCPayNetworkProvider btcpayNetworkProvider;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
_Cache = new MemoryCache(_CacheOptions);
}
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
{
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
}
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{
var dependentQueries = new List<Task<QueryRateResult>>();
foreach (var requiredExchange in i.RateRule.ExchangeRates)
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = QueryRates(requiredExchange.Exchange);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);
}
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
}
return fetchingRates;
}
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{
var result = new RateResult();
result.Cached = true;
foreach (var queryAsync in dependentQueries)
{
var query = await queryAsync;
if (!query.CachedResult)
result.Cached = false;
result.ExchangeExceptions.AddRange(query.Exceptions);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);
return result;
}
private async Task<QueryRateResult> QueryRates(string exchangeName)
{
List<IRateProvider> providers = new List<IRateProvider>();
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
providers.Add(directProvider);
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
{
providers.Add(new CoinAverageRateProvider()
{
Exchange = exchangeName,
Authenticator = _CoinAverageSettings
});
}
var fallback = new FallbackRateProvider(providers.ToArray());
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
{
CacheSpan = CacheSpan
};
var value = await cached.GetRatesAsync();
return new QueryRateResult()
{
CachedResult = !fallback.Used,
ExchangeRates = value,
Exceptions = fallback.Exceptions
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
};
}
}
}

@ -0,0 +1,163 @@
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
{
public class BackgroundFetcherRateProvider : IRateProvider
{
public class LatestFetch
{
public ExchangeRates Latest;
public DateTimeOffset NextRefresh;
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
internal ExchangeRates GetResult()
{
if (Expiration <= DateTimeOffset.UtcNow)
{
if (Exception != null)
{
ExceptionDispatchInfo.Capture(Exception).Throw();
}
else
{
throw new InvalidOperationException($"The rate has expired ({ExchangeName})");
}
}
return Latest;
}
}
IRateProvider _Inner;
public BackgroundFetcherRateProvider(IRateProvider inner)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
_Inner = inner;
}
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)
return DateTimeOffset.UtcNow;
return latest.NextRefresh;
}
}
public bool DoNotAutoFetchIfExpired { get; set; }
public async Task<LatestFetch> UpdateIfNecessary()
{
if (NextUpdate <= DateTimeOffset.UtcNow)
{
try
{
await Fetch();
}
catch { } // Exception is inside _Latest
return _Latest;
}
return _Latest;
}
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync()
{
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)
{
fetch.Latest = previous.Latest;
fetch.Expiration = previous.Expiration;
}
else
{
fetch.Expiration = DateTimeOffset.UtcNow;
}
fetch.NextRefresh = DateTimeOffset.UtcNow;
fetch.Exception = ex;
}
_Latest = fetch;
fetch.GetResult(); // Will throw if not valid
return fetch;
}
public void InvalidateCache()
{
_Latest = null;
}
}
}

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

@ -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,14 +31,14 @@ namespace BTCPayServer.Services.Rates
}
}
public string ExchangeName { get; set; }
public string ExchangeName { get; }
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
public Task<ExchangeRates> GetRatesAsync()
{

@ -49,13 +49,26 @@ namespace BTCPayServer.Services.Rates
Task AddHeader(HttpRequestMessage message);
}
public class CoinAverageRateProvider : IRateProvider
public class CoinAverageRateProvider : IRateProvider, IHasExchangeName
{
public const string CoinAverageName = "coinaverage";
public CoinAverageRateProvider()
{
}
public HttpClient HttpClient
{
get
{
return _LocalClient ?? _Client;
}
set
{
_LocalClient = null;
}
}
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
public string Exchange { get; set; } = CoinAverageName;
@ -69,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;
@ -107,7 +122,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
using (resp)
{
@ -150,7 +165,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
@ -162,7 +177,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetRateLimitsResponse();
@ -193,7 +208,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -9,7 +10,7 @@ using ExchangeSharp;
namespace BTCPayServer.Services.Rates
{
public class ExchangeSharpRateProvider : IRateProvider
public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName
{
readonly ExchangeAPI _ExchangeAPI;
readonly string _ExchangeName;
@ -28,6 +29,8 @@ namespace BTCPayServer.Services.Rates
get; set;
}
public string ExchangeName => _ExchangeName;
public async Task<ExchangeRates> GetRatesAsync()
{
await new SynchronizationContextRemover();
@ -43,17 +46,17 @@ namespace BTCPayServer.Services.Rates
}
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
HashSet<string> notFoundSymbols = new HashSet<string>();
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
{
if (notFoundSymbols.Contains(ticker.Key))
if (notFoundSymbols.ContainsKey(ticker.Key))
return null;
try
{
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
if (!CurrencyPair.TryParse(tickerName, out var pair))
{
notFoundSymbols.Add(ticker.Key);
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
return null;
}
if(ReverseCurrencyPair)
@ -66,7 +69,7 @@ namespace BTCPayServer.Services.Rates
}
catch (ArgumentException)
{
notFoundSymbols.Add(ticker.Key);
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
return null;
}
}

@ -9,7 +9,6 @@ namespace BTCPayServer.Services.Rates
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public bool Used { get; set; }
public FallbackRateProvider(IRateProvider[] providers)
{
if (providers == null)
@ -19,7 +18,6 @@ namespace BTCPayServer.Services.Rates
public async Task<ExchangeRates> GetRatesAsync()
{
Used = true;
foreach (var p in _Providers)
{
try

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

@ -0,0 +1,138 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName
{
public KrakenExchangeRateProvider()
{
_Helper = new ExchangeKrakenAPI();
}
ExchangeKrakenAPI _Helper;
public HttpClient HttpClient
{
get
{
return _LocalClient ?? _Client;
}
set
{
_LocalClient = null;
}
}
public string ExchangeName => "kraken";
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;
public async Task<ExchangeRates> GetRatesAsync()
{
var result = new ExchangeRates();
var symbols = await GetSymbolsAsync();
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList);
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, new Dictionary<string, object> { { "pair", csvPairsList } });
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
foreach (string symbol in symbols)
{
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);
if (ticker != null)
{
try
{
var global = _Helper.ExchangeSymbolToGlobalSymbol(symbol);
if (CurrencyPair.TryParse(global, out var pair))
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
else
notFoundSymbols.TryAdd(symbol, symbol);
}
catch (ArgumentException)
{
notFoundSymbols.TryAdd(symbol, symbol);
}
}
}
return result;
}
private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker)
{
if (ticker == null)
return null;
decimal last = ticker["c"][0].ConvertInvariant<decimal>();
return new ExchangeTicker
{
Ask = ticker["a"][0].ConvertInvariant<decimal>(),
Bid = ticker["b"][0].ConvertInvariant<decimal>(),
Last = last,
Volume = new ExchangeVolume
{
BaseVolume = ticker["v"][1].ConvertInvariant<decimal>(),
BaseSymbol = symbol,
ConvertedVolume = ticker["v"][1].ConvertInvariant<decimal>() * last,
ConvertedSymbol = symbol,
Timestamp = DateTime.UtcNow
}
};
}
private async Task<string[]> GetSymbolsAsync()
{
if (_LastSymbolUpdate != null && DateTimeOffset.UtcNow - _LastSymbolUpdate.Value < TimeSpan.FromDays(0.5))
{
return _Symbols;
}
else
{
JToken json = await MakeJsonRequestAsync<JToken>("/0/public/AssetPairs");
var symbols = (from prop in json.Children<JProperty>() where !prop.Name.Contains(".d", StringComparison.OrdinalIgnoreCase) select prop.Name).ToArray();
_Symbols = symbols;
_LastSymbolUpdate = DateTimeOffset.UtcNow;
return symbols;
}
}
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
{
StringBuilder sb = new StringBuilder();
sb.Append("https://api.kraken.com");
;
sb.Append(url);
if (payload != null)
{
sb.Append("?");
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
}
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
var response = await HttpClient.SendAsync(request);
string stringResult = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<T>(stringResult);
if (result is JToken json)
{
if (!(json is JArray) && json["error"] is JArray error && error.Count != 0)
{
throw new APIException(error[0].ToStringInvariant());
}
result = (T)(object)(json["result"] ?? json);
}
return result;
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class NullRateProvider : IRateProvider
{
private NullRateProvider()
{
}
private static readonly NullRateProvider _Instance = new NullRateProvider();
public static NullRateProvider Instance
{
get
{
return _Instance;
}
}
public Task<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(new ExchangeRates());
}
}
}

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

@ -0,0 +1,94 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using static BTCPayServer.Services.Rates.RateProviderFactory;
namespace BTCPayServer.Services.Rates
{
public class ExchangeException
{
public Exception Exception { get; set; }
public string ExchangeName { get; set; }
}
public class RateResult
{
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public BidAsk BidAsk { get; set; }
public TimeSpan Latency { get; internal set; }
}
public class RateFetcher
{
private readonly RateProviderFactory _rateProviderFactory;
public RateFetcher(RateProviderFactory rateProviderFactory)
{
_rateProviderFactory = rateProviderFactory;
}
public RateProviderFactory RateProviderFactory => _rateProviderFactory;
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
{
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
}
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{
var dependentQueries = new List<Task<QueryRateResult>>();
foreach (var requiredExchange in i.RateRule.ExchangeRates)
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);
}
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
}
return fetchingRates;
}
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{
var result = new RateResult();
foreach (var queryAsync in dependentQueries)
{
var query = await queryAsync;
result.Latency = query.Latency;
if (query.Exception != null)
result.ExchangeExceptions.Add(query.Exception);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);
return result;
}
}
}

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class RateProviderFactory
{
class WrapperRateProvider : IRateProvider
{
private readonly IRateProvider _inner;
public Exception Exception { get; private set; }
public TimeSpan Latency { get; set; }
public WrapperRateProvider(IRateProvider inner)
{
_inner = inner;
}
public async Task<ExchangeRates> GetRatesAsync()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return await _inner.GetRatesAsync();
}
catch (Exception ex)
{
Exception = ex;
return new ExchangeRates();
}
finally
{
Latency = DateTimeOffset.UtcNow - now;
}
}
}
public class QueryRateResult
{
public TimeSpan Latency { get; set; }
public ExchangeRates ExchangeRates { get; set; }
public ExchangeException Exception { get; internal set; }
}
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
IHttpClientFactory httpClientFactory,
CoinAverageSettings coinAverageSettings)
{
_httpClientFactory = httpClientFactory;
_CoinAverageSettings = coinAverageSettings;
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
}
private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
var cache = new MemoryCache(_CacheOptions);
foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null))
{
provider.CacheSpan = CacheSpan;
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;
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> Providers
{
get
{
return _DirectProviders;
}
}
private void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
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() });
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
foreach (var provider in Providers.ToArray())
{
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
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;
}
var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Key))
{
var coinAverage = new CoinAverageRateProvider()
{
Exchange = supportedExchange.Key,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
};
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Key, cached);
}
}
}
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
public async Task<QueryRateResult> QueryRates(string exchangeName)
{
Providers.TryGetValue(exchangeName, out var directProvider);
directProvider = directProvider ?? NullRateProvider.Instance;
var wrapper = new WrapperRateProvider(directProvider);
var value = await wrapper.GetRatesAsync();
return new QueryRateResult()
{
Latency = wrapper.Latency,
ExchangeRates = value,
Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
};
}
}
}

@ -83,7 +83,7 @@ namespace BTCPayServer.Services
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellation.Register(() =>
{
try

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

@ -1,4 +1,5 @@
@model ListAppsViewModel
@using BTCPayServer.Services.Apps
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
}
@ -52,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>

@ -1,6 +1,6 @@
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model ViewPointOfSaleViewModel
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
ViewData["Title"] = Model.Title;
Layout = null;

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

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

@ -0,0 +1,20 @@
@{
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h2>Pay Button request failed</h2>
Please fix following errors:
<ul>
@foreach (var error in allErrors)
{
<li>@error</li>
}
</ul>
</div>
</div>
</div>
</section>

@ -13,49 +13,46 @@
</div>
<div class="row">
@if(Model.LNDServices.Count != 0)
{
<div class="col-md-8">
<div class="form-group">
<h5>LND nodes</h5>
<span>You can get access to internal LND services here. For gRPC, only BTC is supported.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
@if(Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-8">
<div class="form-group">
<span>You can get access here to LND-gRPC or SSH services exposed by your server</span>
</div>
}
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
@if(Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section Scripts {

@ -26,12 +26,14 @@
@* JS *@
<bundle name="wwwroot/bundles/main-bundle.min.js" />
@RenderSection("HeadScripts", required: false)
</head>
<body id="page-top">
@{
if(ViewBag.AlwaysShrinkNavBar == null)
if (ViewBag.AlwaysShrinkNavBar == null)
{
ViewBag.AlwaysShrinkNavBar = true;
}
@ -43,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>
}
@ -53,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>
}
@ -71,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>
}
@ -86,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>
@ -94,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" />
}

@ -3,7 +3,6 @@
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
}
<section>
<div class="container">
<div class="row">
@ -16,7 +15,7 @@
<div>
<div class="row">
<div class="col-md-3">
@if(ViewBag.ShowMenu)
@if (ViewBag.ShowMenu)
{
@await Html.PartialAsync("_Nav")
}
@ -28,6 +27,10 @@
</div>
</div>
</section>
@section HeadScripts {
@RenderSection("HeadScripts", required: false)
}
@section Scripts {
@RenderSection("Scripts", required: false)
}

@ -1,8 +1,7 @@
@model DerivationSchemeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = $"{Model.CryptoCode} Derivation scheme";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
ViewData.SetActivePageAndTitle(StoreNavPages.Index, $"{Model.CryptoCode} Derivation scheme");
}
<partial name="_StatusMessage" for="StatusMessage" />

@ -1,8 +1,7 @@
@model LightningNodeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Add lightning node (Experimental)";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Add lightning node (Experimental)");
}
<h4>@ViewData["Title"]</h4>

@ -1,8 +1,7 @@
@model CheckoutExperienceViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Checkout experience";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Checkout);
ViewData.SetActivePageAndTitle(StoreNavPages.Checkout, "Checkout experience");
}
<h4>@ViewData["Title"]</h4>

@ -1,8 +1,7 @@
@model CreateTokenViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Create a new token";
ViewData.AddActivePage(StoreNavPages.Tokens);
ViewData.SetActivePageAndTitle(StoreNavPages.Tokens, "Create a new token");
ViewBag.HidePublicKey = ViewBag.HidePublicKey ?? false;
ViewBag.ShowStores = ViewBag.ShowStores ?? false;
}

@ -1,8 +1,7 @@
@model TokensViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Access Tokens";
ViewData.AddActivePage(StoreNavPages.Tokens);
ViewData.SetActivePageAndTitle(StoreNavPages.Tokens, "Access Tokens");
}
<partial name="_StatusMessage" for="StatusMessage" />

@ -0,0 +1,154 @@
@model PayButtonViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton);
ViewBag.MainTitle = "Pay Button";
}
<div class="container" id="payButtonCtrl">
<div class="row">
<div class="col-lg-7">
<div class="form-row">
<div class="form-group col-md-8">
<label>Price</label>
<input name="price" type="text" class="form-control"
v-model="srvModel.price" v-on:change="inputChanges"
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
<small class="text-danger">{{ errors.first('price') }}</small>
</div>
<div class="form-group col-md-4">
<label>&nbsp;</label>
<input name="price" type="text" class="form-control"
v-model="srvModel.currency" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('currency') }">
</div>
</div>
<div class="form-group">
<label>Checkout Description</label>
<input name="checkoutDesc" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.checkoutDesc" v-on:change="inputChanges">
</div>
<div class="form-group">
<label>Order Id</label>
<input name="orderId" type="text" class="form-control" id="inputAddress" placeholder="(optional)"
v-model="srvModel.orderId" v-on:change="inputChanges">
</div>
<div class="form-group">
<label>Pay Button Image Url</label>
<input name="payButtonImageUrl" type="text" class="form-control" id="inputAddress"
v-model="srvModel.payButtonImageUrl" v-on:change="inputChanges"
v-validate="'required|url'" :class="{'is-invalid': errors.has('payButtonImageUrl') }">
<small class="text-danger">{{ errors.first('payButtonImageUrl') }}</small>
</div>
<div class="form-group">
<label>Button Size</label>
<div style="vertical-align:top; font-size:12px; display:flex;">
<button class="btn" style="width:95px;height:40px;margin-right:40px;"
v-on:click="inputChanges($event, 0)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 0) }">
146 x 40 px
</button>
<button class="btn btn-default" style="width:126px;height:46px;margin-right:40px;"
v-on:click="inputChanges($event, 1)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 1) }">
168 x 46 px
</button>
<button class="btn btn-default" style="width:146px;height:57px;"
v-on:click="inputChanges($event, 2)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 2) }">
209 x 57 px
</button>
</div>
</div>
</div>
<div class="col-lg-5">
<br />
Define parameters that define the purchase: price, currency and then optional description of purchase.
<br /><br />
Generated HTML will be displayed at the bottom of this page and all you need is to paste that HTML into your final page.
Clicking on the button will redirect customer to checkout.
</div>
</div>
<hr />
<h3>Payment Notifications</h3>
<br />
<div class="row">
<div class="col-lg-7">
<div class="form-group">
<label>Server IPN</label>
<input name="serverIpn" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.serverIpn" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
</div>
<div class="form-group">
<label>Send Email Notifications to</label>
<input name="notifyEmail" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
v-validate="'email'" :class="{'is-invalid': errors.has('notifyEmail') }">
<small class="text-danger">{{ errors.first('notifyEmail') }}</small>
</div>
<div class="form-group">
<label>Browser Redirect</label>
<input name="browserRedirect" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.browserRedirect" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('browserRedirect') }">
<small class="text-danger">{{ errors.first('browserRedirect') }}</small>
</div>
</div>
<div class="col-lg-5">
<br />
These parameters allow you to influence process after purchase. <i>Server IPN</i> is location we'll query with details.
We can also deliver email notification to specified addres.
<br /><br />
Finally <i>Browser Redirect</i> defines where BtcPayServer will redirect customer after puchase is completed.
</div>
</div>
<hr />
<h3>Generated code</h3>
<div class="row" v-show="!errors.any()">
<div class="col-lg-9">
<pre><code id="mainCode" class="html"></code></pre>
<button class="btn btn-primary" id="copyCode">
<i class="fa fa-copy"></i> Copy Code
</button>
<span class="copyLabelPopup" style="display:none;">Copied</span>
</div>
<div class="col-lg-3">
<div id="preview"></div>
</div>
</div>
<div class="row" v-show="errors.any()">
<div class="col-lg-12 text-danger">
Please fix errors shown in order for code generation to successfully execute.
</div>
</div>
@section HeadScripts {
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script src="~/vendor/vuejs/vue.js"></script>
<script src="~/vendor/vuejs-vee-validate/vee-validate.js"></script>
<script src="~/vendor/clipboard.js/clipboard.js"></script>
<script src="~/paybutton/paybutton.js"></script>
}
@section Scripts {
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var payButtonCtrl = new Vue({
el: '#payButtonCtrl',
data: {
srvModel: srvModel
},
methods: {
inputChanges: function (event, buttonSize) {
inputChanges(event, buttonSize);
}
}
});
</script>
}

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