Compare commits

...

84 Commits

Author SHA1 Message Date
718a36ddd0 Remove dev time stuff 2018-03-22 01:10:14 +09:00
c0b903d79c Wallet page is now an action link in the store settings 2018-03-22 01:07:11 +09:00
48eaf906b0 Update text in AddLightningNode 2018-03-21 23:51:28 +09:00
f42fde970a bump 2018-03-21 15:07:26 +09:00
710dbb51f4 Remove useless code 2018-03-21 03:11:03 +09:00
d426d66819 fill exisitng values in AddDerivationScheme and AddLightningNode 2018-03-21 03:05:51 +09:00
265cddc38b Change the UX to set lightning node or derivation schemes 2018-03-21 02:48:11 +09:00
21b91ac8f7 Add btc.lightning and ltc.lightning 2018-03-21 02:09:25 +09:00
e8730f74be update docker-compose 2018-03-21 01:10:10 +09:00
6d611d7d05 Can connect directly to CLightning via TCP or UNIX socket 2018-03-21 00:31:19 +09:00
392f3a16f1 Introduce LightningClientFactory 2018-03-20 12:10:35 +09:00
2b2e12b290 Abstract ChargeClient to prepare for support of other lightning implementation 2018-03-20 11:59:43 +09:00
73cc75fe66 Fixing display bug when invoice is paid 2018-03-19 11:16:57 +09:00
cc186fc8b3 Fix bug: Incorrect confirmation count in Invoice screen under some circumstances. 2018-03-19 09:45:54 +09:00
632ad81b94 update .net core 2018-03-19 00:44:12 +09:00
1d243910ae bump 2018-03-18 15:29:21 +09:00
e624649cd8 Merge branch 'lepipele-dev-shapeshift' 2018-03-18 15:28:42 +09:00
e6ca07e9b5 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-18 00:42:55 -05:00
b3d6435772 Custom text in Conversion tab in case of BTC LN payment 2018-03-18 00:41:16 -05:00
806474c8c6 Allow account selection of the ledger 2018-03-18 14:15:23 +09:00
1524fb4499 Merge remote-tracking branch 'source/master' into dev-shapeshift 2018-03-17 23:50:11 -05:00
14b70ff35e Rendering of Conversion tab on Invoice if enabled in store settings 2018-03-17 23:49:09 -05:00
c36a900627 Store setting for allowing conversion through Shapeshift 2018-03-17 23:48:06 -05:00
d3befb5b86 Fixing slider style when there are only two tabs 2018-03-17 23:36:32 -05:00
7f0ce1f802 Changing text to clarify usage 2018-03-17 23:28:39 -05:00
8342ad9175 Remove reference to browser mode of ledger 2018-03-18 12:58:14 +09:00
acb2407654 Fix bug: Paying a lightning invoice might miss 1 satoshi due to rounding error 2018-03-18 02:26:33 +09:00
b8a4f0c012 fix tests 2018-03-18 01:59:16 +09:00
57bb3b231c Add linux script for manual testing 2018-03-17 19:40:23 +09:00
e5d626e0fd Remove useless stuff in command line for tests 2018-03-17 19:35:37 +09:00
e2c4c913ff Remove hard coded container names in test docker-compose 2018-03-17 19:33:36 +09:00
09f97915d6 Fix charge listener bug, and decouple charge from clightning in test docker compose 2018-03-17 19:26:30 +09:00
81328b2667 Update charge in tests and fix two build time warnings 2018-03-17 17:49:42 +09:00
0d8affc68d Remove dependency on Eclair for tests 2018-03-17 17:02:47 +09:00
da77d278fb Adding Shapeshift button in new Altcoins tab 2018-03-16 23:46:39 -05:00
5e9f6f3542 Refactoring jquery logic for tab switching
Need to be more general to incorporate third tab
2018-03-16 23:15:01 -05:00
f337470f09 Adding styles for third tab - altcoin payments 2018-03-16 23:14:13 -05:00
636224d0c8 Checkout html and js cleanup 2018-03-16 22:46:30 -05:00
b28b3ef4ff Fix: Invoice can't be paid in lightning anymore if lightning server sent error 2018-03-14 20:10:04 +09:00
9e2e102ec4 Fix bug making it impossible to remove LTC xpub 2018-03-14 19:32:24 +09:00
9e16b83202 Testing Shapeshift integration 2018-03-13 12:20:22 -05:00
cbd40d49c1 bump 2018-03-13 15:56:17 +09:00
1d051648b7 Merge pull request #60 from lepipele/dev-lepi
Bugfixing loading spinner when switching currency
2018-03-13 15:41:22 +09:00
49cf804914 bump 2018-03-13 15:39:52 +09:00
0f6ad75536 Remove internal exception thrown by NBitcoin 2018-03-13 15:28:39 +09:00
56eea18b2d Bugfixing loading spinner when switching currency
Moving it to buttons so it directly interacts with actions and doesn't break form states
2018-03-13 00:34:26 -05:00
b3698846c6 Improve UX of invoice list and invoice details 2018-03-13 09:13:16 +09:00
6806d96baa Listen to all derivation schemes 2018-03-12 19:02:03 +09:00
dc3b3077c2 Add text align for rate in invoice detail page 2018-03-12 11:02:02 +09:00
936ae64ca3 Allow connection via non https lightning charge node through localhost or 127.0.0.1 2018-03-11 15:14:05 +09:00
3a0a5dbd7f Accept all success HTTP code for invoice callbacks 2018-03-07 14:22:02 -05:00
ed4430ae7d bump 2018-03-07 07:49:46 -05:00
9a0e4e35d9 Merge pull request #56 from lepipele/dev-lepi
Tweaking Checkout page so that it works properly in IE
2018-03-07 07:48:52 -05:00
5715dd2058 Disabling AJAX caching that messes up checkout in IE
Ref: https://stackoverflow.com/questions/4303829/how-to-prevent-a-jquery-ajax-request-from-caching-in-internet-explorer
2018-03-06 22:04:03 -06:00
da4c132f9d Adding Vue.js binding attributes 2018-03-06 22:02:34 -06:00
303a617f9e Improve invoice.cshtml display if offchain payment is present 2018-03-06 16:37:25 -05:00
3116ec9cb8 Fix bitcoin logo for internet explorer, update nbxplorer 2018-03-06 10:41:41 -05:00
b3f4eab075 fix doc 2018-03-06 09:40:21 -05:00
c98593b47b add dependencies to README 2018-03-06 09:35:20 -05:00
2c49d61682 shebang and fix line ending 2018-03-06 09:23:08 -05:00
21f5c94cff update readme 2018-03-06 09:18:00 -05:00
834ba4afab chmod +x scripts 2018-03-06 09:15:09 -05:00
d690cd6b7b Add build and run scripts 2018-03-06 09:14:45 -05:00
937bd07daa add doc 2018-03-05 10:58:27 -05:00
559b822111 fix broken expiration screen on checkout 2018-03-03 20:33:52 -05:00
7afb4c6b11 bump 2018-03-03 15:08:11 -05:00
1c98a3a33d Rename currency selection with "Pay With" 2018-03-03 15:07:34 -05:00
3320eb284e Merge branch 'lepipele-dev-lepi' 2018-03-03 15:06:58 -05:00
919fb60558 Hiding currency selection when invoice paid 2018-03-03 00:52:49 -06:00
7796589105 Extracting public methods in core.js 2018-03-03 00:41:52 -06:00
de6d3198ff Bundling JS and CSS files for Checkout.cshtml
Now we'll finally have versioning so when those JS/CSS files update, clients will properly request new bundle
2018-03-03 00:32:51 -06:00
f1e971d047 Refactoring core.js in preparation for bundling
Moving Vue registration to body for quick update of page
Removing defer dependancy for core.js
2018-03-03 00:32:04 -06:00
acd98aad32 Showing loader for better UX when switching currencies 2018-03-03 00:11:08 -06:00
b0c810398c Moving currency selection to order details
This way state transitions of form are now properly preserved
2018-03-02 23:49:51 -06:00
03a0044745 Currency selection moved to top of the form 2018-03-02 23:42:17 -06:00
15684efdce Update README.md 2018-03-02 15:04:00 -05:00
b67a962d12 Make sure the txrelayfee is correctly set 2018-03-02 14:16:16 -05:00
339cedadf7 Save a call to nbxplorer.GetStatus, update NBXplorer 2018-03-02 14:03:47 -05:00
e19d730fb7 Merge pull request #54 from practicalswift/typos
Fix typos
2018-03-03 02:43:16 +09:00
649497e54f Fix typos 2018-03-01 15:11:30 +01:00
40eee0924a bump 2018-03-01 11:48:55 +09:00
bbf5fb3c30 show port if failing to connect to lightning node 2018-03-01 10:31:01 +09:00
346cdf2431 show block gap if lightning node is not synched 2018-02-28 23:12:09 +09:00
030cb09af8 Fix bug of address parsing for lightning 2018-02-28 22:56:12 +09:00
97 changed files with 2150 additions and 1338 deletions

17
.gitattributes vendored Normal file
View File

@ -0,0 +1,17 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Declare files that will always have CRLF line endings on checkout.
*.sh text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

View File

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

View File

