Compare commits

...

20 Commits

Author SHA1 Message Date
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
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
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
43 changed files with 498 additions and 567 deletions

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,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,24 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Payments.Lightning.CLightning;
using BTCPayServer.Payments.Lightning.CLightning.RPC;
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,8 @@ 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.RPC;
namespace BTCPayServer.Tests
{
@ -56,8 +56,8 @@ namespace BTCPayServer.Tests
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
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", "http://127.0.0.1:30992/")), 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"))
{
@ -73,55 +73,63 @@ namespace BTCPayServer.Tests
/// <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 channel = (await CustomerLightningD.ListPeersAsync())
.SelectMany(p => p.Channels)
.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<Payments.Lightning.CLightning.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 +143,10 @@ 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 ChargeTester MerchantCharge { get; private set; }
internal string GetEnvironment(string variable, string defaultValue)

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>();
@ -347,7 +346,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,8 +17,8 @@ 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_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
expose:
- "80"
links:
@ -36,11 +36,12 @@ services:
links:
- nbxplorer
- postgres
- eclair
- customer_lightningd
- merchant_lightningd
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.19
image: nicolasdorier/nbxplorer:1.0.1.22
ports:
- "32838:32838"
expose:
@ -64,7 +65,6 @@ services:
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
image: nicolasdorier/docker-bitcoin:0.16.0
environment:
BITCOIN_EXTRA_ARGS: |
@ -75,11 +75,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 +83,27 @@ services:
volumes:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning
environment:
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
network=regtest
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 +111,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,32 +120,28 @@ 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:
LIGHTNINGD_OPT: |
bitcoin-datadir=/etc/bitcoin
bitcoin-rpcconnect=bitcoind
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
environment:
BITCOIN_EXTRA_ARGS: |
@ -155,3 +167,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

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.45</Version>
<Version>1.0.1.50</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.60" />
<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.14" />
<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

@ -77,7 +77,7 @@ namespace BTCPayServer.Controllers
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if(onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
cryptoPayment.Address = onchainMethod.DepositAddress;
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
@ -368,6 +368,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}"
});
}

View File

@ -65,7 +65,6 @@ namespace BTCPayServer.Controllers
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
vm.DerivationScheme = strategy.ToString();
}
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
}
catch
{
@ -75,7 +74,7 @@ namespace BTCPayServer.Controllers
}
if (vm.Confirmation)
if (vm.Confirmation || strategy == null)
{
try
{

View File

@ -54,10 +54,11 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (uri.Scheme != "https")
var domain = GetDomain(uri.AbsoluteUri);
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
{
var internalNode = GetInternalLightningNodeIfAuthorized();
if (internalNode == null || GetDomain(internalNode) != GetDomain(uri.AbsoluteUri))
if (internalNode == null || GetDomain(internalNode) != domain)
{
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
return View(vm);

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

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

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

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

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

@ -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
{
@ -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,118 @@
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.Tasks;
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
{
public class CLightningRPCClient
{
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));
Address = address;
Network = network;
}
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>("getinfo");
}
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)
{
parameters = parameters ?? Array.Empty<string>();
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 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(address, Address.Port));
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);
await jsonWriter.FlushAsync();
}
await textWriter.FlushAsync();
}
await networkStream.FlushAsync();
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
{
using (var jsonReader = new JsonTextReader(textReader))
{
var result = await JObject.LoadAsync(jsonReader);
var error = result.Property("error");
if(error != null)
{
throw new Exception(error.Value.ToString());
}
if (noReturn)
return default(T);
if (isArray)
{
return result["result"].Children().First().Children().First().ToObject<T>();
}
return result["result"].ToObject<T>();
}
}
}
}
public async Task<BitcoinAddress> NewAddressAsync()
{
var obj = await SendCommandAsync<JObject>("newaddr");
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
}
}
}

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.RPC
{
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.RPC
{
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

@ -62,7 +62,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))
@ -92,7 +91,7 @@ namespace BTCPayServer.Payments.Lightning
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;
@ -157,18 +156,20 @@ namespace BTCPayServer.Payments.Lightning
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
}
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
}
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice 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
}, network.CryptoCode, accounted: true);
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
if(payment != null)
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
}
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
@ -202,9 +203,26 @@ namespace BTCPayServer.Payments.Lightning
return false;
}
/// <summary>
/// Stop listening all invoices on this server
/// </summary>
/// <param name="uri"></param>
private void DoneListening(Uri uri)
{
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 +248,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

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

@ -21,7 +21,7 @@ namespace BTCPayServer.Payments.Lightning
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
{
var invoice = paymentMethod.ParentEntity;
var due = invoice.ProductInformation.Price / paymentMethod.Rate;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var client = GetClient(supportedPaymentMethod, network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()

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()
@ -662,7 +663,7 @@ namespace BTCPayServer.Services.Invoices
totalDue += txFee;
paidTxFee += txFee;
}
paidEnough |= paid >= RoundUp(totalDue, 8);
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
if (GetId() == _.GetPaymentMethodId())
{
cryptoPaid += _.GetCryptoPaymentData().GetValue();
@ -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();

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

@ -93,7 +93,7 @@
</div>
</div>
<div class="single-item-order__right">
<div>
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
@ -101,6 +101,13 @@
</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>
}
@ -180,13 +187,6 @@
</a>
</div>
</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 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">

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>
@ -154,10 +157,10 @@
<thead class="thead-inverse">
<tr>
<th style="white-space:nowrap;">Payment method</th>
<th>Rate</th>
<th>Paid</th>
<th>Due</th>
<th>Address</th>
<th class="money">Rate</th>
<th class="money">Paid</th>
<th class="money">Due</th>
</tr>
</thead>
<tbody>
@ -165,10 +168,10 @@
{
<tr>
<td>@payment.PaymentMethod</td>
<td>@payment.Rate</td>
<td>@payment.Paid</td>
<td>@payment.Due</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>
@ -184,19 +187,18 @@
<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.PaymentMethod</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,16 +213,15 @@
<tr>
<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 style="width:100px;">@address.PaymentMethod</td>
<td style="max-width:100px;overflow:hidden;">@address.Destination</td>
<td>@address.Current.ToString()</td>
<td style="width:100px;@current">@address.PaymentMethod</td>
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
</tr>
}
</tbody>

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

@ -10894,14 +10894,13 @@ bp-spinner {
}
.payment__spinner {
margin-top: 140px;
display: none;
}
.payment__spinner > bp-spinner > svg {
margin: auto auto;
height: 60px;
width: 60px;
margin: auto 0px 0px auto;
height: 32px;
width: 32px;
fill: gray;
animation: spin 0.55s linear infinite;
opacity: .85;

View File

@ -49,7 +49,7 @@ function onDataCallback(jsonData) {
// restoring qr code view only when currency is switched
if (jsonData.paymentMethodId == srvModel.paymentMethodId) {
$("#scan").show();
$(".payment__currencies").show();
$(".payment__spinner").hide();
}
@ -59,7 +59,7 @@ function onDataCallback(jsonData) {
function changeCurrency(currency) {
if (srvModel.paymentMethodId != currency) {
$("#scan").hide();
$(".payment__currencies").hide();
$(".payment__spinner").show();
srvModel.paymentMethodId = currency;
fetchStatus();