diff --git a/BTCPayServer.Tests/EclairTester.cs b/BTCPayServer.Tests/EclairTester.cs deleted file mode 100644 index 3c886f764..000000000 --- a/BTCPayServer.Tests/EclairTester.cs +++ /dev/null @@ -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 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(); - } - - } -} diff --git a/BTCPayServer.Tests/LightningDTester.cs b/BTCPayServer.Tests/LightningDTester.cs new file mode 100644 index 000000000..d014310c1 --- /dev/null +++ b/BTCPayServer.Tests/LightningDTester.cs @@ -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; } + + } +} diff --git a/BTCPayServer.Tests/README.md b/BTCPayServer.Tests/README.md index 5b1d7689b..a9bb1a930 100644 --- a/BTCPayServer.Tests/README.md +++ b/BTCPayServer.Tests/README.md @@ -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 diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 7b7255ff5..e001cddab 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -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 /// - /// This will setup a channel going from customer to merchant + /// Connect a customer LN node to the merchant LN node /// public void PrepareLightning() { PrepareLightningAsync().GetAwaiter().GetResult(); } + + /// + /// Connect a customer LN node to the merchant LN node + /// + /// 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 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) diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 9ba40becd..39d0edbdf 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -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(); diff --git a/BTCPayServer.Tests/docker-bitcoin-cli.ps1 b/BTCPayServer.Tests/docker-bitcoin-cli.ps1 index 6516d1011..163f64a83 100644 --- a/BTCPayServer.Tests/docker-bitcoin-cli.ps1 +++ b/BTCPayServer.Tests/docker-bitcoin-cli.ps1 @@ -1 +1 @@ -docker exec -ti btcpayserver_dev_bitcoind bitcoin-cli -regtest -conf="/data/bitcoin.conf" -datadir="/data" $args \ No newline at end of file +docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args diff --git a/BTCPayServer.Tests/docker-bitcoin-cli.sh b/BTCPayServer.Tests/docker-bitcoin-cli.sh new file mode 100755 index 000000000..9d7a9215d --- /dev/null +++ b/BTCPayServer.Tests/docker-bitcoin-cli.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@" diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index f96b52438..810bf3c86 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -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,7 +36,8 @@ services: links: - nbxplorer - postgres - - eclair + - customer_lightningd + - merchant_lightningd - lightning-charged nbxplorer: @@ -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: diff --git a/BTCPayServer.Tests/docker-customer-lightning-cli.ps1 b/BTCPayServer.Tests/docker-customer-lightning-cli.ps1 new file mode 100644 index 000000000..64209bfe0 --- /dev/null +++ b/BTCPayServer.Tests/docker-customer-lightning-cli.ps1 @@ -0,0 +1 @@ +docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args diff --git a/BTCPayServer.Tests/docker-customer-lightning-cli.sh b/BTCPayServer.Tests/docker-customer-lightning-cli.sh new file mode 100755 index 000000000..d2893ea77 --- /dev/null +++ b/BTCPayServer.Tests/docker-customer-lightning-cli.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@" diff --git a/BTCPayServer.Tests/docker-litecoin-cli.ps1 b/BTCPayServer.Tests/docker-litecoin-cli.ps1 index 8e3038a95..f79d6b58a 100644 --- a/BTCPayServer.Tests/docker-litecoin-cli.ps1 +++ b/BTCPayServer.Tests/docker-litecoin-cli.ps1 @@ -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 diff --git a/BTCPayServer.Tests/docker-litecoin-cli.sh b/BTCPayServer.Tests/docker-litecoin-cli.sh new file mode 100755 index 000000000..584bbf22e --- /dev/null +++ b/BTCPayServer.Tests/docker-litecoin-cli.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@" diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 00d5d7664..32df3a015 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.0 - 1.0.1.46 + 1.0.1.50 NU1701,CA1816,CA1308,CA1810,CA2208 diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index 8d65f4eef..b57d04858 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -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; diff --git a/BTCPayServer/Controllers/StoresController.BTCLike.cs b/BTCPayServer/Controllers/StoresController.BTCLike.cs index bb02856ea..8ad036521 100644 --- a/BTCPayServer/Controllers/StoresController.BTCLike.cs +++ b/BTCPayServer/Controllers/StoresController.BTCLike.cs @@ -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 { diff --git a/BTCPayServer/Data/AddressInvoiceData.cs b/BTCPayServer/Data/AddressInvoiceData.cs index b37ef24e0..65e482367 100644 --- a/BTCPayServer/Data/AddressInvoiceData.cs +++ b/BTCPayServer/Data/AddressInvoiceData.cs @@ -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() diff --git a/BTCPayServer/Data/StoreData.cs b/BTCPayServer/Data/StoreData.cs index e04ade65d..aec91dabf 100644 --- a/BTCPayServer/Data/StoreData.cs +++ b/BTCPayServer/Data/StoreData.cs @@ -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 diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs index 5f24c943d..a93393c42 100644 --- a/BTCPayServer/Extensions.cs +++ b/BTCPayServer/Extensions.cs @@ -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(info.PaymentType)); diff --git a/BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs b/BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs index 805a18c50..a0f023fb9 100644 --- a/BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs +++ b/BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs @@ -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; } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs index 7f83e7ac2..d283a5458 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikeOnChainPaymentMethod.cs @@ -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); + } /////////////////////////////////////////////////////////////////////////////////////// } } diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs index 47e9a2410..64c68532a 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentHandler.cs @@ -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; } diff --git a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs index 3f65f730a..d3f2856d5 100644 --- a/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs +++ b/BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs @@ -160,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 { @@ -330,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++; } } @@ -351,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); diff --git a/BTCPayServer/Payments/Lightning/CLightning/RPC/CLightningRPCClient.cs b/BTCPayServer/Payments/Lightning/CLightning/RPC/CLightningRPCClient.cs new file mode 100644 index 000000000..361539820 --- /dev/null +++ b/BTCPayServer/Payments/Lightning/CLightning/RPC/CLightningRPCClient.cs @@ -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 GetInfoAsync() + { + return SendCommandAsync("getinfo"); + } + + public Task SendAsync(string bolt11) + { + return SendCommandAsync("pay", new[] { bolt11 }, true); + } + + public async Task ListPeersAsync() + { + var peers = await SendCommandAsync("listpeers", isArray: true); + foreach(var peer in peers) + { + peer.Channels = peer.Channels ?? Array.Empty(); + } + return peers; + } + + public Task FundChannelAsync(NodeInfo nodeInfo, Money money) + { + return SendCommandAsync("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true); + } + + public Task ConnectAsync(NodeInfo nodeInfo) + { + return SendCommandAsync("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true); + } + + static Encoding UTF8 = new UTF8Encoding(false); + private async Task SendCommandAsync(string command, object[] parameters = null, bool noReturn = false, bool isArray = false) + { + parameters = parameters ?? Array.Empty(); + 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(); + } + return result["result"].ToObject(); + } + } + } + } + + public async Task NewAddressAsync() + { + var obj = await SendCommandAsync("newaddr"); + return BitcoinAddress.Create(obj.Property("address").Value.Value(), Network); + } + } +} diff --git a/BTCPayServer/Payments/Lightning/Eclair/NodeInfo.cs b/BTCPayServer/Payments/Lightning/CLightning/RPC/NodeInfo.cs similarity index 91% rename from BTCPayServer/Payments/Lightning/Eclair/NodeInfo.cs rename to BTCPayServer/Payments/Lightning/CLightning/RPC/NodeInfo.cs index e403d02b6..4f72eae99 100644 --- a/BTCPayServer/Payments/Lightning/Eclair/NodeInfo.cs +++ b/BTCPayServer/Payments/Lightning/CLightning/RPC/NodeInfo.cs @@ -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 { diff --git a/BTCPayServer/Payments/Lightning/CLightning/RPC/PeerInfo.cs b/BTCPayServer/Payments/Lightning/CLightning/RPC/PeerInfo.cs new file mode 100644 index 000000000..2c9789469 --- /dev/null +++ b/BTCPayServer/Payments/Lightning/CLightning/RPC/PeerInfo.cs @@ -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; } + + } +} diff --git a/BTCPayServer/Payments/Lightning/ChargeListener.cs b/BTCPayServer/Payments/Lightning/ChargeListener.cs index a3e1fe964..e418a4633 100644 --- a/BTCPayServer/Payments/Lightning/ChargeListener.cs +++ b/BTCPayServer/Payments/Lightning/ChargeListener.cs @@ -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; } + /// + /// Stop listening all invoices on this server + /// + /// + 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); diff --git a/BTCPayServer/Payments/Lightning/Eclair/AllChannelResponse.cs b/BTCPayServer/Payments/Lightning/Eclair/AllChannelResponse.cs deleted file mode 100644 index 67c33d4a9..000000000 --- a/BTCPayServer/Payments/Lightning/Eclair/AllChannelResponse.cs +++ /dev/null @@ -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; } - } -} diff --git a/BTCPayServer/Payments/Lightning/Eclair/ChannelResponse.cs b/BTCPayServer/Payments/Lightning/Eclair/ChannelResponse.cs deleted file mode 100644 index 6e2424177..000000000 --- a/BTCPayServer/Payments/Lightning/Eclair/ChannelResponse.cs +++ /dev/null @@ -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"; - } -} diff --git a/BTCPayServer/Payments/Lightning/Eclair/EclairRPCClient.cs b/BTCPayServer/Payments/Lightning/Eclair/EclairRPCClient.cs deleted file mode 100644 index 771788b12..000000000 --- a/BTCPayServer/Payments/Lightning/Eclair/EclairRPCClient.cs +++ /dev/null @@ -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 GetInfoAsync() - { - return SendCommandAsync(new RPCRequest("getinfo", Array.Empty())); - } - - public async Task SendCommandAsync(RPCRequest request, bool throwIfRPCError = true) - { - var response = await SendCommandAsync(request, throwIfRPCError); - return Serializer.ToObject(response.ResultString, Network); - } - - public async Task 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 AllChannelsAsync() - { - return await SendCommandAsync(new RPCRequest("allchannels", Array.Empty())).ConfigureAwait(false); - } - - public ChannelInfo[] Channels() - { - return ChannelsAsync().GetAwaiter().GetResult(); - } - - public async Task ChannelsAsync() - { - return await SendCommandAsync(new RPCRequest("channels", Array.Empty())).ConfigureAwait(false); - } - - public void Close(string channelId) - { - CloseAsync(channelId).GetAwaiter().GetResult(); - } - - public async Task SendAsync(string paymentRequest) - { - await SendCommandAsync(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 ChannelAsync(string channelId) - { - if (channelId == null) - throw new ArgumentNullException(nameof(channelId)); - return await SendCommandAsync(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false); - } - - public string[] AllNodes() - { - return AllNodesAsync().GetAwaiter().GetResult(); - } - - public async Task AllNodesAsync() - { - return await SendCommandAsync(new RPCRequest("allnodes", Array.Empty())).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 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 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 ReceiveAsync(LightMoney amount, string description = null) - { - if (amount == null) - throw new ArgumentNullException(nameof(amount)); - List args = new List(); - args.Add(amount.MilliSatoshi); - if(description != null) - { - args.Add(description); - } - return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString; - } - - public async Task 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; - } - - - } -} diff --git a/BTCPayServer/Payments/Lightning/Eclair/GetInfoResponse.cs b/BTCPayServer/Payments/Lightning/Eclair/GetInfoResponse.cs deleted file mode 100644 index d43b07bbe..000000000 --- a/BTCPayServer/Payments/Lightning/Eclair/GetInfoResponse.cs +++ /dev/null @@ -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; } - } -} diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs index 61bc332a4..e8b852670 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs @@ -21,7 +21,7 @@ namespace BTCPayServer.Payments.Lightning public override async Task 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() diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index d164c44cd..0730c6c18 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -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(); diff --git a/BTCPayServer/Services/Invoices/InvoiceRepository.cs b/BTCPayServer/Services/Invoices/InvoiceRepository.cs index 5fdc5d17a..4dc28f7c6 100644 --- a/BTCPayServer/Services/Invoices/InvoiceRepository.cs +++ b/BTCPayServer/Services/Invoices/InvoiceRepository.cs @@ -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()); } + /// + /// Add a payment to an invoice + /// + /// + /// + /// + /// + /// + /// The PaymentEntity or null if already added public async Task 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; } diff --git a/BTCPayServer/Services/Wallets/BTCPayWallet.cs b/BTCPayServer/Services/Wallets/BTCPayWallet.cs index fe807afd7..2877c91c3 100644 --- a/BTCPayServer/Services/Wallets/BTCPayWallet.cs +++ b/BTCPayServer/Services/Wallets/BTCPayWallet.cs @@ -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) diff --git a/BTCPayServer/Views/Invoice/ListInvoices.cshtml b/BTCPayServer/Views/Invoice/ListInvoices.cshtml index 35781fd26..96707d2a5 100644 --- a/BTCPayServer/Views/Invoice/ListInvoices.cshtml +++ b/BTCPayServer/Views/Invoice/ListInvoices.cshtml @@ -48,7 +48,7 @@ InvoiceId Status Amount - Actions + Actions diff --git a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml index 8726a2494..6be492c03 100644 --- a/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml +++ b/BTCPayServer/Views/Stores/AddDerivationScheme.cshtml @@ -31,7 +31,7 @@
    -
  • Activate the Browser support and refresh this page
  • +
  • Make sure you are running the Ledger Bitcoin or Litecoin app with version superior or equal to 1.2.4
  • Use a browser supporting the U2F protocol

Detecting hardware wallet...

diff --git a/BTCPayServer/wwwroot/js/StoreWallet.js b/BTCPayServer/wwwroot/js/StoreWallet.js index c5373fc11..dbd370d22 100644 --- a/BTCPayServer/wwwroot/js/StoreWallet.js +++ b/BTCPayServer/wwwroot/js/StoreWallet.js @@ -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) {