@ -1,5 +1,7 @@
using BTCPayServer.Configuration;
using BTCPayServer.Hosting;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Tests.Logging;
@ -84,7 +86,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
config.AppendLine($"internallightningnode={IntegratedLightning.AbsoluteUri}");
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
@ -118,6 +120,7 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
}
public string HostName
@ -127,6 +130,7 @@ namespace BTCPayServer.Tests
}
public InvoiceRepository InvoiceRepository { get; private set; }
public Uri IntegratedLightning { get; internal set; }
public bool InContainer { get; internal set; }
public T GetService<T>()
{

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Eclair;
using NBitcoin;
namespace BTCPayServer.Tests
{
public class EclairTester
{
ServerTester parent;
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
{
this.parent = parent;
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}
public EclairRPCClient RPC { get; }
public string P2PHost { get; }
NodeInfo _NodeInfo;
public async Task<NodeInfo> GetNodeInfoAsync()
{
if (_NodeInfo != null)
return _NodeInfo;
var info = await RPC.GetInfoAsync();
_NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port);
return _NodeInfo;
}
public NodeInfo GetNodeInfo()
{
return GetNodeInfoAsync().GetAwaiter().GetResult();
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using NBitcoin;
namespace BTCPayServer.Tests
{
public class LightningDTester
{
ServerTester parent;
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);
}
public CLightningRPCClient RPC { get; }
public string P2PHost { get; }
}
}

View File

@ -26,7 +26,7 @@ docker-compose down
If you want to stop, and remove all existing data
```
docker-compose down -v
docker-compose down --v
```
You can run the tests inside a container by running
@ -35,11 +35,13 @@ You can run the tests inside a container by running
docker-compose run --rm tests
```
## Send commands to bitcoind
## How to manually test payments
### Using the test bitcoin-cli
You can call bitcoin-cli inside the container with `docker exec`, for example, if you want to send `0.23111090` to `mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf`:
```
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
./docker-bitcoin-cli.sh sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
If you are using Powershell:
@ -47,7 +49,29 @@ If you are using Powershell:
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
```
For sending to litecoin, use .\docker-litecoin-cli.ps1 instead.
### Using the test litecoin-cli
Same as bitcoin-cli, but with `.\docker-litecoin-cli.ps1` and `.\docker-litecoin-cli.sh` instead.
### Using the test lightning-cli
If you are using Linux:
```
./docker-customer-lightning-cli.sh pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you are using Powershell:
```
.\docker-customer-lightning-cli.ps1 pay lnbcrt100u1pd2e6uspp5ajnadvhazjrz55twd5k6yeg9u87wpw0q2fdr7g960yl5asv5fmnqdq9d3hkccqpxmedyrk0ehw5ueqx5e0r4qrrv74cewddfcvsxaawqz7634cmjj39sqwy5tvhz0hasktkk6t9pqfdh3edmf3z09zst5y7khv3rvxh8ctqqw6mwhh
```
If you get this message:
```
{ "code" : 205, "message" : "Could not find a route", "data" : { "getroute_tries" : 1, "sendpay_tries" : 0 } }
```
Please, run the test `CanSetLightningServer`, this will establish a channel between the customer and the merchant, then, retry.
## FAQ

View File

@ -17,8 +17,9 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Payments.Lightning.Eclair;
using System.Globalization;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.Charge;
namespace BTCPayServer.Tests
{
@ -56,8 +57,10 @@ 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;
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
@ -68,60 +71,71 @@ namespace BTCPayServer.Tests
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
PayTester.Start();
}
/// <summary>
/// This will setup a channel going from customer to merchant
/// Connect a customer LN node to the merchant LN node
/// </summary>
public void PrepareLightning()
{
PrepareLightningAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Connect a customer LN node to the merchant LN node
/// </summary>
/// <returns></returns>
public async Task PrepareLightningAsync()
{
// Activate segwit
var blockCount = ExplorerNode.GetBlockCountAsync();
// Fetch node info, but that in cache
var merchantInfo = MerchantCharge.Client.GetInfoAsync();
var customer = CustomerEclair.GetNodeInfoAsync();
var channels = CustomerEclair.RPC.ChannelsAsync();
var info = await merchantInfo;
var clightning = new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port);
var connect = CustomerEclair.RPC.ConnectAsync(clightning);
await Task.WhenAll(blockCount, customer, channels, connect);
// If the channel is not created, let's do it
if (channels.Result.Length == 0)
while (true)
{
var c = (await CustomerEclair.RPC.ChannelsAsync());
bool generated = false;
bool createdChannel = false;
CancellationTokenSource timeout = new CancellationTokenSource();
timeout.CancelAfter(10000);
while (c.Length == 0 || c[0].State != "NORMAL")
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
var channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.Where(c => !skippedStates.Contains(c.State ?? ""))
.FirstOrDefault();
switch (channel?.State)
{
if (timeout.IsCancellationRequested)
{
timeout = new CancellationTokenSource();
timeout.CancelAfter(10000);
createdChannel = c.Length == 0;
generated = false;
}
if (!createdChannel)
{
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
createdChannel = true;
}
if (!generated && c.Length != 0 && c[0].State == "WAIT_FOR_FUNDING_CONFIRMED")
{
ExplorerNode.Generate(6);
generated = true;
}
c = (await CustomerEclair.RPC.ChannelsAsync());
case null:
var merchantInfo = await WaitLNSynched();
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
await CustomerLightningD.ConnectAsync(clightning);
var address = await CustomerLightningD.NewAddressAsync();
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
ExplorerNode.Generate(1);
await WaitLNSynched();
await Task.Delay(1000);
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
break;
case "CHANNELD_AWAITING_LOCKIN":
ExplorerNode.Generate(1);
await WaitLNSynched();
break;
case "CHANNELD_NORMAL":
return;
default:
throw new NotSupportedException(channel?.State ?? "");
}
}
}
private async Task<GetInfoResponse> WaitLNSynched()
{
while (true)
{
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
var blockCount = await ExplorerNode.GetBlockCountAsync();
if (merchantInfo.BlockHeight != blockCount)
{
await Task.Delay(1000);
}
else
{
return merchantInfo;
}
}
}
@ -135,11 +149,11 @@ namespace BTCPayServer.Tests
{
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
await CustomerEclair.RPC.SendAsync(bolt11);
await CustomerLightningD.SendAsync(bolt11);
}
public EclairTester MerchantEclair { get; set; }
public EclairTester CustomerEclair { get; set; }
public CLightningRPCClient CustomerLightningD { get; set; }
public CLightningRPCClient MerchantLightningD { get; private set; }
public ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)

View File

@ -1,4 +1,5 @@
using BTCPayServer.Controllers;
using System.Linq;
using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Invoices;
@ -11,6 +12,8 @@ using System.Text;
using System.Threading.Tasks;
using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Tests
{
@ -77,11 +80,10 @@ namespace BTCPayServer.Tests
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
});
}, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
@ -111,20 +113,23 @@ namespace BTCPayServer.Tests
{
get; set;
}
public void RegisterLightningNode(string cryptoCode)
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
{
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
}
public async Task RegisterLightningNodeAsync(string cryptoCode)
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
{
var storeController = parent.PayTester.GetController<StoresController>(UserId);
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
}, "save");
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
: throw new NotSupportedException(connectionType.ToString())
}, "save", "BTC");
if (storeController.ModelState.ErrorCount != 0)
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
}
}
}
}

View File

@ -22,7 +22,6 @@ using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Payments.Lightning.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
@ -45,7 +44,7 @@ namespace BTCPayServer.Tests
[Fact]
public void CanCalculateCryptoDue2()
{
var dummy = new Key().PubKey.GetAddress(Network.RegTest);
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
#pragma warning disable CS0618
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
@ -304,24 +303,95 @@ namespace BTCPayServer.Tests
user.GrantAccess();
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "test").GetAwaiter().GetResult();
}, "test", "BTC").GetAwaiter().GetResult();
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
Assert.True(storeController.ModelState.IsValid);
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
{
CryptoCurrency = "BTC",
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
}, "save").GetAwaiter().GetResult());
}, "save", "BTC").GetAwaiter().GetResult());
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
Assert.Single(storeVm.LightningNodes);
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
}
}
[Fact]
public void CanParseLightningURL()
{
LightningConnectionString conn = null;
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
Assert.Equal("unix://test/a", conn.ToString());
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
Assert.Equal("tcp://test/a", conn.ToString());
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
Assert.Equal("aaa", conn.Username);
Assert.Equal("bbb", conn.Password);
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
}
[Fact]
public void CanSendLightningPayment2()
{
using (var tester = ServerTester.Create())
{
tester.Start();
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.01,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>
{
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("complete", localInvoice.Status);
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
});
}
}
@ -335,7 +405,7 @@ namespace BTCPayServer.Tests
tester.PrepareLightning();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterLightningNode("BTC");
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
@ -347,7 +417,6 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description"
});
tester.SendLightningPayment(invoice);
Eventually(() =>

View File

@ -1 +1 @@
docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"

View File

@ -1,7 +1,7 @@
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
services:
@ -17,14 +17,19 @@ services:
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
TEST_ECLAIR: http://eclair-cli:gpwefwmmewci@eclair:8080/
TEST_CHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
TESTS_INCONTAINER: "true"
expose:
- "80"
links:
- dev
extra_hosts:
- "tests:127.0.0.1"
volumes:
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
dev:
@ -36,11 +41,12 @@ services:
links:
- nbxplorer
- postgres
- eclair
- customer_lightningd
- merchant_lightningd
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.16
image: nicolasdorier/nbxplorer:1.0.1.24
ports:
- "32838:32838"
expose:
@ -64,7 +70,6 @@ services:
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
@ -75,11 +80,6 @@ services:
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
# Eclair is still using addwitnessaddress
deprecatedrpc=addwitnessaddress
ports:
- "43782:43782"
expose:
@ -88,8 +88,29 @@ services:
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
ipaddr=customer_lightningd
log-level=debug
ports:
- "30992:9835" # api port
expose:
- "9735" # server port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "customer_lightningd_datadir:/root/.lightning"
links:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.1
image: shesek/lightning-charge:0.3.5
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -97,6 +118,8 @@ services:
BITCOIND_RPCCONNECT: bitcoind
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "lightning_charge_datadir:/data"
- "merchant_lightningd_datadir:/etc/lightning"
expose:
- "9112" # Charge
- "9735" # Lightning
@ -104,33 +127,31 @@ services:
- "54938:9112" # Charge
links:
- bitcoind
- merchant_lightningd
eclair:
image: acinq/eclair@sha256:758eaf02683046a096ee03390d3a54df8fcfca50883f7560ab946a36ee4e81d8
environment:
JAVA_OPTS: >
-Xmx512m
-Declair.printToConsole
-Declair.bitcoind.host=bitcoind
-Declair.bitcoind.rpcport=43782
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
-Declair.bitcoind.rpcpassword=DwubwWsoo3
-Declair.bitcoind.zmq=tcp://bitcoind:29000
-Declair.api.enabled=true
-Declair.api.password=gpwefwmmewci
-Declair.chain=regtest
-Declair.api.binding-ip=0.0.0.0
links:
- bitcoind
merchant_lightningd:
image: nicolasdorier/clightning
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
ipaddr=merchant_lightningd
network=regtest
log-level=debug
ports:
- "30992:8080" # api port
- "30993:9835" # api port
expose:
- "9735" # server port
- "8080" # api port
- "9835" # api port
volumes:
- "bitcoin_datadir:/etc/bitcoin"
- "merchant_lightningd_datadir:/root/.lightning"
links:
- bitcoind
litecoind:
container_name: btcpayserver_dev_litecoind
image: nicolasdorier/docker-litecoin:0.14.2
image: nicolasdorier/docker-litecoin:0.15.1
environment:
BITCOIN_EXTRA_ARGS: |
rpcuser=ceiwHEbqWI83
@ -155,3 +176,6 @@ services:
volumes:
bitcoin_datadir:
customer_lightningd_datadir:
merchant_lightningd_datadir:
lightning_charge_datadir:

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"

View File

@ -1 +1 @@
docker exec -ti btcpayserver_dev_litecoind litecoin-cli -regtest -conf="/data/litecoin.conf" -datadir="/data" $args
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"

View File

@ -0,0 +1 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args

View File

@ -0,0 +1,3 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"

View File

@ -68,7 +68,6 @@ namespace BTCPayServer
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public string CLightningNetworkName { get; internal set; }
public override string ToString()
{

View File

@ -28,10 +28,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
LightningImagePath = "imlegacy/btc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
ChainType == ChainType.Test ? "testnet" :
ChainType == ChainType.Regtest ? "regtest" : null
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
});
}
}

View File

@ -27,9 +27,7 @@ namespace BTCPayServer
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
LightningImagePath = "imlegacy/ltc-lightning.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
ChainType == ChainType.Test ? "litecoin-testnet" : null
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
});
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.39</Version>
<Version>1.0.1.55</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -31,10 +31,10 @@
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.0.0.56" />
<PackageReference Include="NBitcoin" Version="4.0.0.65" />
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />

View File

@ -10,6 +10,7 @@ using System.Text;
using StandardConfiguration;
using Microsoft.Extensions.Configuration;
using NBXplorer;
using BTCPayServer.Payments.Lightning;
namespace BTCPayServer.Configuration
{
@ -73,6 +74,17 @@ namespace BTCPayServer.Configuration
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
if(lightning.Length != 0)
{
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
@ -80,10 +92,12 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
InternalLightningNode = conf.GetOrDefault<Uri>("internallightningnode", null);
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if(old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
}
public Uri InternalLightningNode { get; set; }
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
public BTCPayNetworkProvider NetworkProvider { get; set; }
public string PostgresConnectionString

View File

@ -36,9 +36,9 @@ namespace BTCPayServer.Configuration
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
}
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
app.Option("--internallightningnode", $"An internal lightning node which can be used without https requirement and easily configured by the admin (default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
return app;
}
@ -105,6 +105,8 @@ namespace BTCPayServer.Configuration
{
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
}
return builder.ToString();
}

