mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Merge remote-tracking branch 'source/master' into dev-shapeshift
This commit is contained in:
@@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
BTCPayServer.Tests/LightningDTester.cs
Normal file
24
BTCPayServer.Tests/LightningDTester.cs
Normal 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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ docker-compose down
|
|||||||
If you want to stop, and remove all existing data
|
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
|
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
|
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`:
|
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:
|
If you are using Powershell:
|
||||||
@@ -47,7 +49,29 @@ If you are using Powershell:
|
|||||||
.\docker-bitcoin-cli.ps1 sendtoaddress "mohu16LH66ptoWGEL1GtP6KHTBJYXMWhEf" 0.23111090
|
.\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
|
## FAQ
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using BTCPayServer.Payments.Lightning.Eclair;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using BTCPayServer.Payments.Lightning.CLightning.RPC;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
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/")));
|
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||||
|
|
||||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "http://127.0.0.1:30992/")), btc);
|
||||||
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", 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"))
|
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||||
{
|
{
|
||||||
@@ -73,55 +73,63 @@ namespace BTCPayServer.Tests
|
|||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This will setup a channel going from customer to merchant
|
/// Connect a customer LN node to the merchant LN node
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PrepareLightning()
|
public void PrepareLightning()
|
||||||
{
|
{
|
||||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connect a customer LN node to the merchant LN node
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task PrepareLightningAsync()
|
public async Task PrepareLightningAsync()
|
||||||
{
|
{
|
||||||
// Activate segwit
|
while (true)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var c = (await CustomerEclair.RPC.ChannelsAsync());
|
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||||
bool generated = false;
|
.SelectMany(p => p.Channels)
|
||||||
bool createdChannel = false;
|
.FirstOrDefault();
|
||||||
CancellationTokenSource timeout = new CancellationTokenSource();
|
switch (channel?.State)
|
||||||
timeout.CancelAfter(10000);
|
|
||||||
while (c.Length == 0 || c[0].State != "NORMAL")
|
|
||||||
{
|
{
|
||||||
if (timeout.IsCancellationRequested)
|
case null:
|
||||||
{
|
var merchantInfo = await WaitLNSynched();
|
||||||
timeout = new CancellationTokenSource();
|
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
|
||||||
timeout.CancelAfter(10000);
|
await CustomerLightningD.ConnectAsync(clightning);
|
||||||
createdChannel = c.Length == 0;
|
var address = await CustomerLightningD.NewAddressAsync();
|
||||||
generated = false;
|
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||||
}
|
ExplorerNode.Generate(1);
|
||||||
if (!createdChannel)
|
await WaitLNSynched();
|
||||||
{
|
await Task.Delay(1000);
|
||||||
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
|
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||||
createdChannel = true;
|
break;
|
||||||
}
|
case "CHANNELD_AWAITING_LOCKIN":
|
||||||
if (!generated && c.Length != 0 && c[0].State == "WAIT_FOR_FUNDING_CONFIRMED")
|
ExplorerNode.Generate(1);
|
||||||
{
|
await WaitLNSynched();
|
||||||
ExplorerNode.Generate(6);
|
break;
|
||||||
generated = true;
|
case "CHANNELD_NORMAL":
|
||||||
}
|
return;
|
||||||
c = (await CustomerEclair.RPC.ChannelsAsync());
|
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;
|
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||||
await CustomerEclair.RPC.SendAsync(bolt11);
|
await CustomerLightningD.SendAsync(bolt11);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EclairTester MerchantEclair { get; set; }
|
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||||
public EclairTester CustomerEclair { get; set; }
|
|
||||||
public ChargeTester MerchantCharge { get; private set; }
|
public ChargeTester MerchantCharge { get; private set; }
|
||||||
|
|
||||||
internal string GetEnvironment(string variable, string defaultValue)
|
internal string GetEnvironment(string variable, string defaultValue)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ using BTCPayServer.Data;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using BTCPayServer.Payments.Lightning.Eclair;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -45,7 +44,7 @@ namespace BTCPayServer.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void CanCalculateCryptoDue2()
|
public void CanCalculateCryptoDue2()
|
||||||
{
|
{
|
||||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest);
|
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
3
BTCPayServer.Tests/docker-bitcoin-cli.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
# 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.
|
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||||
services:
|
services:
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ services:
|
|||||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||||
TESTS_PORT: 80
|
TESTS_PORT: 80
|
||||||
TESTS_HOSTNAME: tests
|
TESTS_HOSTNAME: tests
|
||||||
TEST_ECLAIR: http://eclair-cli:gpwefwmmewci@eclair:8080/
|
TEST_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
|
||||||
TEST_CHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||||
expose:
|
expose:
|
||||||
- "80"
|
- "80"
|
||||||
links:
|
links:
|
||||||
@@ -36,7 +36,8 @@ services:
|
|||||||
links:
|
links:
|
||||||
- nbxplorer
|
- nbxplorer
|
||||||
- postgres
|
- postgres
|
||||||
- eclair
|
- customer_lightningd
|
||||||
|
- merchant_lightningd
|
||||||
- lightning-charged
|
- lightning-charged
|
||||||
|
|
||||||
nbxplorer:
|
nbxplorer:
|
||||||
@@ -64,7 +65,6 @@ services:
|
|||||||
- litecoind
|
- litecoind
|
||||||
|
|
||||||
bitcoind:
|
bitcoind:
|
||||||
container_name: btcpayserver_dev_bitcoind
|
|
||||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_EXTRA_ARGS: |
|
BITCOIN_EXTRA_ARGS: |
|
||||||
@@ -75,11 +75,6 @@ services:
|
|||||||
rpcport=43782
|
rpcport=43782
|
||||||
port=39388
|
port=39388
|
||||||
whitelist=0.0.0.0/0
|
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:
|
ports:
|
||||||
- "43782:43782"
|
- "43782:43782"
|
||||||
expose:
|
expose:
|
||||||
@@ -88,8 +83,27 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- "bitcoin_datadir:/data"
|
- "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:
|
lightning-charged:
|
||||||
image: shesek/lightning-charge:0.3.1
|
image: shesek/lightning-charge:0.3.5
|
||||||
environment:
|
environment:
|
||||||
NETWORK: regtest
|
NETWORK: regtest
|
||||||
API_TOKEN: foiewnccewuify
|
API_TOKEN: foiewnccewuify
|
||||||
@@ -97,6 +111,8 @@ services:
|
|||||||
BITCOIND_RPCCONNECT: bitcoind
|
BITCOIND_RPCCONNECT: bitcoind
|
||||||
volumes:
|
volumes:
|
||||||
- "bitcoin_datadir:/etc/bitcoin"
|
- "bitcoin_datadir:/etc/bitcoin"
|
||||||
|
- "lightning_charge_datadir:/data"
|
||||||
|
- "merchant_lightningd_datadir:/etc/lightning"
|
||||||
expose:
|
expose:
|
||||||
- "9112" # Charge
|
- "9112" # Charge
|
||||||
- "9735" # Lightning
|
- "9735" # Lightning
|
||||||
@@ -104,32 +120,28 @@ services:
|
|||||||
- "54938:9112" # Charge
|
- "54938:9112" # Charge
|
||||||
links:
|
links:
|
||||||
- bitcoind
|
- bitcoind
|
||||||
|
- merchant_lightningd
|
||||||
|
|
||||||
eclair:
|
merchant_lightningd:
|
||||||
image: acinq/eclair@sha256:758eaf02683046a096ee03390d3a54df8fcfca50883f7560ab946a36ee4e81d8
|
image: nicolasdorier/clightning
|
||||||
environment:
|
environment:
|
||||||
JAVA_OPTS: >
|
LIGHTNINGD_OPT: |
|
||||||
-Xmx512m
|
bitcoin-datadir=/etc/bitcoin
|
||||||
-Declair.printToConsole
|
bitcoin-rpcconnect=bitcoind
|
||||||
-Declair.bitcoind.host=bitcoind
|
network=regtest
|
||||||
-Declair.bitcoind.rpcport=43782
|
log-level=debug
|
||||||
-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
|
|
||||||
ports:
|
ports:
|
||||||
- "30992:8080" # api port
|
- "30993:9835" # api port
|
||||||
expose:
|
expose:
|
||||||
- "9735" # server port
|
- "9735" # server port
|
||||||
- "8080" # api port
|
- "9835" # api port
|
||||||
|
volumes:
|
||||||
|
- "bitcoin_datadir:/etc/bitcoin"
|
||||||
|
- "merchant_lightningd_datadir:/root/.lightning"
|
||||||
|
links:
|
||||||
|
- bitcoind
|
||||||
|
|
||||||
litecoind:
|
litecoind:
|
||||||
container_name: btcpayserver_dev_litecoind
|
|
||||||
image: nicolasdorier/docker-litecoin:0.14.2
|
image: nicolasdorier/docker-litecoin:0.14.2
|
||||||
environment:
|
environment:
|
||||||
BITCOIN_EXTRA_ARGS: |
|
BITCOIN_EXTRA_ARGS: |
|
||||||
@@ -155,3 +167,6 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
bitcoin_datadir:
|
bitcoin_datadir:
|
||||||
|
customer_lightningd_datadir:
|
||||||
|
merchant_lightningd_datadir:
|
||||||
|
lightning_charge_datadir:
|
||||||
|
|||||||
1
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
1
BTCPayServer.Tests/docker-customer-lightning-cli.ps1
Normal file
@@ -0,0 +1 @@
|
|||||||
|
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
|
||||||
3
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
3
BTCPayServer.Tests/docker-customer-lightning-cli.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
|
||||||
@@ -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
|
||||||
|
|||||||
3
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
3
BTCPayServer.Tests/docker-litecoin-cli.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
<Version>1.0.1.46</Version>
|
<Version>1.0.1.50</Version>
|
||||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ namespace BTCPayServer.Controllers
|
|||||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||||
if(onchainMethod != null)
|
if(onchainMethod != null)
|
||||||
{
|
{
|
||||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||||
}
|
}
|
||||||
cryptoPayment.Rate = FormatCurrency(data);
|
cryptoPayment.Rate = FormatCurrency(data);
|
||||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ namespace BTCPayServer.Controllers
|
|||||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||||
vm.DerivationScheme = strategy.ToString();
|
vm.DerivationScheme = strategy.ToString();
|
||||||
}
|
}
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -75,7 +74,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (vm.Confirmation)
|
if (vm.Confirmation || strategy == null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace BTCPayServer.Data
|
|||||||
}
|
}
|
||||||
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
||||||
{
|
{
|
||||||
Address = address + "#" + paymentMethodId?.ToString();
|
Address = address + "#" + paymentMethodId.ToString();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public PaymentMethodId GetpaymentMethodId()
|
public PaymentMethodId GetpaymentMethodId()
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ namespace BTCPayServer.Data
|
|||||||
{
|
{
|
||||||
DerivationStrategy = null;
|
DerivationStrategy = null;
|
||||||
}
|
}
|
||||||
else if (!existing)
|
else if (!existing && supportedPaymentMethod != null)
|
||||||
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
|
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
|
||||||
DerivationStrategies = strategies.ToString();
|
DerivationStrategies = strategies.ToString();
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ namespace BTCPayServer
|
|||||||
{
|
{
|
||||||
public static class Extensions
|
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)
|
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||||
{
|
{
|
||||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ namespace BTCPayServer.JsonConverters
|
|||||||
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type longType = typeof(long).GetTypeInfo();
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return reader.TokenType == JsonToken.Null ? null :
|
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))
|
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
|
|
||||||
public string GetPaymentDestination()
|
public string GetPaymentDestination()
|
||||||
{
|
{
|
||||||
return DepositAddress?.ToString();
|
return DepositAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public decimal GetTxFee()
|
public decimal GetTxFee()
|
||||||
@@ -33,10 +33,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
|
|
||||||
public void SetPaymentDestination(string newPaymentDestination)
|
public void SetPaymentDestination(string newPaymentDestination)
|
||||||
{
|
{
|
||||||
if (newPaymentDestination == null)
|
DepositAddress = newPaymentDestination;
|
||||||
DepositAddress = null;
|
|
||||||
else
|
|
||||||
DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
|
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
|
||||||
@@ -45,7 +42,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Money TxFee { get; set; }
|
public Money TxFee { get; set; }
|
||||||
[JsonIgnore]
|
[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);
|
||||||
|
}
|
||||||
///////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||||
onchainMethod.FeeRate = await getFeeRate;
|
onchainMethod.FeeRate = await getFeeRate;
|
||||||
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||||
onchainMethod.DepositAddress = await getAddress;
|
onchainMethod.DepositAddress = (await getAddress).ToString();
|
||||||
return onchainMethod;
|
return onchainMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
if (!alreadyExist)
|
if (!alreadyExist)
|
||||||
{
|
{
|
||||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -330,7 +331,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
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++;
|
totalPayment++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,11 +353,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||||
if (paymentMethod != null &&
|
if (paymentMethod != null &&
|
||||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
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)
|
paymentMethod.Calculate().Due > Money.Zero)
|
||||||
{
|
{
|
||||||
var address = await wallet.ReserveAddressAsync(strategy);
|
var address = await wallet.ReserveAddressAsync(strategy);
|
||||||
btc.DepositAddress = address;
|
btc.DepositAddress = address.ToString();
|
||||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||||
paymentMethod.SetPaymentMethodDetails(btc);
|
paymentMethod.SetPaymentMethodDetails(btc);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
|
||||||
{
|
{
|
||||||
public class NodeInfo
|
public class NodeInfo
|
||||||
{
|
{
|
||||||
60
BTCPayServer/Payments/Lightning/CLightning/RPC/PeerInfo.cs
Normal file
60
BTCPayServer/Payments/Lightning/CLightning/RPC/PeerInfo.cs
Normal 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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,6 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
if (Listening(invoiceId))
|
if (Listening(invoiceId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||||
@@ -92,7 +91,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||||
if (chargeInvoice == null)
|
if (chargeInvoice == null)
|
||||||
continue;
|
continue;
|
||||||
if(chargeInvoice.Status == "paid")
|
if (chargeInvoice.Status == "paid")
|
||||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
||||||
continue;
|
continue;
|
||||||
@@ -157,18 +156,20 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||||
|
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
|
||||||
}
|
}
|
||||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
|
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,
|
BOLT11 = notification.PaymentRequest,
|
||||||
Amount = notification.MilliSatoshi
|
Amount = notification.MilliSatoshi
|
||||||
}, network.CryptoCode, accounted: true);
|
}, 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)
|
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
@@ -202,9 +203,26 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
return false;
|
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)
|
bool Listening(string invoiceId)
|
||||||
{
|
{
|
||||||
lock(_ListenedInvoiceByLightningUrl)
|
lock (_ListenedInvoiceByLightningUrl)
|
||||||
{
|
{
|
||||||
return _InvoiceIds.Contains(invoiceId);
|
return _InvoiceIds.Contains(invoiceId);
|
||||||
}
|
}
|
||||||
@@ -230,13 +248,6 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
{
|
{
|
||||||
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
||||||
_ListeningLightning.Add(listen);
|
_ListeningLightning.Add(listen);
|
||||||
listen.ContinueWith(_ =>
|
|
||||||
{
|
|
||||||
lock (_ListenedInvoiceByLightningUrl)
|
|
||||||
{
|
|
||||||
_ListeningLightning.Remove(listen);
|
|
||||||
}
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
||||||
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,7 @@ namespace BTCPayServer.Payments.Lightning
|
|||||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
var invoice = paymentMethod.ParentEntity;
|
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 client = GetClient(supportedPaymentMethod, network);
|
||||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||||
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
|
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
|
||||||
|
|||||||
@@ -582,7 +582,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||||
{
|
{
|
||||||
FeeRate = FeeRate,
|
FeeRate = FeeRate,
|
||||||
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork),
|
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
|
||||||
TxFee = TxFee
|
TxFee = TxFee
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||||
{
|
{
|
||||||
btcLike.TxFee = TxFee;
|
btcLike.TxFee = TxFee;
|
||||||
btcLike.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
|
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
|
||||||
btcLike.FeeRate = FeeRate;
|
btcLike.FeeRate = FeeRate;
|
||||||
}
|
}
|
||||||
return details;
|
return details;
|
||||||
@@ -615,7 +615,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
{
|
{
|
||||||
TxFee = bitcoinPaymentMethod.TxFee;
|
TxFee = bitcoinPaymentMethod.TxFee;
|
||||||
FeeRate = bitcoinPaymentMethod.FeeRate;
|
FeeRate = bitcoinPaymentMethod.FeeRate;
|
||||||
DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString();
|
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||||
}
|
}
|
||||||
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
|
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
|
||||||
PaymentMethodDetails = jobj;
|
PaymentMethodDetails = jobj;
|
||||||
@@ -646,8 +646,9 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
var paid = 0m;
|
var paid = 0m;
|
||||||
var cryptoPaid = 0.0m;
|
var cryptoPaid = 0.0m;
|
||||||
|
|
||||||
|
int precision = 8;
|
||||||
var paidTxFee = 0m;
|
var paidTxFee = 0m;
|
||||||
bool paidEnough = paid >= RoundUp(totalDue, 8);
|
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||||
int txRequired = 0;
|
int txRequired = 0;
|
||||||
var payments =
|
var payments =
|
||||||
ParentEntity.GetPayments()
|
ParentEntity.GetPayments()
|
||||||
@@ -662,7 +663,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
totalDue += txFee;
|
totalDue += txFee;
|
||||||
paidTxFee += txFee;
|
paidTxFee += txFee;
|
||||||
}
|
}
|
||||||
paidEnough |= paid >= RoundUp(totalDue, 8);
|
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
|
||||||
if (GetId() == _.GetPaymentMethodId())
|
if (GetId() == _.GetPaymentMethodId())
|
||||||
{
|
{
|
||||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||||
@@ -681,7 +682,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
paidTxFee += GetTxFee();
|
paidTxFee += GetTxFee();
|
||||||
}
|
}
|
||||||
|
|
||||||
accounting.TotalDue = Money.Coins(RoundUp(totalDue, 8));
|
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||||
accounting.Paid = Money.Coins(paid);
|
accounting.Paid = Money.Coins(paid);
|
||||||
accounting.TxRequired = txRequired;
|
accounting.TxRequired = txRequired;
|
||||||
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
||||||
@@ -691,20 +692,6 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return accounting;
|
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()
|
private decimal GetTxFee()
|
||||||
{
|
{
|
||||||
var method = GetPaymentMethodDetails();
|
var method = GetPaymentMethodDetails();
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
throw new InvalidOperationException("CryptoCode unsupported");
|
throw new InvalidOperationException("CryptoCode unsupported");
|
||||||
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||||
|
|
||||||
string address = GetDestination(paymentMethod);
|
string address = GetDestination(paymentMethod, paymentMethod.Network.NBitcoinNetwork);
|
||||||
context.AddressInvoices.Add(new AddressInvoiceData()
|
context.AddressInvoices.Add(new AddressInvoiceData()
|
||||||
{
|
{
|
||||||
InvoiceDataId = invoice.Id,
|
InvoiceDataId = invoice.Id,
|
||||||
@@ -162,12 +162,12 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
return invoice;
|
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
|
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
|
||||||
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
|
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();
|
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||||
@@ -209,7 +209,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
InvoiceDataId = invoiceId,
|
InvoiceDataId = invoiceId,
|
||||||
CreatedTime = DateTimeOffset.UtcNow
|
CreatedTime = DateTimeOffset.UtcNow
|
||||||
}
|
}
|
||||||
.Set(GetDestination(currencyData), currencyData.GetId()));
|
.Set(GetDestination(currencyData, network.NBitcoinNetwork), currencyData.GetId()));
|
||||||
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
||||||
{
|
{
|
||||||
InvoiceDataId = invoiceId,
|
InvoiceDataId = invoiceId,
|
||||||
@@ -461,6 +461,15 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
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)
|
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
@@ -486,7 +495,11 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
|
|
||||||
context.Payments.Add(data);
|
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());
|
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,17 +126,18 @@ namespace BTCPayServer.Services.Wallets
|
|||||||
}
|
}
|
||||||
catch
|
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;
|
throw;
|
||||||
}
|
}
|
||||||
var spentTime = DateTimeOffset.UtcNow - now;
|
var spentTime = DateTimeOffset.UtcNow - now;
|
||||||
if (spentTime.TotalSeconds > 30)
|
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;
|
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
|
||||||
completionSource.TrySetResult(utxos);
|
completionSource.TrySetResult(utxos);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<th>InvoiceId</th>
|
<th>InvoiceId</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th style="text-align:right">Amount</th>
|
<th style="text-align:right">Amount</th>
|
||||||
<th style="text-align:right">Actions</th>
|
<th style="text-align:right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<input asp-for="DerivationScheme" class="form-control" />
|
<input asp-for="DerivationScheme" class="form-control" />
|
||||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||||
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
|
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
|
||||||
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
|
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
|
||||||
</p>
|
</p>
|
||||||
<p id="ledger-info" class="form-text text-muted" style="display: none;">
|
<p id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||||
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
|
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
If your Ledger wallet is not detected:
|
If your Ledger wallet is not detected:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
|
<li>Make sure you are running the Ledger Bitcoin or Litecoin app with version superior or equal to 1.2.4</li>
|
||||||
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
|
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
.catch(function (reason)
|
.catch(function (reason)
|
||||||
{
|
{
|
||||||
if (reason.message === "Sign failed")
|
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);
|
Write('hw', 'error', reason);
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user