View File

@ -68,16 +68,16 @@ namespace BTCPayServer.Controllers
{
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if(onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
if (onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
@ -86,25 +86,29 @@ namespace BTCPayServer.Controllers
var payments = invoice
.GetPayments()
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(async payment =>
{
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
paymentData.ConfirmationCount = confirmationCount;
payment.SetCryptoPaymentData(paymentData);
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
}
else
{
confirmationCount = paymentData.ConfirmationCount;
}
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
@ -112,7 +116,7 @@ namespace BTCPayServer.Controllers
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
@ -121,12 +125,32 @@ namespace BTCPayServer.Controllers
})
.ToArray();
await Task.WhenAll(payments);
model.Addresses = invoice.HistoricalAddresses;
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
{
Destination = h.GetAddress(),
PaymentMethod = ToString(h.GetPaymentMethodId()),
Current = !h.UnAssigned.HasValue
}).ToArray();
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
model.StatusMessage = StatusMessage;
return View(model);
}
private string ToString(PaymentMethodId paymentMethodId)
{
var type = paymentMethodId.PaymentType.ToString();
switch (paymentMethodId.PaymentType)
{
case PaymentTypes.BTCLike:
type = "On-Chain";
break;
case PaymentTypes.LightningLike:
type = "Off-Chain";
break;
}
return $"{paymentMethodId.CryptoCode} ({type})";
}
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
@ -166,7 +190,7 @@ namespace BTCPayServer.Controllers
return null;
if (!invoice.Support(paymentMethodId))
{
if(!isDefaultCrypto)
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
@ -207,20 +231,21 @@ namespace BTCPayServer.Controllers
TxCount = accounting.TxRequired,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
AllowCoinConversion = store.GetStoreBlob().AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv=> new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.Select(kv => new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetpaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
var isMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
@ -348,6 +373,7 @@ namespace BTCPayServer.Controllers
Date = Prettify(invoice.InvoiceTime),
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
@ -361,7 +387,7 @@ namespace BTCPayServer.Controllers
{
var ago = DateTime.UtcNow - invoiceTime;
if(ago.TotalMinutes < 1)
if (ago.TotalMinutes < 1)
{
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
}

View File

@ -368,7 +368,7 @@ namespace BTCPayServer.Controllers
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
@ -387,7 +387,7 @@ namespace BTCPayServer.Controllers
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);

View File

@ -20,40 +20,53 @@ namespace BTCPayServer.Controllers
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
{
selectedScheme = selectedScheme ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
vm.CryptoCode = cryptoCode;
SetExistingValues(store, vm);
return View(vm);
}
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm)
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = GetStoreUrl(storeId);
vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_ExplorerProvider, vm.CryptoCurrency);
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
return NotFound();
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
return NotFound();
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
@ -65,7 +78,6 @@ namespace BTCPayServer.Controllers
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
vm.DerivationScheme = strategy.ToString();
}
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
}
catch
{
@ -75,7 +87,7 @@ namespace BTCPayServer.Controllers
}
if (vm.Confirmation)
if (vm.Confirmation || strategy == null)
{
try
{
@ -130,6 +142,8 @@ namespace BTCPayServer.Controllers
string command,
// getinfo
string cryptoCode = null,
// getxpub
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
@ -197,7 +211,7 @@ namespace BTCPayServer.Controllers
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for substract fees"); }
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
@ -205,7 +219,9 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
result = await hw.GetExtPubKey(network);
var getxpubResult = await hw.GetExtPubKey(network, account); ;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
result = getxpubResult;
}
if (command == "getinfo")
{
@ -224,17 +240,20 @@ namespace BTCPayServer.Controllers
if (command == "sendtoaddress")
{
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
changeAddress.Item2);
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });

View File

@ -8,38 +8,65 @@ using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning.CLightning;
using Microsoft.AspNetCore.Mvc;
using BTCPayServer.Payments.Lightning;
using System.Net;
using BTCPayServer.Data;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
{
selectedCrypto = selectedCrypto ?? "BTC";
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
SetExistingValues(store, vm);
return View(vm);
}
[HttpPost]
[Route("{storeId}/lightning")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<LightningSupportedPaymentMethod>()
.FirstOrDefault(d => d.PaymentId == id);
return existing;
}
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
{
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
{
return CanUseInternalLightning() ? connectionString : null;
}
return null;
}
[HttpPost]
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
{
vm.CryptoCode = cryptoCode;
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
if (network == null || network.CLightningNetworkName == null)
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
var internalLightning = GetInternalLighningNode(network.CryptoCode);
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
if (network == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
return View(vm);
}
@ -47,40 +74,39 @@ namespace BTCPayServer.Controllers
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
if (!string.IsNullOrEmpty(vm.Url))
{
Uri uri;
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
{
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
return View(vm);
}
if (uri.Scheme != "https")
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
isLocal;
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
{
var internalNode = GetInternalLightningNodeIfAuthorized();
if (internalNode == null || GetDomain(internalNode) != GetDomain(uri.AbsoluteUri))
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);
}
}
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
if (isInternalNode && !CanUseInternalLightning())
{
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
return View(vm);
}
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
{
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
return View(vm);
}
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
{
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningChargeUrl(uri);
paymentMethod.SetLightningUrl(connectionString);
}
if (command == "save")
{
@ -111,24 +137,9 @@ namespace BTCPayServer.Controllers
}
}
private string GetInternalLightningNodeIfAuthorized()
{
if (_BtcpayServerOptions.InternalLightningNode != null &&
CanUseInternalLightning())
{
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
}
return null;
}
private bool CanUseInternalLightning()
{
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
}
string GetDomain(string uri)
{
return new UriBuilder(uri).Host;
}
}
}

View File

@ -1,6 +1,7 @@
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
@ -31,6 +32,7 @@ namespace BTCPayServer.Controllers
public partial class StoresController : Controller
{
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
@ -45,6 +47,7 @@ namespace BTCPayServer.Controllers
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
{
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
@ -59,6 +62,7 @@ namespace BTCPayServer.Controllers
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
}
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
@ -106,15 +110,15 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
[Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
model.CryptoCurrency = cryptoCode;
return View(model);
}
@ -218,30 +222,39 @@ namespace BTCPayServer.Controllers
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
return View(vm);
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
foreach(var strategy in store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>())
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.ToDictionary(c => c.Network.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = strategy.PaymentId.CryptoCode,
Value = strategy.DerivationStrategyBase.ToString()
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
});
}
foreach(var lightning in store
var lightningByCryptoCode = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>()
.ToDictionary(c => c.CryptoCode);
foreach (var network in _NetworkProvider.GetAll())
{
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = lightning.CryptoCode,
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
});
}
}
@ -294,6 +307,7 @@ namespace BTCPayServer.Controllers
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.AllowCoinConversion = model.AllowCoinConversion;
if (store.SetStoreBlob(blob))
{
@ -516,7 +530,7 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
StatusMessage = "Pairing is successfull";
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new

View File

@ -33,7 +33,7 @@ namespace BTCPayServer.Data
}
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
{
Address = address + "#" + paymentMethodId?.ToString();
Address = address + "#" + paymentMethodId.ToString();
return this;
}
public PaymentMethodId GetpaymentMethodId()

View File

@ -27,9 +27,10 @@ namespace BTCPayServer.Data
public string CryptoCode { get; set; }
#pragma warning disable CS0618
public string GetCryptoCode()
public Payments.PaymentMethodId GetPaymentMethodId()
{
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
return string.IsNullOrEmpty(CryptoCode) ? new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)
: Payments.PaymentMethodId.Parse(CryptoCode);
}
public string GetAddress()
{

View File

@ -118,7 +118,7 @@ namespace BTCPayServer.Data
{
DerivationStrategy = null;
}
else if (!existing)
else if (!existing && supportedPaymentMethod != null)
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
DerivationStrategies = strategies.ToString();
#pragma warning restore CS0618
@ -213,6 +213,10 @@ namespace BTCPayServer.Data
{
get; set;
}
public bool AllowCoinConversion
{
get; set;
}
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration

View File

@ -77,7 +77,7 @@ namespace BTCPayServer
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
}
public BTCPayNetwork GetNetwork(string cryptoCode)

View File

@ -30,6 +30,19 @@ namespace BTCPayServer
{
public static class Extensions
{
public static decimal RoundUp(decimal value, int precision)
{
for (int i = 0; i < precision; i++)
{
value = value * 10m;
}
value = Math.Ceiling(value);
for (int i = 0; i < precision; i++)
{
value = value / 10m;
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));

View File

@ -75,7 +75,8 @@ namespace BTCPayServer.HostedServices
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
await SendNotification(invoice, eventCode, name, cts.Token);
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
@ -112,7 +113,7 @@ namespace BTCPayServer.HostedServices
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)

View File

@ -41,9 +41,11 @@ namespace BTCPayServer.HostedServices
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public bool IsFullySynched(string cryptoCode)
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
{
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
return _Summaries.TryGetValue(cryptoCode, out summary) &&
summary.Status != null &&
summary.Status.IsFullySynched;
}
public IEnumerable<NBXplorerSummary> GetAll()

View File

@ -126,6 +126,7 @@ namespace BTCPayServer.Hosting
}
return dbContext;
});
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
@ -147,7 +148,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();

View File

@ -17,12 +17,15 @@ namespace BTCPayServer.JsonConverters
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 ? new LightMoney((long)reader.Value) :
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;
}

View File

@ -12,16 +12,22 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class CryptoPayment
{
public string CryptoCode { get; set; }
public string PaymentMethod { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
}
public class AddressModel
{
public string PaymentMethod { get; set; }
public string Destination { get; set; }
public bool Current { get; set; }
}
public class Payment
{
public string CryptoCode { get; set; }
public string PaymentMethod { get; set; }
public string Confirmations
{
get; set;
@ -126,7 +132,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}

View File

@ -39,6 +39,7 @@ namespace BTCPayServer.Models.InvoicingModels
}
public string OrderId { get; set; }
public string RedirectUrl { get; set; }
public string InvoiceId
{
get; set;

View File

@ -43,5 +43,7 @@ namespace BTCPayServer.Models.InvoicingModels
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
public bool AllowCoinConversion { get; set; }
}
}

View File

@ -41,26 +41,11 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
public string CryptoCode { get; set; }
public bool Confirmation { get; set; }
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -9,11 +9,6 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class LightningNodeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
[Display(Name = "Lightning charge url")]
public string Url
{
@ -21,24 +16,12 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Crypto currency")]
public string CryptoCurrency
public string CryptoCode
{
get;
set;
}
public SelectList CryptoCurrencies { get; set; }
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
{
var choices = networkProvider.GetAll()
.Where(n => n.CLightningNetworkName != null)
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -94,6 +94,12 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
public bool AllowCoinConversion
{
get; set;
}
public string StatusMessage
{
get; set;

View File

@ -10,25 +10,10 @@ namespace BTCPayServer.Models.StoreViewModels
public class WalletModel
{
public string ServerUrl { get; set; }
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

View File

@ -16,7 +16,7 @@ namespace BTCPayServer
/// there is functionality to allow adding or removing more than a single
/// value at once.
///
/// The MultiValueDictionary can also be viewed as a IReadOnlyDictionary&lt;TKey,IReadOnlyCollection&lt;TValue&gt;t&gt;
/// The MultiValueDictionary can also be viewed as an IReadOnlyDictionary&lt;TKey,IReadOnlyCollection&lt;TValue&gt;t&gt;
/// where the <see cref="IReadOnlyCollection{TValue}" /> is abstracted from the view of the programmer.
///
/// For a read-only MultiValueDictionary.

View File

@ -17,7 +17,7 @@ namespace BTCPayServer.Payments.Bitcoin
public string GetPaymentDestination()
{
return DepositAddress?.ToString();
return DepositAddress;
}
public decimal GetTxFee()
@ -33,10 +33,7 @@ namespace BTCPayServer.Payments.Bitcoin
public void SetPaymentDestination(string newPaymentDestination)
{
if (newPaymentDestination == null)
DepositAddress = null;
else
DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network);
DepositAddress = newPaymentDestination;
}
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
@ -45,7 +42,11 @@ namespace BTCPayServer.Payments.Bitcoin
[JsonIgnore]
public Money TxFee { get; set; }
[JsonIgnore]
public BitcoinAddress DepositAddress { get; set; }
public String DepositAddress { get; set; }
public BitcoinAddress GetDepositAddress(Network network)
{
return string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, network);
}
///////////////////////////////////////////////////////////////////////////////////////
}
}

View File

@ -34,7 +34,7 @@ namespace BTCPayServer.Payments.Bitcoin
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = await getFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
onchainMethod.DepositAddress = await getAddress;
onchainMethod.DepositAddress = (await getAddress).ToString();
return onchainMethod;
}

View File

@ -98,20 +98,6 @@ namespace BTCPayServer.Payments.Bitcoin
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
await Task.WhenAll(invoice.GetSupportedPaymentMethod<DerivationStrategy>(_NetworkProvider)
.Select(s => (Session: _SessionsByCryptoCode.TryGet(s.PaymentId.CryptoCode),
DerivationStrategy: s.DerivationStrategyBase))
.Where(s => s.Session != null)
.Select(s => s.Session.ListenDerivationSchemesAsync(new[] { s.DerivationStrategy }))
.ToArray()).ConfigureAwait(false);
}
}));
return Task.CompletedTask;
}
@ -139,7 +125,7 @@ namespace BTCPayServer.Payments.Bitcoin
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
await session.ListenAllDerivationSchemesAsync(cancellation: _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
int paymentCount = await FindPaymentViaPolling(wallet, network);
@ -174,7 +160,8 @@ namespace BTCPayServer.Payments.Bitcoin
if (!alreadyExist)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
if(payment != null)
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
}
else
{
@ -213,7 +200,7 @@ namespace BTCPayServer.Payments.Bitcoin
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
{
return invoice.GetPayments()
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
}
@ -227,7 +214,7 @@ namespace BTCPayServer.Payments.Bitcoin
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(wallet.Network))
{
if (payment.GetpaymentMethodId().PaymentType != PaymentTypes.BTCLike)
if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike)
continue;
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
@ -279,7 +266,7 @@ namespace BTCPayServer.Payments.Bitcoin
}
else
{
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
// Take the most recent (bitcoin node would not forward a conflict without a successful RBF)
_Winner = Transactions
.OrderByDescending(t => t.Value.Timestamp)
.First()
@ -344,7 +331,8 @@ namespace BTCPayServer.Payments.Bitcoin
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
if (payment != null)
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++;
}
}
@ -365,11 +353,11 @@ namespace BTCPayServer.Payments.Bitcoin
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
btc.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey &&
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.Output.ScriptPubKey &&
paymentMethod.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address;
btc.DepositAddress = address.ToString();
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);
@ -379,21 +367,6 @@ namespace BTCPayServer.Payments.Bitcoin
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
return invoice;
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var strategy = GetDerivationStrategy(invoice, network);
if (strategy != null)
strategies.Add(strategy);
}
return strategies;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();

View File

@ -0,0 +1,230 @@
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 BTCPayServer.Payments.Lightning.Charge;
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<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
{
return SendCommandAsync<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<ChargeInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
if (invoices.Length == 0)
return null;
return ChargeClient.ToLightningInvoice(invoices[0]);
}
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
{
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
var invoice = await SendCommandAsync<CreateInvoiceResponse>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
invoice.Label = id;
invoice.MilliSatoshi = amount;
invoice.Status = "unpaid";
return ToLightningInvoice(invoice);
}
private static LightningInvoice ToLightningInvoice(CreateInvoiceResponse 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 chargeInvoice = await SendCommandAsync<CreateInvoiceResponse>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
lastInvoiceIndex = chargeInvoice.PayIndex.Value;
return ToLightningInvoice(chargeInvoice);
}
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()
{
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
void IDisposable.Dispose()
{
}
}
}

View File

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

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Eclair
namespace BTCPayServer.Payments.Lightning.CLightning
{
public class NodeInfo
{

View File

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

View File

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

View File

@ -13,9 +13,9 @@ using NBitcoin;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeClient
public class ChargeClient : ILightningInvoiceClient
{
private Uri _Uri;
public Uri Uri
@ -126,5 +126,47 @@ namespace BTCPayServer.Payments.Lightning.CLightning
uri += "/";
return new Uri(uri + partialUrl);
}
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
{
var invoice = await GetInvoice(invoiceId, cancellation);
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, TimeSpan expiry, CancellationToken cancellation)
{
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
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()
{
P2PPort = port,
Address = address,
BlockHeight = info.BlockHeight
};
}
}
}

View File

@ -8,7 +8,7 @@ using System.Threading.Tasks;
using NBXplorer;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class ChargeInvoice
{
@ -27,8 +27,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
[JsonProperty("payreq")]
public string PaymentRequest { get; set; }
public string Label { get; set; }
}
public class ChargeSession : IDisposable
public class ChargeSession : ILightningListenInvoiceSession
{
private ClientWebSocket socket;
@ -42,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning.CLightning
}
ArraySegment<byte> _Buffer;
public async Task<ChargeInvoice> NextEvent(CancellationToken cancellation = default(CancellationToken))
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
{
var buffer = _Buffer;
var array = _Buffer.Array;
@ -120,5 +121,15 @@ namespace BTCPayServer.Payments.Lightning.CLightning
{
await this.socket.CloseSocket();
}
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
{
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
}
void IDisposable.Dispose()
{
Dispose();
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
namespace BTCPayServer.Payments.Lightning.Charge
{
public class CreateInvoiceRequest
{

View File

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

View File

@ -3,13 +3,20 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.CLightning
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 string[] Address { get; set; }
public GetInfoAddress[] Address { get; set; }
public string Version { get; set; }
public int BlockHeight { get; set; }
public string Network { get; set; }

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Eclair
{
public class AllChannelResponse
{
public string ShortChannelId { get; set; }
public string NodeId1 { get; set; }
public string NodeId2 { get; set; }
}
}

View File

@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning.Eclair
{
public class ChannelResponse
{
public string NodeId { get; set; }
public string ChannelId { get; set; }
public string State { get; set; }
}
public static class ChannelStates
{
public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED";
public const string NORMAL = "NORMAL";
}
}

View File

@ -1,252 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.JsonConverters;
using NBitcoin.RPC;
namespace BTCPayServer.Payments.Lightning.Eclair
{
public class SendResponse
{
public string PaymentHash { get; set; }
}
public class ChannelInfo
{
public string NodeId { get; set; }
public string ChannelId { get; set; }
public string State { get; set; }
}
public class EclairRPCClient
{
public EclairRPCClient(Uri address, Network network)
{
if (address == null)
throw new ArgumentNullException(nameof(address));
if (network == null)
throw new ArgumentNullException(nameof(network));
Address = address;
Network = network;
if (string.IsNullOrEmpty(address.UserInfo))
throw new ArgumentException(paramName: nameof(address), message: "User info in the url should be provided");
Password = address.UserInfo;
}
public string Password { get; set; }
public Network Network { get; private set; }
public GetInfoResponse GetInfo()
{
return GetInfoAsync().GetAwaiter().GetResult();
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", Array.Empty<object>()));
}
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
{
var response = await SendCommandAsync(request, throwIfRPCError);
return Serializer.ToObject<T>(response.ResultString, Network);
}
public async Task<RPCResponse> SendCommandAsync(RPCRequest request, bool throwIfRPCError = true)
{
RPCResponse response = null;
HttpWebRequest webRequest = response == null ? CreateWebRequest() : null;
if (response == null)
{
var writer = new StringWriter();
request.WriteJSON(writer);
writer.Flush();
var json = writer.ToString();
var bytes = Encoding.UTF8.GetBytes(json);
#if !(PORTABLE || NETCORE)
webRequest.ContentLength = bytes.Length;
#endif
var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false);
await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await dataStream.FlushAsync().ConfigureAwait(false);
dataStream.Dispose();
}
WebResponse webResponse = null;
WebResponse errorResponse = null;
try
{
webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null;
response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
catch (WebException ex)
{
if (ex.Response == null || ex.Response.ContentLength == 0 ||
!ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal))
throw;
errorResponse = ex.Response;
response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false));
if (throwIfRPCError)
response.ThrowIfError();
}
finally
{
if (errorResponse != null)
{
errorResponse.Dispose();
errorResponse = null;
}
if (webResponse != null)
{
webResponse.Dispose();
webResponse = null;
}
}
return response;
}
public AllChannelResponse[] AllChannels()
{
return AllChannelsAsync().GetAwaiter().GetResult();
}
public async Task<AllChannelResponse[]> AllChannelsAsync()
{
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
}
public ChannelInfo[] Channels()
{
return ChannelsAsync().GetAwaiter().GetResult();
}
public async Task<ChannelInfo[]> ChannelsAsync()
{
return await SendCommandAsync<ChannelInfo[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
}
public void Close(string channelId)
{
CloseAsync(channelId).GetAwaiter().GetResult();
}
public async Task SendAsync(string paymentRequest)
{
await SendCommandAsync<SendResponse>(new RPCRequest("send", new[] { paymentRequest })).ConfigureAwait(false);
}
public async Task CloseAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
try
{
await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false);
}
catch (RPCException ex) when (ex.Message == "closing already in progress")
{
}
}
public ChannelResponse Channel(string channelId)
{
return ChannelAsync(channelId).GetAwaiter().GetResult();
}
public async Task<ChannelResponse> ChannelAsync(string channelId)
{
if (channelId == null)
throw new ArgumentNullException(nameof(channelId));
return await SendCommandAsync<ChannelResponse>(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false);
}
public string[] AllNodes()
{
return AllNodesAsync().GetAwaiter().GetResult();
}
public async Task<string[]> AllNodesAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", Array.Empty<object>())).ConfigureAwait(false);
}
public Uri Address { get; private set; }
private HttpWebRequest CreateWebRequest()
{
var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri);
webRequest.ContentType = "application/json";
webRequest.Method = "POST";
var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(Password));
webRequest.Headers[HttpRequestHeader.Authorization] = $"Basic {auth}";
return webRequest;
}
private async Task<Stream> ToMemoryStreamAsync(Stream stream)
{
MemoryStream ms = new MemoryStream();
await stream.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
return ms;
}
public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult();
}
public string Connect(NodeInfo node)
{
return ConnectAsync(node).GetAwaiter().GetResult();
}
public async Task<string> ConnectAsync(NodeInfo node)
{
if (node == null)
throw new ArgumentNullException(nameof(node));
return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString;
}
public string Receive(LightMoney amount, string description = null)
{
return ReceiveAsync(amount, description).GetAwaiter().GetResult();
}
public async Task<string> ReceiveAsync(LightMoney amount, string description = null)
{
if (amount == null)
throw new ArgumentNullException(nameof(amount));
List<object> args = new List<object>();
args.Add(amount.MilliSatoshi);
if(description != null)
{
args.Add(description);
}
return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString;
}
public async Task<string> OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null)
{
if (fundingSatoshi == null)
throw new ArgumentNullException(nameof(fundingSatoshi));
if (node == null)
throw new ArgumentNullException(nameof(node));
pushAmount = pushAmount ?? LightMoney.Zero;
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
return result.ResultString;
}
}
}

View File

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning.Eclair
{
public class GetInfoResponse
{
public string NodeId { get; set; }
public string Alias { get; set; }
public int Port { get; set; }
public uint256 ChainHash { get; set; }
public int BlockHeight { get; set; }
}
}

View File

@ -0,0 +1,40 @@
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 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, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
}
public interface ILightningListenInvoiceSession : IDisposable
{
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
namespace BTCPayServer.Payments.Lightning
{
public class LightningClientFactory
{
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
var uri = supportedPaymentMethod.GetLightningUrl();
if (uri.ConnectionType == LightningConnectionType.Charge)
{
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
}
else if (uri.ConnectionType == LightningConnectionType.CLightning)
{
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
}
else
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
}
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments.Lightning
{
public enum LightningConnectionType
{
Charge,
CLightning
}
public class LightningConnectionString
{
public static bool TryParse(string str, out LightningConnectionString connectionString)
{
return TryParse(str, out connectionString, out var error);
}
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (str.StartsWith('/'))
str = "unix:" + str;
var result = new LightningConnectionString();
connectionString = null;
error = null;
Uri uri;
if (!System.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);
}
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];
}
else if (!string.IsNullOrEmpty(uri.UserInfo))
{
error = "The url should not have user information";
return false;
}
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
connectionString = result;
return true;
}
public LightningConnectionString()
{
}
public string Username { get; set; }
public string Password { get; set; }
public Uri BaseUri { get; set; }
public LightningConnectionType ConnectionType
{
get
{
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
: LightningConnectionType.CLightning;
}
}
public Uri ToUri(bool withCredentials)
{
if (withCredentials)
{
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
}
else
{
return BaseUri;
}
}
public override string ToString()
{
return ToUri(true).AbsoluteUri;
}
}
}

View File

@ -5,6 +5,8 @@ using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments.Lightning.Charge;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
@ -12,25 +14,27 @@ namespace BTCPayServer.Payments.Lightning
{
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
{
ExplorerClientProvider _ExplorerClientProvider;
public LightningLikePaymentHandler(ExplorerClientProvider explorerClientProvider)
NBXplorerDashboard _Dashboard;
LightningClientFactory _LightningClientFactory;
public LightningLikePaymentHandler(
LightningClientFactory lightningClientFactory,
NBXplorerDashboard dashboard)
{
_ExplorerClientProvider = explorerClientProvider;
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
var invoice = paymentMethod.ParentEntity;
var due = invoice.ProductInformation.Price / paymentMethod.Rate;
var client = GetClient(supportedPaymentMethod, network);
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
{
Amont = new LightMoney(due, LightMoneyUnit.BTC),
Expiry = expiry < TimeSpan.Zero ? TimeSpan.FromSeconds(1) : expiry
});
if (expiry < TimeSpan.Zero)
expiry = TimeSpan.FromSeconds(1);
var lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
return new LightningLikePaymentMethodDetails()
{
BOLT11 = lightningInvoice.PayReq,
BOLT11 = lightningInvoice.BOLT11,
InvoiceId = lightningInvoice.Id
};
}
@ -45,54 +49,50 @@ namespace BTCPayServer.Payments.Lightning
catch { return false; }
}
/// <summary>
/// Used for testing
/// </summary>
public bool SkipP2PTest { get; set; }
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
if (!_ExplorerClientProvider.IsAvailable(network))
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"Full node not available");
var explorerClient = _ExplorerClientProvider.GetExplorerClient(network);
var cts = new CancellationTokenSource(5000);
var client = GetClient(supportedPaymentMethod, network);
var status = explorerClient.GetStatusAsync();
GetInfoResponse info = null;
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
LightningNodeInformation info = null;
try
{
info = await client.GetInfoAsync(cts.Token);
info = await client.GetInfo(cts.Token);
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
}
var address = info.Address?.FirstOrDefault();
var port = info.Port;
address = address ?? client.Uri.DnsSafeHost;
if (info.Network != network.CLightningNetworkName)
{
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
throw new Exception($"Error while connecting to the API ({ex.Message})");
}
if (Math.Abs(info.BlockHeight - (await status).ChainHeight) > 10)
if(info.Address == null)
{
throw new Exception($"The lightning node is not synched");
throw new Exception($"No lightning node public address has been configured");
}
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
if (blocksGap > 10)
{
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
}
try
{
await TestConnection(address, port, cts.Token);
if(!SkipP2PTest)
await TestConnection(info.Address, info.P2PPort, cts.Token);
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the lightning node via {address} ({ex.Message})");
throw new Exception($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
}
}
private static ChargeClient GetClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
}
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
{
IPAddress address = null;

View File

@ -6,14 +6,13 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Logging;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Services.Invoices;
using Microsoft.Extensions.Hosting;
using NBXplorer;
namespace BTCPayServer.Payments.Lightning
{
public class ChargeListener : IHostedService
public class LightningListener : IHostedService
{
class ListenedInvoice
{
@ -28,13 +27,16 @@ namespace BTCPayServer.Payments.Lightning
EventAggregator _Aggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
public ChargeListener(EventAggregator aggregator,
LightningClientFactory _LightningClientFactory;
public LightningListener(EventAggregator aggregator,
InvoiceRepository invoiceRepository,
LightningClientFactory lightningClientFactory,
BTCPayNetworkProvider networkProvider)
{
_Aggregator = aggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_LightningClientFactory = lightningClientFactory;
}
CompositeDisposable leases = new CompositeDisposable();
@ -62,7 +64,6 @@ namespace BTCPayServer.Payments.Lightning
{
if (Listening(invoiceId))
return;
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
@ -78,7 +79,7 @@ namespace BTCPayServer.Payments.Lightning
var listenedInvoice = new ListenedInvoice()
{
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
PaymentMethodDetails = lightningMethod,
SupportedPaymentMethod = lightningSupportedMethod,
PaymentMethod = paymentMethod,
@ -88,11 +89,11 @@ namespace BTCPayServer.Payments.Lightning
if (poll)
{
var charge = GetChargeClient(lightningSupportedMethod, network);
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
if (chargeInvoice == null)
continue;
if(chargeInvoice.Status == "paid")
if (chargeInvoice.Status == "paid")
await AddPayment(network, chargeInvoice, listenedInvoice);
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
continue;
@ -124,18 +125,17 @@ namespace BTCPayServer.Payments.Lightning
{
try
{
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
var charge = GetChargeClient(supportedPaymentMethod, network);
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
var session = await charge.Listen(_Cts.Token);
while (true)
{
var notification = await session.NextEvent(_Cts.Token);
var notification = await session.WaitInvoice(_Cts.Token);
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
if (listenedInvoice == null)
continue;
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
notification.PaymentRequest == listenedInvoice.PaymentMethodDetails.BOLT11)
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
{
if (notification.Status == "paid" && notification.PaidAt.HasValue)
{
@ -156,24 +156,21 @@ namespace BTCPayServer.Payments.Lightning
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
DoneListening(supportedPaymentMethod.GetLightningUrl());
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
}
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
{
await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.PaymentRequest,
Amount = notification.MilliSatoshi
BOLT11 = notification.BOLT11,
Amount = notification.Amount
}, network.CryptoCode, accounted: true);
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
}
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
{
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
}
List<Task> _ListeningLightning = new List<Task>();
@ -202,9 +199,27 @@ namespace BTCPayServer.Payments.Lightning
return false;
}
/// <summary>
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(LightningConnectionString connectionString)
{
var uri = connectionString.BaseUri;
lock (_ListenedInvoiceByChargeInvoiceId)
{
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
{
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
}
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
}
}
bool Listening(string invoiceId)
{
lock(_ListenedInvoiceByLightningUrl)
lock (_ListenedInvoiceByLightningUrl)
{
return _InvoiceIds.Contains(invoiceId);
}
@ -230,13 +245,6 @@ namespace BTCPayServer.Payments.Lightning
{
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
_ListeningLightning.Add(listen);
listen.ContinueWith(_ =>
{
lock (_ListenedInvoiceByLightningUrl)
{
_ListeningLightning.Remove(listen);
}
}, TaskScheduler.Default);
}
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);

View File

@ -8,41 +8,36 @@ namespace BTCPayServer.Payments.Lightning
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
{
public string CryptoCode { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string LightningChargeUrl { get; set; }
public Uri GetLightningChargeUrl(bool withCredentials)
public LightningConnectionString GetLightningUrl()
{
#pragma warning disable CS0618 // Type or member is obsolete
UriBuilder uri = new UriBuilder(LightningChargeUrl);
if (withCredentials)
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
{
uri.UserName = Username;
uri.Password = Password;
throw new FormatException(error);
}
return connectionString;
#pragma warning restore CS0618 // Type or member is obsolete
return uri.Uri;
}
public void SetLightningChargeUrl(Uri uri)
public void SetLightningUrl(LightningConnectionString connectionString)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
if (string.IsNullOrEmpty(uri.UserInfo))
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
var splitted = uri.UserInfo.Split(':');
if (splitted.Length != 2)
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
#pragma warning disable CS0618 // Type or member is obsolete
Username = splitted[0];
Password = splitted[1];
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
Username = connectionString.Username;
Password = connectionString.Password;
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
#pragma warning restore CS0618 // Type or member is obsolete
}
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string Username { get; set; }
[Obsolete("Use Get/SetLightningChargeUrl")]
[Obsolete("Use Get/SetLightningUrl")]
public string Password { get; set; }
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
}

View File

@ -5,11 +5,12 @@
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_INTERNALLIGHTNINGNODE": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://localhost:14142/"

View File

@ -8,6 +8,7 @@ using BTCPayServer.Services.Wallets;
using LedgerWallet;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
namespace BTCPayServer.Services
{
@ -76,18 +77,19 @@ namespace BTCPayServer.Services
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
});
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
@ -152,7 +154,8 @@ namespace BTCPayServer.Services
(IDestination destination, Money amount, bool substractFees)[] send,
FeeRate feeRate,
IDestination changeAddress,
KeyPath changeKeyPath)
KeyPath changeKeyPath,
FeeRate minTxRelayFee)
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
@ -185,6 +188,7 @@ namespace BTCPayServer.Services
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
@ -229,5 +233,8 @@ namespace BTCPayServer.Services
public class GetXPubResult
{
public string ExtPubKey { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public KeyPath KeyPath { get; set; }
public int CoinType { get; internal set; }
}
}

View File

@ -582,7 +582,7 @@ namespace BTCPayServer.Services.Invoices
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
FeeRate = FeeRate,
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork),
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
TxFee = TxFee
};
}
@ -592,7 +592,7 @@ namespace BTCPayServer.Services.Invoices
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
{
btcLike.TxFee = TxFee;
btcLike.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
btcLike.FeeRate = FeeRate;
}
return details;
@ -615,7 +615,7 @@ namespace BTCPayServer.Services.Invoices
{
TxFee = bitcoinPaymentMethod.TxFee;
FeeRate = bitcoinPaymentMethod.FeeRate;
DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString();
DepositAddress = bitcoinPaymentMethod.DepositAddress;
}
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
PaymentMethodDetails = jobj;
@ -646,8 +646,9 @@ namespace BTCPayServer.Services.Invoices
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = 8;
var paidTxFee = 0m;
bool paidEnough = paid >= RoundUp(totalDue, 8);
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0;
var payments =
ParentEntity.GetPayments()
@ -655,15 +656,15 @@ namespace BTCPayServer.Services.Invoices
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetpaymentMethodId()].GetTxFee());
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee());
paid += _.GetValue(paymentMethods, GetId());
if (!paidEnough)
{
totalDue += txFee;
paidTxFee += txFee;
}
paidEnough |= paid >= RoundUp(totalDue, 8);
if (GetId() == _.GetpaymentMethodId())
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
if (GetId() == _.GetPaymentMethodId())
{
cryptoPaid += _.GetCryptoPaymentData().GetValue();
txRequired++;
@ -681,7 +682,7 @@ namespace BTCPayServer.Services.Invoices
paidTxFee += GetTxFee();
}
accounting.TotalDue = Money.Coins(RoundUp(totalDue, 8));
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
accounting.Paid = Money.Coins(paid);
accounting.TxRequired = txRequired;
accounting.CryptoPaid = Money.Coins(cryptoPaid);
@ -691,20 +692,6 @@ namespace BTCPayServer.Services.Invoices
return accounting;
}
private static decimal RoundUp(decimal value, int precision)
{
for (int i = 0; i < precision; i++)
{
value = value * 10m;
}
value = Math.Ceiling(value);
for (int i = 0; i < precision; i++)
{
value = value / 10m;
}
return value;
}
private decimal GetTxFee()
{
var method = GetPaymentMethodDetails();
@ -766,7 +753,7 @@ namespace BTCPayServer.Services.Invoices
paymentData.Legacy = true;
return paymentData;
}
if (GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
if (GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
{
var paymentData = JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikePaymentData>(CryptoPaymentData);
// legacy
@ -774,7 +761,7 @@ namespace BTCPayServer.Services.Invoices
paymentData.Outpoint = Outpoint;
return paymentData;
}
if(GetpaymentMethodId().PaymentType== PaymentTypes.LightningLike)
if(GetPaymentMethodId().PaymentType== PaymentTypes.LightningLike)
{
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
}
@ -802,7 +789,7 @@ namespace BTCPayServer.Services.Invoices
{
value = value ?? this.GetCryptoPaymentData().GetValue();
var to = paymentMethodId;
var from = this.GetpaymentMethodId();
var from = this.GetPaymentMethodId();
if (to == from)
return decimal.Round(value.Value, 8);
var fromRate = paymentMethods[from].Rate;
@ -813,7 +800,7 @@ namespace BTCPayServer.Services.Invoices
return otherCurrencyValue;
}
public PaymentMethodId GetpaymentMethodId()
public PaymentMethodId GetPaymentMethodId()
{
#pragma warning disable CS0618 // Type or member is obsolete
return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(CryptoPaymentDataType));

View File

@ -130,7 +130,7 @@ namespace BTCPayServer.Services.Invoices
throw new InvalidOperationException("CryptoCode unsupported");
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
string address = GetDestination(paymentMethod);
string address = GetDestination(paymentMethod, paymentMethod.Network.NBitcoinNetwork);
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoice.Id,
@ -162,12 +162,12 @@ namespace BTCPayServer.Services.Invoices
return invoice;
}
private static string GetDestination(PaymentMethod paymentMethod)
private static string GetDestination(PaymentMethod paymentMethod, Network network)
{
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
{
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).DepositAddress.ScriptPubKey.Hash.ToString();
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetDepositAddress(network).ScriptPubKey.Hash.ToString();
}
///////////////
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
@ -209,7 +209,7 @@ namespace BTCPayServer.Services.Invoices
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(currencyData), currencyData.GetId()));
.Set(GetDestination(currencyData, network.NBitcoinNetwork), currencyData.GetId()));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoiceId,
@ -461,6 +461,15 @@ namespace BTCPayServer.Services.Invoices
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
}
/// <summary>
/// Add a payment to an invoice
/// </summary>
/// <param name="invoiceId"></param>
/// <param name="date"></param>
/// <param name="paymentData"></param>
/// <param name="cryptoCode"></param>
/// <param name="accounted"></param>
/// <returns>The PaymentEntity or null if already added</returns>
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
{
using (var context = _ContextFactory.CreateContext())
@ -486,7 +495,11 @@ namespace BTCPayServer.Services.Invoices
context.Payments.Add(data);
await context.SaveChangesAsync().ConfigureAwait(false);
try
{
await context.SaveChangesAsync().ConfigureAwait(false);
}
catch(DbUpdateException) { return null; } // Already exists
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
return entity;
}

View File

@ -126,17 +126,18 @@ namespace BTCPayServer.Services.Wallets
}
catch
{
Logs.PayServer.LogError("Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers");
Logs.PayServer.LogError($"{Network.CryptoCode}: Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers");
throw;
}
var spentTime = DateTimeOffset.UtcNow - now;
if (spentTime.TotalSeconds > 30)
{
Logs.PayServer.LogWarning($"NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers");
Logs.PayServer.LogWarning($"{Network.CryptoCode}: NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers");
}
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return result;
});
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
completionSource.TrySetResult(utxos);
}
catch (Exception ex)

View File

@ -1,4 +1,5 @@
@model PaymentModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model PaymentModel
@{
Layout = null;
ViewData["Title"] = "Payment";
@ -12,18 +13,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>BTCPay Invoice</title>
<link href="~/vendor/font-awesome/css/font-awesome.css" rel="stylesheet" />
<link href="~/css/css.css" rel="stylesheet" type="text/css">
<link href="~/css/normalizer.css" rel="stylesheet" type="text/css">
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
<script type="text/javascript">
@Model.ToJSVariableModel("srvModel")
</script>
<script src="~/vendor/clipboard.js/clipboard.js"></script>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/js/vue.min.js" type="text/javascript"></script>
<script src="~/js/vue-qrcode.js" type="text/javascript"></script>
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
</head>
<body style="background: #E4E4E4">
<noscript>
@ -72,7 +68,7 @@
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</svg>
</bp-spinner>
</div>
<div class="timer-row__message">
@ -88,6 +84,33 @@
</div>
</div>
<div class="order-details">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
Pay with
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
<div class="payment__spinner">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</div>
</div>
}
<!---->
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
@ -147,22 +170,20 @@
<div class="payment-tabs__tab" id="copy-tab">
<span i18n="">Copy</span>
</div>
<div class="payment-tabs__slider"></div>
@if (Model.AllowCoinConversion)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span i18n="">Conversion</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider"></div>
}
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode :val="srvModel.invoiceBitcoinUrlQR" :size="256" bg-color="#f5f5f7" fg-color="#000" />
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span i18n="">Open in wallet</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
@ -187,7 +208,7 @@
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</svg>
</bp-spinner>
</div>
</button>
@ -195,6 +216,94 @@
</div>
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span i18n="">Open in wallet</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span i18n="">Copied</span>
</div>
</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="">Address</div>
<!---->
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img :src="srvModel.cryptoImage" height="16" />
</div>
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
</div>
<div class="copied-label" style="top: 5px;">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
@if (Model.AllowCoinConversion)
{
<div id="altcoins" class="bp-view payment manual-flow">
<div v-if="srvModel.paymentMethodId != 'BTC_LightningLike'">
<div class="manual__step-two__instructions">
<span>
You can pay {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} using altcoins other than the ones merchant directly supports.
<br /><br />
This service is provided by 3rd party. Please keep in mind that
<span style="font-weight: 900;">we have no control</span> over how providers will forward your funds.
Invoice will only be marked paid once funds are received on {{srvModel.cryptoCode}} Blockchain.
</span>
</div>
<center>
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
</a>
@*Changelly doesn't have TO_AMOUNT support so we can't include it
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
</a>*@
</center>
</div>
<div v-else>
<div class="manual__step-two__instructions">
<span>
No conversion providers available for BTC Lightning Network payments.
</span>
</div>
</div>
</div>
}
<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>
@ -330,46 +439,7 @@
</div>
</form>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span i18n="">Copied</span>
</div>
</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="">Address</div>
<!---->
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img :src="srvModel.cryptoImage" height="16" />
</div>
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
</div>
<div class="copied-label" style="top: 5px;">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
<div class="bp-view pad" id="paid">
<div class="status-block">
<div class="success-block">
@ -527,7 +597,6 @@
</div>
</div>
</div>
<!---->
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
@ -587,8 +656,6 @@
</bp-done-text>
</button>
</div>
<!---->
<!---->
<div class="footer-button enter-different-address-button">
<bp-done-text>
<!---->
@ -600,23 +667,27 @@
</div>
</div>
</div>
<div class="footer">
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
@if(Model.AvailableCryptos.Count > 1)
{
<div style="text-align:center">Accepted here</div>
<div style="text-align:center">
@foreach(var crypto in Model.AvailableCryptos)
{
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.paymentMethodId='@crypto.PaymentMethodId'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" /></a>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
</invoice>
<script type="text/javascript">
// TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [
'line-items',
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
el: '#checkoutCtrl',
components: {
qrcode: VueQr
},
data: {
srvModel: srvModel
}
});
</script>
</body>
</html>

View File

@ -10,6 +10,9 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.money {
text-align: right;
}
</style>
<section>
@ -67,7 +70,7 @@
</tr>
<tr>
<th>Refund email</th>
<td>@Model.RefundEmail</td>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
</tr>
<tr>
<th>Order Id</th>
@ -83,7 +86,7 @@
</tr>
<tr>
<th>Redirect Url</th>
<td>@Model.RedirectUrl</td>
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
</tr>
</table>
</div>
@ -98,7 +101,7 @@
</tr>
<tr>
<th>Email</th>
<td>@Model.BuyerInformation.BuyerEmail</td>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
@ -153,22 +156,22 @@
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Rate</th>
<th>Paid</th>
<th>Due</th>
<th style="white-space:nowrap;">Payment method</th>
<th>Address</th>
<th class="money">Rate</th>
<th class="money">Paid</th>
<th class="money">Due</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.CryptoCode</td>
<td>@payment.Rate</td>
<td>@payment.Paid</td>
<td>@payment.Due</td>
<td>@payment.PaymentMethod</td>
<td>@payment.Address</td>
<td class="money">@payment.Rate</td>
<td class="money">@payment.Paid</td>
<td class="money">@payment.Due</td>
</tr>
}
</tbody>
@ -181,24 +184,21 @@
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Date</th>
<th style="white-space:nowrap;">Payment method</th>
<th>Deposit address</th>
<th>Transaction Id</th>
<th>Confirmations</th>
<th>Replaced</th>
<th style="text-align:right;">Confirmations</th>
</tr>
</thead>
<tbody>
@foreach(var payment in Model.Payments)
{
var replaced = payment.Replaced ? "text-decoration: line-through;" : "";
<tr>
<td>@payment.CryptoCode</td>
<td>@payment.ReceivedTime</td>
<td>@payment.DepositAddress</td>
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td>@payment.Confirmations</td>
<td>@payment.Replaced</td>
<td style="@replaced">@payment.PaymentMethod</td>
<td style="@replaced">@payment.DepositAddress</td>
<td style="@replaced"><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td style="text-align:right;">@payment.Confirmations</td>
</tr>
}
</tbody>
@ -211,20 +211,19 @@
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th style="white-space:nowrap;">Payment method</th>
<th>Address</th>
<th>Current</th>
</tr>
</thead>
<tbody>
@foreach(var address in Model.Addresses)
{
{
var current = address.Current ? "font-weight: bold;" : "";
<tr>
<td>@address.GetCryptoCode()</td>
<td>@address.GetAddress()</td>
<td>@(!address.UnAssigned.HasValue)</td>
<td style="width:100px;@current">@address.PaymentMethod</td>
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
</tr>
}
}
</tbody>
</table>
</div>

View File

@ -47,8 +47,8 @@
<th>OrderId</th>
<th>InvoiceId</th>
<th>Status</th>
<th>Amount</th>
<th>Actions</th>
<th style="text-align:right">Amount</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@ -56,7 +56,16 @@
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.OrderId</td>
<td>
@if(invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl">@invoice.OrderId</a>
}
else
{
<span>@invoice.OrderId</span>
}
</td>
<td>@invoice.InvoiceId</td>
@if(invoice.Status == "paid")
{
@ -73,10 +82,15 @@
}
else
{
<td>@invoice.Status</td>
<td >@invoice.Status</td>
}
<td>@invoice.AmountCurrency</td>
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> - <a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a></td>
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.Status == "new")
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
</td>
</tr>
}
</tbody>

View File

@ -16,101 +16,102 @@
<div class="col-md-8">
<form method="post">
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
</p>
<p id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
</p>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="CryptoCurrency" />
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
</div>
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
}
</ul>
</div>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Address type</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCode)</h5>
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
</form>
</div>
</div>

View File

@ -16,14 +16,14 @@
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices. <br /></span>
<span>A connection to a lightning charge node or clightning unix socket is required to generate lignting network enabled invoices. <br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
</p>
<ul>
<li>You might lose your money</li>
<li>The devs of BTCPay Server don't know what they are doing and won't be able to help you if shit hit the fan</li>
<li>You approve being #reckless and being the sole responsible party for your loss</li>
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node</li>
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node or CLightning unix socket</li>
<li>If you have no idea what above mean, search by yourself</li>
<li>If you still have no idea how to use lightning, give up for now, we'll make it easier later</li>
</ul>
@ -33,13 +33,8 @@
<form method="post">
<div class="form-group">
<h5>Lightning node url</h5>
<span>This URL should point to an installed lightning charge server</span>
<span>This URL should point to an installed lightning charge server for @Model.CryptoCode</span>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="Url"></label>
<input id="lightningurl" asp-for="Url" class="form-control" />

View File

@ -28,7 +28,7 @@
<th>Name</th>
<th>Website</th>
<th>Balances</th>
<th>Actions</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@ -52,7 +52,7 @@
}
}
</td>
<td><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
<td style="text-align:right"><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
</tr>
}
</tbody>

View File

@ -14,10 +14,8 @@ namespace BTCPayServer.Views.Stores
public static string Tokens => "Tokens";
public static string Wallet => "Wallet";
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);

View File

@ -70,27 +70,38 @@
</select>
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AllowCoinConversion"></label>
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice on chain.</span>
</div>
<div class="form-group">
<a asp-action="AddDerivationScheme" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a derivation scheme</a>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.DerivationSchemes)
@foreach (var scheme in Model.DerivationSchemes)
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:400px;overflow:hidden;">@scheme.Value</td>
</tr>
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>
</tr>
}
</tbody>
</table>
@ -105,20 +116,21 @@
</p>
</div>
<div class="form-group">
<a asp-action="AddLightningNode" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a lightning node</a>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Crypto</th>
<th>Address</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var scheme in Model.LightningNodes)
@foreach (var scheme in Model.LightningNodes)
{
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}
</tbody>

View File

@ -2,7 +2,6 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.AddActivePage(StoreNavPages.Wallet);
}
<h4>@ViewData["Title"]</h4>
@ -18,7 +17,7 @@
If your Ledger wallet is not detected:
</p>
<ul>
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
<li>Make sure you are running the Ledger Bitcoin or Litecoin app with version superior or equal to 1.2.4</li>
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
</ul>
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
@ -32,10 +31,7 @@
</div>
<div class="col-md-6">
<form id="sendform" style="display:none;">
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
@ -58,7 +54,7 @@
</p>
</div>
<div class="form-group">
<label>Substract fees from amount</label>
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>

View File

@ -4,6 +4,5 @@
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
</ul>

View File

@ -27,5 +27,23 @@
"wwwroot/vendor/jquery-validate/jquery.validate.js",
"wwwroot/vendor/jquery-validate-unobtrusive/jquery.validate.unobtrusive.js"
]
},
{
"outputFileName": "wwwroot/bundles/checkout-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/css/css.css",
"wwwroot/css/normalizer.css"
]
},
{
"outputFileName": "wwwroot/bundles/checkout-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/js/vue.min.js",
"wwwroot/js/vue-qrcode.js",
"wwwroot/js/core.js"
]
}
]

View File

@ -8411,6 +8411,17 @@ strong {
float: right;
}
.currency-selection {
border-bottom: 1px solid #E9E9E9;
position: relative;
padding: 4px 15px;
display: flex;
font-weight: 300;
color: #565D6E;
letter-spacing: .45px;
background: #fff;
}
.single-item-order {
position: relative;
padding: 15px;
@ -8551,10 +8562,23 @@ strong {
transition: all .2s ease;
}
.payment-tabs__slider.slide-right {
.payment-tabs__slider.slide-copy {
right: 0;
}
.payment-tabs__slider.three-tabs {
width: 33%;
right: 67%;
}
.payment-tabs__slider.three-tabs.slide-copy {
right: 33%;
}
.payment-tabs__slider.three-tabs.slide-altcoins {
right: 0;
}
.manual__step-one__header {
padding-top: 20px;
text-align: center;
@ -10487,6 +10511,7 @@ All mobile class names should be prefixed by m- */
min-height: 575px;
}
.paid .currency-selection,
.expired .order-details,
.archived .order-details {
display: none;
@ -10881,6 +10906,19 @@ bp-spinner {
display: flex;
}
.payment__spinner {
display: none;
}
.payment__spinner > bp-spinner > svg {
margin: auto 0px 0px auto;
height: 32px;
width: 32px;
fill: gray;
animation: spin 0.55s linear infinite;
opacity: .85;
}
bp-refund-address.ng-valid .bitcoin-logo {
opacity: 1;
margin-left: 0;

View File

@ -1,6 +1,10 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g transform="translate(0.00630876,-0.00301984) scale(0.25,0.25)">
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1"
x="0px" y="0px"
viewBox="0 0 64 64"
xml:space="preserve"
xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
<g transform="translate(0.00630876,-0.00301984)">
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
</g>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,8 +1,12 @@
$(function () {
var ledgerDetected = false;
var recommendedPubKey = "";
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
var cryptoSelector = $("#CryptoCurrency");
function GetSelectedCryptoCode() {
return cryptoSelector.val();
}
function WriteAlert(type, message) {
}
@ -14,37 +18,41 @@
}
}
$("#ledger-info-recommended").on("click", function (elem) {
$(".ledger-info-recommended").on("click", function (elem) {
elem.preventDefault();
$("#DerivationScheme").val(recommendedPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
var account = elem.currentTarget.getAttribute("data-ledgeraccount");
var cryptoCode = GetSelectedCryptoCode();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&account=" + account)
.then(function (result) {
if (cryptoCode !== GetSelectedCryptoCode())
return;
$("#DerivationScheme").val(result.extPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
})
.catch(function (reason) { Write('check', 'error', reason); });
return false;
});
$("#CryptoCurrency").on("change", function (elem) {
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "none");
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
var cryptoCode = $("#CryptoCurrency").val();
var cryptoCode = GetSelectedCryptoCode();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (cryptoCode !== GetSelectedCryptoCode())
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
recommendedPubKey = result.extPubKey;
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "block");
$(".ledger-info-cointype").text(result.coinType);
}
});
};

View File

@ -3,6 +3,7 @@
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();
function WriteAlert(type, message) {
$(".alert").removeClass("alert-danger");
@ -37,7 +38,7 @@
}
var args = "";
args += "cryptoCode=" + $("#cryptoCurrencies").val();
args += "cryptoCode=" + cryptoCode;
args += "&destination=" + $("#destination-textbox").val();
args += "&amount=" + $("#amount-textbox").val();
args += "&feeRate=" + $("#fee-textbox").val();
@ -64,6 +65,10 @@
WriteAlert("danger", result.error);
} else {
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
$("#fee-textbox").val("");
$("#amount-textbox").val("");
$("#destination-textbox").val("");
$("#substract-checkbox").prop("checked", false);
updateInfo();
}
});
@ -85,15 +90,10 @@
return false;
});
$("#cryptoCurrencies").on("change", function (elem) {
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
$(".crypto-info").css("display", "none");
var cryptoCode = $("#cryptoCurrencies").val();
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
@ -125,7 +125,7 @@
.catch(function (reason)
{
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
reason = "Are you running the ledger app with version equals or above 1.2.4?";
Write('hw', 'error', reason);
})
.then(function (result) {

View File

@ -1,156 +1,22 @@
function resetTabsSlider() {
$("#tabsSlider").removeClass("slide-copy");
$("#tabsSlider").removeClass("slide-altcoins");
/* TAF
$("#scan-tab").removeClass("active");
$("#copy-tab").removeClass("active");
$("#altcoins-tab").removeClass("active");
- Version mobile
$("#copy").hide();
$("#copy").removeClass("active");
- Réparer le décallage par timer
$("#scan").hide();
$("#scan").removeClass("active");
- Preparer les variables de l'API
- Gestion des differents evenements en fonction du status de l'invoice
- sécuriser les CDN
*/
// TODO: Vue controller... complete migrate to it for binding, animations can stay in jQuery
Vue.config.ignoredElements = [
'line-items',
'low-fee-timeline',
// Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/
];
var checkoutCtrl = new Vue({
el: '#checkoutCtrl',
components: {
qrcode: VueQr
},
data: {
srvModel: srvModel
}
});
var display = $(".timer-row__time-left"); // Timer container
// check if the Document expired
if (srvModel.expirationSeconds > 0) {
progressStart(srvModel.maxTimeSeconds); // Progress bar
startTimer(srvModel.expirationSeconds, display); // Timer
if (!validateEmail(srvModel.customerEmail))
emailForm(); // Email form Display
else
hideEmailForm();
$("#altcoins").hide();
$("#altcoins").removeClass("active");
}
function hideEmailForm() {
$("[role=document]").removeClass("enter-purchaser-email");
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);
// Remove Email mode
$(".modal-dialog").removeClass("enter-purchaser-email");
$("#scan").addClass("active");
}
// Email Form
// Setup Email mode
function emailForm() {
$(".modal-dialog").addClass("enter-purchaser-email");
$("#emailAddressForm .action-button").click(function () {
var emailAddress = $("#emailAddressFormInput").val();
if (validateEmail(emailAddress)) {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
// Push the email to a server, once the reception is confirmed move on
srvModel.customerEmail = emailAddress;
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
$.ajax({
url: path,
type: "POST",
data: JSON.stringify({ Email: srvModel.customerEmail }),
contentType: "application/json; charset=utf-8"
}).done(function () {
hideEmailForm();
})
.fail(function (jqXHR, textStatus, errorThrown) {
})
.always(function () {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
});
} else {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
});
}
/* =============== Even listeners =============== */
// Email
$("#emailAddressFormInput").change(function () {
if ($("#emailAddressForm").hasClass("ng-submitted")) {
$("#emailAddressForm").removeClass("ng-submitted");
}
});
// Scan/Copy Transitions
// Scan Tab
$("#scan-tab").click(function () {
if (!$(this).is(".active")) {
$(this).addClass("active");
}
if ($("#copy-tab").is(".active")) {
$("#copy-tab").removeClass("active");
}
$(".payment-tabs__slider").removeClass("slide-right");
if (!$("#scan").is(".active")) {
$("#copy").hide();
$("#copy").removeClass("active");
$("#scan").show();
$("#scan").addClass("active");
}
});
// Main Copy tab
$("#copy-tab").click(function () {
if (!$(this).is(".active")) {
$(this).addClass("active");
}
if ($("#scan-tab").is(".active")) {
$("#scan-tab").removeClass("active");
}
if (!$(".payment-tabs__slider").is("slide-right")) {
$(".payment-tabs__slider").addClass("slide-right");
}
if (!$("#copy").is(".active")) {
$("#copy").show();
$("#copy").addClass("active");
$("#scan").hide();
$("#scan").removeClass("active");
}
});
// Payment received
// Should connect using webhook ?
// If notification received
onDataCallback(srvModel);
// public methods
function onDataCallback(jsonData) {
var newStatus = jsonData.status;
@ -172,11 +38,7 @@ function onDataCallback(jsonData) {
$(".modal-dialog").addClass("paid");
if ($("#scan").hasClass("active")) {
$("#scan").removeClass("active");
} else if ($("#copy").hasClass("active")) {
$("#copy").removeClass("active");
}
resetTabsSlider();
$("#paid").addClass("active");
}
@ -187,21 +49,41 @@ function onDataCallback(jsonData) {
$("#emailAddressView").removeClass("active");
$(".modal-dialog").addClass("expired");
$("#expired").addClass("active");
resetTabsSlider();
}
if (checkoutCtrl.srvModel.status !== newStatus) {
window.parent.postMessage({ "invoiceId": srvModel.invoiceId, "status": newStatus }, "*");
}
// restoring qr code view only when currency is switched
if (jsonData.paymentMethodId == srvModel.paymentMethodId) {
$(".payment__currencies").show();
$(".payment__spinner").hide();
}
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
// updating ui
checkoutCtrl.srvModel = jsonData;
}
function changeCurrency(currency) {
if (srvModel.paymentMethodId != currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
srvModel.paymentMethodId = currency;
fetchStatus();
}
return false;
}
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.paymentMethodId + "/status";
$.ajax({
url: path,
type: "GET"
type: "GET",
cache: false
}).done(function (data) {
onDataCallback(data);
}).fail(function (jqXHR, textStatus, errorThrown) {
@ -209,123 +91,244 @@ function fetchStatus() {
});
}
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
path = path.replace("https://", "wss://");
path = path.replace("http://", "ws://");
try {
var socket = new WebSocket(path);
socket.onmessage = function (e) {
fetchStatus();
};
}
catch (e) {
console.error("Error while connecting to websocket for invoice notifications");
}
}
// private methods
$(document).ready(function () {
// initialize
onDataCallback(srvModel);
var watcher = setInterval(function () {
fetchStatus();
}, 2000);
/* TAF
- Version mobile
- Réparer le décallage par timer
- Preparer les variables de l'API
- Gestion des differents evenements en fonction du status de l'invoice
- sécuriser les CDN
*/
var display = $(".timer-row__time-left"); // Timer container
// check if the Document expired
if (srvModel.expirationSeconds > 0) {
progressStart(srvModel.maxTimeSeconds); // Progress bar
startTimer(srvModel.expirationSeconds, display); // Timer
if (!validateEmail(srvModel.customerEmail))
emailForm(); // Email form Display
else
hideEmailForm();
}
function hideEmailForm() {
$("[role=document]").removeClass("enter-purchaser-email");
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);
// Remove Email mode
$(".modal-dialog").removeClass("enter-purchaser-email");
$("#scan").addClass("active");
}
// Email Form
// Setup Email mode
function emailForm() {
$(".modal-dialog").addClass("enter-purchaser-email");
$("#emailAddressForm .action-button").click(function () {
var emailAddress = $("#emailAddressFormInput").val();
if (validateEmail(emailAddress)) {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
// Push the email to a server, once the reception is confirmed move on
srvModel.customerEmail = emailAddress;
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
$.ajax({
url: path,
type: "POST",
data: JSON.stringify({ Email: srvModel.customerEmail }),
contentType: "application/json; charset=utf-8"
}).done(function () {
hideEmailForm();
}).fail(function (jqXHR, textStatus, errorThrown) {
})
.always(function () {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
});
} else {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
});
}
// Validate Email address
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
/* =============== Even listeners =============== */
// Email
$("#emailAddressFormInput").change(function () {
if ($("#emailAddressForm").hasClass("ng-submitted")) {
$("#emailAddressForm").removeClass("ng-submitted");
}
});
// Scan/Copy Transitions
// Scan Tab
$("#scan-tab").click(function () {
resetTabsSlider();
activateTab("#scan");
});
// Copy tab
$("#copy-tab").click(function () {
resetTabsSlider();
activateTab("#copy");
$("#tabsSlider").addClass("slide-copy");
});
// Altcoins tab
$("#altcoins-tab").click(function () {
resetTabsSlider();
activateTab("#altcoins");
$("#tabsSlider").addClass("slide-altcoins");
});
function activateTab(senderName) {
$(senderName + "-tab").addClass("active");
$(senderName).show();
$(senderName).addClass("active");
}
// Payment received
// Should connect using webhook ?
// If notification received
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
path = path.replace("https://", "wss://");
path = path.replace("http://", "ws://");
try {
var socket = new WebSocket(path);
socket.onmessage = function (e) {
fetchStatus();
};
}
catch (e) {
console.error("Error while connecting to websocket for invoice notifications");
}
}
var watcher = setInterval(function () {
fetchStatus();
}, 2000);
$(".menu__item").click(function () {
$(".menu__scroll .menu__item").removeClass("selected");
$(this).addClass("selected");
language();
$(".selector span").text($(".selected").text());
// function to load contents in different language should go there
});
// Expand Line-Items
$(".buyerTotalLine").click(function () {
$("line-items").toggleClass("expanded");
$(".buyerTotalLine").toggleClass("expanded");
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
});
// Timer Countdown
function startTimer(duration, display) {
var timer = duration, minutes, seconds;
var timeout = setInterval(function () {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timeout);
}
}, 1000);
}
// Progress bar
function progressStart(timerMax) {
var end = new Date(); // Setup Time Variable, should come from server
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
animateUpdate(); //Launch it
function updateProgress(percentage) {
$('.timer-row__progress-bar').css("width", percentage + "%");
}
function animateUpdate() {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100);
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");
$(".timer-row__message span").html("Invoice expiring soon ...");
updateProgress(perc);
}
if (perc <= 100) {
updateProgress(perc);
setTimeout(animateUpdate, timeoutVal);
}
if (perc >= 100 && status === "expired") {
onDataCallback(status);
}
}
}
// Manual Copy
// Amount
var copyAmount = new Clipboard('.manual-box__amount__value', {
target: function () {
var $el = $(".manual-box__amount__value");
$el.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $el.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__amount__value span');
}
});
// Address
var copyAddress = new Clipboard('.manual-box__address__value', {
target: function () {
var $elm = $(".manual-box__address__value");
$elm.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $elm.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__address__value .manual-box__address__wrapper .manual-box__address__wrapper__value');
}
});
// Disable enter key
$(document).keypress(
function (event) {
if (event.which === '13') {
event.preventDefault();
}
}
);
$(".menu__item").click(function () {
$(".menu__scroll .menu__item").removeClass("selected");
$(this).addClass("selected");
language();
$(".selector span").text($(".selected").text());
// function to load contents in different language should go there
});
// Validate Email address
function validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
// Expand Line-Items
$(".buyerTotalLine").click(function () {
$("line-items").toggleClass("expanded");
$(".buyerTotalLine").toggleClass("expanded");
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
});
// Timer Countdown
function startTimer(duration, display) {
var timer = duration, minutes, seconds;
var timeout = setInterval(function () {
minutes = parseInt(timer / 60, 10);
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.text(minutes + ":" + seconds);
if (--timer < 0) {
clearInterval(timeout);
}
}, 1000);
}
// Progress bar
function progressStart(timerMax) {
var end = new Date(); // Setup Time Variable, should come from server
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
animateUpdate(); //Launch it
function updateProgress(percentage) {
$('.timer-row__progress-bar').css("width", percentage + "%");
}
function animateUpdate() {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100);
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");
$(".timer-row__message span").html("Invoice expiring soon ...");
updateProgress(perc);
}
if (perc <= 100) {
updateProgress(perc);
setTimeout(animateUpdate, timeoutVal);
}
if (perc >= 100 && status === "expired") {
onDataCallback(status);
}
}
}
// Manual Copy
// Amount
var copyAmount = new Clipboard('.manual-box__amount__value', {
target: function () {
var $el = $(".manual-box__amount__value");
$el.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $el.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__amount__value span');
}
});
// Address
var copyAddress = new Clipboard('.manual-box__address__value', {
target: function () {
var $elm = $(".manual-box__address__value");
$elm.removeClass("copy-cursor").addClass("copied");
setTimeout(function () { $elm.removeClass("copied").addClass("copy-cursor"); }, 500);
return document.querySelector('.manual-box__address__value .manual-box__address__wrapper .manual-box__address__wrapper__value');
}
});
// Disable enter key
$(document).keypress(
function (event) {
if (event.which === '13') {
event.preventDefault();
}
}
);

View File

@ -1,4 +1,4 @@
FROM microsoft/aspnetcore-build:2.0.5-2.1.4-stretch AS builder
FROM microsoft/aspnetcore-build:2.0.6-2.1.101-stretch AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
# Cache some dependencies
@ -6,7 +6,7 @@ RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/aspnetcore:2.0.5-stretch
FROM microsoft/aspnetcore:2.0.6-stretch
WORKDIR /app
RUN mkdir /datadir

View File

@ -23,3 +23,39 @@ This solution is for you if:
## Documentation
Please check out our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) for more details.
You can also checkout [The Merchants Guide to accepting Bitcoin directly with no intermediates through BTCPay](https://www.reddit.com/r/Bitcoin/comments/81h1oy/the_merchants_guide_to_accepting_bitcoin_directly/).
## How to build
While the documentation advise using docker-compose, you may want to build yourself outside of development purpose.
First install .NET Core SDK as specified by [Microsoft website](https://www.microsoft.com/net/download).
On Powershell:
```
.\build.ps1
```
On linux:
```
./build.sh
```
## How to run
Use the `run` scripts to run BTCPayServer, this example show how to print the available command line arguments of BTCPayServer.
On Powershell:
```
.\run.ps1 --help
```
On linux:
```
./run.sh --help
```
## Other dependencies
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Deployment.md).

1
build.ps1 Executable file
View File

@ -0,0 +1 @@
dotnet build -c Release .\BTCPayServer\BTCPayServer.csproj

3
build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
dotnet build -c Release BTCPayServer/BTCPayServer.csproj

1
run.ps1 Executable file
View File

@ -0,0 +1 @@
dotnet run --no-launch-profile --no-build -c Release -p .\BTCPayServer\BTCPayServer.csproj -- $args

3
run.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
dotnet run --no-launch-profile --no-build -c Release -p "BTCPayServer/BTCPayServer.csproj" -- "$@"