Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
acb2407654 | |||
b8a4f0c012 | |||
57bb3b231c | |||
e5d626e0fd | |||
e2c4c913ff | |||
09f97915d6 | |||
81328b2667 | |||
0d8affc68d | |||
b28b3ef4ff | |||
9e2e102ec4 | |||
cbd40d49c1 | |||
1d051648b7 | |||
49cf804914 | |||
0f6ad75536 | |||
56eea18b2d | |||
b3698846c6 | |||
6806d96baa | |||
dc3b3077c2 | |||
936ae64ca3 | |||
3a0a5dbd7f | |||
ed4430ae7d | |||
9a0e4e35d9 | |||
5715dd2058 | |||
da4c132f9d | |||
303a617f9e | |||
3116ec9cb8 | |||
b3f4eab075 | |||
c98593b47b | |||
2c49d61682 | |||
21f5c94cff | |||
834ba4afab | |||
d690cd6b7b | |||
937bd07daa | |||
559b822111 | |||
7afb4c6b11 | |||
1c98a3a33d | |||
3320eb284e | |||
919fb60558 | |||
7796589105 | |||
de6d3198ff | |||
f1e971d047 | |||
acd98aad32 | |||
b0c810398c | |||
03a0044745 | |||
15684efdce | |||
b67a962d12 | |||
339cedadf7 | |||
e19d730fb7 | |||
649497e54f | |||
40eee0924a | |||
bbf5fb3c30 | |||
346cdf2431 | |||
030cb09af8 |
17
.gitattributes
vendored
Normal file
17
.gitattributes
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sh text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.1" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
@ -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
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
|
@ -17,8 +17,8 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning.RPC;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -56,8 +56,8 @@ namespace BTCPayServer.Tests
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
|
||||
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "http://127.0.0.1:30992/")), btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
@ -73,55 +73,63 @@ namespace BTCPayServer.Tests
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This will setup a channel going from customer to merchant
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PrepareLightningAsync()
|
||||
{
|
||||
// Activate segwit
|
||||
var blockCount = ExplorerNode.GetBlockCountAsync();
|
||||
// Fetch node info, but that in cache
|
||||
var merchantInfo = MerchantCharge.Client.GetInfoAsync();
|
||||
var customer = CustomerEclair.GetNodeInfoAsync();
|
||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||
|
||||
var info = await merchantInfo;
|
||||
var clightning = new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port);
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(clightning);
|
||||
await Task.WhenAll(blockCount, customer, channels, connect);
|
||||
|
||||
// If the channel is not created, let's do it
|
||||
if (channels.Result.Length == 0)
|
||||
while (true)
|
||||
{
|
||||
var c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
bool generated = false;
|
||||
bool createdChannel = false;
|
||||
CancellationTokenSource timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
while (c.Length == 0 || c[0].State != "NORMAL")
|
||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||
.SelectMany(p => p.Channels)
|
||||
.FirstOrDefault();
|
||||
switch (channel?.State)
|
||||
{
|
||||
if (timeout.IsCancellationRequested)
|
||||
{
|
||||
timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
createdChannel = c.Length == 0;
|
||||
generated = false;
|
||||
}
|
||||
if (!createdChannel)
|
||||
{
|
||||
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
|
||||
createdChannel = true;
|
||||
}
|
||||
if (!generated && c.Length != 0 && c[0].State == "WAIT_FOR_FUNDING_CONFIRMED")
|
||||
{
|
||||
ExplorerNode.Generate(6);
|
||||
generated = true;
|
||||
}
|
||||
c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
case null:
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
|
||||
await CustomerLightningD.ConnectAsync(clightning);
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await Task.Delay(1000);
|
||||
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
default:
|
||||
throw new NotSupportedException(channel?.State ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Payments.Lightning.CLightning.GetInfoResponse> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,11 +143,10 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerEclair.RPC.SendAsync(bolt11);
|
||||
await CustomerLightningD.SendAsync(bolt11);
|
||||
}
|
||||
|
||||
public EclairTester MerchantEclair { get; set; }
|
||||
public EclairTester CustomerEclair { get; set; }
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
|
@ -22,7 +22,6 @@ using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using System.Threading.Tasks;
|
||||
@ -45,7 +44,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue2()
|
||||
{
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest);
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
|
||||
#pragma warning disable CS0618
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
@ -347,7 +346,6 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
|
@ -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"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
@ -17,8 +17,8 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_ECLAIR: http://eclair-cli:gpwefwmmewci@eclair:8080/
|
||||
TEST_CHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
TEST_CUSTOMERLIGHTNINGD: http://customer_lightningd:9835/
|
||||
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
@ -36,11 +36,12 @@ services:
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- eclair
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.16
|
||||
image: nicolasdorier/nbxplorer:1.0.1.22
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -64,7 +65,6 @@ services:
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -75,11 +75,6 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:29000
|
||||
zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
txindex=1
|
||||
# Eclair is still using addwitnessaddress
|
||||
deprecatedrpc=addwitnessaddress
|
||||
ports:
|
||||
- "43782:43782"
|
||||
expose:
|
||||
@ -88,8 +83,27 @@ services:
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning
|
||||
environment:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
log-level=debug
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "customer_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.1
|
||||
image: shesek/lightning-charge:0.3.5
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -97,6 +111,8 @@ services:
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
@ -104,32 +120,28 @@ services:
|
||||
- "54938:9112" # Charge
|
||||
links:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
eclair:
|
||||
image: acinq/eclair@sha256:758eaf02683046a096ee03390d3a54df8fcfca50883f7560ab946a36ee4e81d8
|
||||
environment:
|
||||
JAVA_OPTS: >
|
||||
-Xmx512m
|
||||
-Declair.printToConsole
|
||||
-Declair.bitcoind.host=bitcoind
|
||||
-Declair.bitcoind.rpcport=43782
|
||||
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
|
||||
-Declair.bitcoind.rpcpassword=DwubwWsoo3
|
||||
-Declair.bitcoind.zmq=tcp://bitcoind:29000
|
||||
-Declair.api.enabled=true
|
||||
-Declair.api.password=gpwefwmmewci
|
||||
-Declair.chain=regtest
|
||||
-Declair.api.binding-ip=0.0.0.0
|
||||
links:
|
||||
- bitcoind
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning
|
||||
environment:
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
log-level=debug
|
||||
ports:
|
||||
- "30992:8080" # api port
|
||||
- "30993:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "8080" # api port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -155,3 +167,6 @@ services:
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
|
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>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.39</Version>
|
||||
<Version>1.0.1.50</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -31,10 +31,10 @@
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.56" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.65" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.16" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
|
@ -68,16 +68,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if(onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
@ -86,13 +86,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var payments = invoice
|
||||
.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
@ -121,12 +121,32 @@ namespace BTCPayServer.Controllers
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Addresses = invoice.HistoricalAddresses;
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h=> new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var type = paymentMethodId.PaymentType.ToString();
|
||||
switch (paymentMethodId.PaymentType)
|
||||
{
|
||||
case PaymentTypes.BTCLike:
|
||||
type = "On-Chain";
|
||||
break;
|
||||
case PaymentTypes.LightningLike:
|
||||
type = "Off-Chain";
|
||||
break;
|
||||
}
|
||||
return $"{paymentMethodId.CryptoCode} ({type})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
@ -220,7 +240,7 @@ namespace BTCPayServer.Controllers
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetpaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
@ -348,6 +368,7 @@ namespace BTCPayServer.Controllers
|
||||
Date = Prettify(invoice.InvoiceTime),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
|
@ -368,7 +368,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!user.TwoFactorEnabled)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
return View(nameof(Disable2fa));
|
||||
@ -387,7 +387,7 @@ namespace BTCPayServer.Controllers
|
||||
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
|
||||
if (!disable2faResult.Succeeded)
|
||||
{
|
||||
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
|
||||
throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
|
||||
|
@ -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
|
||||
{
|
||||
@ -197,7 +196,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for substract fees"); }
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
@ -224,17 +223,20 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
||||
feeRateValue,
|
||||
changeAddress.Item1,
|
||||
changeAddress.Item2);
|
||||
changeAddress.Item2, summary.Status.BitcoinStatus.MinRelayTxFee);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
|
@ -54,10 +54,11 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (uri.Scheme != "https")
|
||||
var domain = GetDomain(uri.AbsoluteUri);
|
||||
if (uri.Scheme != "https" && domain != "127.0.0.1" && domain != "localhost")
|
||||
{
|
||||
var internalNode = GetInternalLightningNodeIfAuthorized();
|
||||
if (internalNode == null || GetDomain(internalNode) != GetDomain(uri.AbsoluteUri))
|
||||
if (internalNode == null || GetDomain(internalNode) != domain)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
@ -31,6 +32,7 @@ namespace BTCPayServer.Controllers
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
@ -45,6 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
@ -59,6 +62,7 @@ namespace BTCPayServer.Controllers
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
}
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
@ -516,7 +520,7 @@ namespace BTCPayServer.Controllers
|
||||
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
|
||||
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
|
||||
{
|
||||
StatusMessage = "Pairing is successfull";
|
||||
StatusMessage = "Pairing is successful";
|
||||
if (pairingResult == PairingResult.Partial)
|
||||
StatusMessage = "Server initiated pairing code: " + pairingCode;
|
||||
return RedirectToAction(nameof(ListTokens), new
|
||||
|
@ -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()
|
||||
|
@ -27,9 +27,10 @@ namespace BTCPayServer.Data
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetCryptoCode()
|
||||
public Payments.PaymentMethodId GetPaymentMethodId()
|
||||
{
|
||||
return string.IsNullOrEmpty(CryptoCode) ? "BTC" : CryptoCode;
|
||||
return string.IsNullOrEmpty(CryptoCode) ? new Payments.PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike)
|
||||
: Payments.PaymentMethodId.Parse(CryptoCode);
|
||||
}
|
||||
public string GetAddress()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -77,7 +77,7 @@ namespace BTCPayServer
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode, out var unused);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
|
@ -30,6 +30,19 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value * 10m;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value / 10m;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
|
@ -75,7 +75,8 @@ namespace BTCPayServer.HostedServices
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
var response = await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
@ -112,7 +113,7 @@ namespace BTCPayServer.HostedServices
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
reschedule = !response.IsSuccessStatusCode;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
|
@ -41,9 +41,11 @@ namespace BTCPayServer.HostedServices
|
||||
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public bool IsFullySynched(string cryptoCode)
|
||||
public bool IsFullySynched(string cryptoCode, out NBXplorerSummary summary)
|
||||
{
|
||||
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
return _Summaries.TryGetValue(cryptoCode, out summary) &&
|
||||
summary.Status != null &&
|
||||
summary.Status.IsFullySynched;
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -12,16 +12,22 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class CryptoPayment
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Due { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public string Rate { get; internal set; }
|
||||
public string PaymentUrl { get; internal set; }
|
||||
}
|
||||
public class AddressModel
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public bool Current { get; set; }
|
||||
}
|
||||
public class Payment
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Confirmations
|
||||
{
|
||||
get; set;
|
||||
@ -126,7 +132,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public HistoricalAddressInvoiceData[] Addresses { get; set; }
|
||||
public AddressModel[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
public string InvoiceId
|
||||
{
|
||||
get; set;
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
/// there is functionality to allow adding or removing more than a single
|
||||
/// value at once.
|
||||
///
|
||||
/// The MultiValueDictionary can also be viewed as a IReadOnlyDictionary<TKey,IReadOnlyCollection<TValue>t>
|
||||
/// The MultiValueDictionary can also be viewed as an IReadOnlyDictionary<TKey,IReadOnlyCollection<TValue>t>
|
||||
/// where the <see cref="IReadOnlyCollection{TValue}" /> is abstracted from the view of the programmer.
|
||||
///
|
||||
/// For a read-only MultiValueDictionary.
|
||||
|
@ -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);
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -98,20 +98,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
|
||||
await Task.WhenAll(invoice.GetSupportedPaymentMethod<DerivationStrategy>(_NetworkProvider)
|
||||
.Select(s => (Session: _SessionsByCryptoCode.TryGet(s.PaymentId.CryptoCode),
|
||||
DerivationStrategy: s.DerivationStrategyBase))
|
||||
.Where(s => s.Session != null)
|
||||
.Select(s => s.Session.ListenDerivationSchemesAsync(new[] { s.DerivationStrategy }))
|
||||
.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -139,7 +125,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
using (session)
|
||||
{
|
||||
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
|
||||
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
|
||||
await session.ListenAllDerivationSchemesAsync(cancellation: _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
|
||||
int paymentCount = await FindPaymentViaPolling(wallet, network);
|
||||
@ -174,7 +160,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
if (!alreadyExist)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
if(payment != null)
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -213,7 +200,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
|
||||
{
|
||||
return invoice.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
|
||||
}
|
||||
|
||||
@ -227,7 +214,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var conflicts = GetConflicts(transactions.Select(t => t.Value));
|
||||
foreach (var payment in invoice.GetPayments(wallet.Network))
|
||||
{
|
||||
if (payment.GetpaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
||||
if (payment.GetPaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
||||
continue;
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
|
||||
@ -279,7 +266,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
}
|
||||
else
|
||||
{
|
||||
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
|
||||
// Take the most recent (bitcoin node would not forward a conflict without a successful RBF)
|
||||
_Winner = Transactions
|
||||
.OrderByDescending(t => t.Value.Timestamp)
|
||||
.First()
|
||||
@ -344,7 +331,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
if (payment != null)
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
@ -365,11 +353,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
btc.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey &&
|
||||
btc.GetDepositAddress(wallet.Network.NBitcoinNetwork).ScriptPubKey == paymentData.Output.ScriptPubKey &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address;
|
||||
btc.DepositAddress = address.ToString();
|
||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
@ -379,21 +367,6 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
|
||||
{
|
||||
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
|
||||
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var strategy = GetDerivationStrategy(invoice, network);
|
||||
if (strategy != null)
|
||||
strategies.Add(strategy);
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
|
@ -5,11 +5,18 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
|
||||
public class GetInfoResponse
|
||||
{
|
||||
public class GetInfoAddress
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Address { get; set; }
|
||||
public int Port { get; set; }
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string[] Address { get; set; }
|
||||
public GetInfoAddress[] Address { get; set; }
|
||||
public string Version { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
public string Network { get; set; }
|
||||
|
@ -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.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning.RPC
|
||||
{
|
||||
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))
|
||||
return;
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
@ -92,7 +91,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||
if (chargeInvoice == null)
|
||||
continue;
|
||||
if(chargeInvoice.Status == "paid")
|
||||
if (chargeInvoice.Status == "paid")
|
||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
||||
continue;
|
||||
@ -157,18 +156,20 @@ namespace BTCPayServer.Payments.Lightning
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
DoneListening(supportedPaymentMethod.GetLightningChargeUrl(false));
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
}
|
||||
|
||||
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
|
||||
{
|
||||
await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.PaymentRequest,
|
||||
Amount = notification.MilliSatoshi
|
||||
}, network.CryptoCode, accounted: true);
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
if(payment != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
|
||||
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
@ -202,9 +203,26 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening all invoices on this server
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
private void DoneListening(Uri uri)
|
||||
{
|
||||
lock (_ListenedInvoiceByChargeInvoiceId)
|
||||
{
|
||||
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
|
||||
}
|
||||
}
|
||||
|
||||
bool Listening(string invoiceId)
|
||||
{
|
||||
lock(_ListenedInvoiceByLightningUrl)
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
return _InvoiceIds.Contains(invoiceId);
|
||||
}
|
||||
@ -230,13 +248,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
||||
_ListeningLightning.Add(listen);
|
||||
listen.ContinueWith(_ =>
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListeningLightning.Remove(listen);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
||||
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
@ -12,15 +13,15 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
|
||||
{
|
||||
ExplorerClientProvider _ExplorerClientProvider;
|
||||
public LightningLikePaymentHandler(ExplorerClientProvider explorerClientProvider)
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public LightningLikePaymentHandler(NBXplorerDashboard dashboard)
|
||||
{
|
||||
_ExplorerClientProvider = explorerClientProvider;
|
||||
_Dashboard = dashboard;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = invoice.ProductInformation.Price / paymentMethod.Rate;
|
||||
var 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()
|
||||
@ -47,13 +48,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_ExplorerClientProvider.IsAvailable(network))
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"Full node not available");
|
||||
|
||||
var explorerClient = _ExplorerClientProvider.GetExplorerClient(network);
|
||||
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
var client = GetClient(supportedPaymentMethod, network);
|
||||
var status = explorerClient.GetStatusAsync();
|
||||
GetInfoResponse info = null;
|
||||
try
|
||||
{
|
||||
@ -64,7 +64,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
|
||||
}
|
||||
var address = info.Address?.FirstOrDefault();
|
||||
var address = info.Address.Select(a=>a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
address = address ?? client.Uri.DnsSafeHost;
|
||||
|
||||
@ -73,9 +73,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
|
||||
}
|
||||
|
||||
if (Math.Abs(info.BlockHeight - (await status).ChainHeight) > 10)
|
||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||
if (blocksGap > 10)
|
||||
{
|
||||
throw new Exception($"The lightning node is not synched");
|
||||
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
|
||||
try
|
||||
@ -84,7 +85,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning node via {address} ({ex.Message})");
|
||||
throw new Exception($"Error while connecting to the lightning node via {address}:{port} ({ex.Message})");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,8 @@ namespace BTCPayServer.Services
|
||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
||||
FeeRate feeRate,
|
||||
IDestination changeAddress,
|
||||
KeyPath changeKeyPath)
|
||||
KeyPath changeKeyPath,
|
||||
FeeRate minTxRelayFee)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
@ -185,6 +186,7 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
|
@ -582,7 +582,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
FeeRate = FeeRate,
|
||||
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork),
|
||||
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
|
||||
TxFee = TxFee
|
||||
};
|
||||
}
|
||||
@ -592,7 +592,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||
{
|
||||
btcLike.TxFee = TxFee;
|
||||
btcLike.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
|
||||
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
|
||||
btcLike.FeeRate = FeeRate;
|
||||
}
|
||||
return details;
|
||||
@ -615,7 +615,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
TxFee = bitcoinPaymentMethod.TxFee;
|
||||
FeeRate = bitcoinPaymentMethod.FeeRate;
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString();
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||
}
|
||||
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
|
||||
PaymentMethodDetails = jobj;
|
||||
@ -646,8 +646,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
var paid = 0m;
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
int precision = 8;
|
||||
var paidTxFee = 0m;
|
||||
bool paidEnough = paid >= RoundUp(totalDue, 8);
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
var payments =
|
||||
ParentEntity.GetPayments()
|
||||
@ -655,15 +656,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetpaymentMethodId()].GetTxFee());
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee());
|
||||
paid += _.GetValue(paymentMethods, GetId());
|
||||
if (!paidEnough)
|
||||
{
|
||||
totalDue += txFee;
|
||||
paidTxFee += txFee;
|
||||
}
|
||||
paidEnough |= paid >= RoundUp(totalDue, 8);
|
||||
if (GetId() == _.GetpaymentMethodId())
|
||||
paidEnough |= paid >= Extensions.RoundUp(totalDue, precision);
|
||||
if (GetId() == _.GetPaymentMethodId())
|
||||
{
|
||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||
txRequired++;
|
||||
@ -681,7 +682,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
paidTxFee += GetTxFee();
|
||||
}
|
||||
|
||||
accounting.TotalDue = Money.Coins(RoundUp(totalDue, 8));
|
||||
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Money.Coins(paid);
|
||||
accounting.TxRequired = txRequired;
|
||||
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
||||
@ -691,20 +692,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
return accounting;
|
||||
}
|
||||
|
||||
private static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value * 10m;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value / 10m;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private decimal GetTxFee()
|
||||
{
|
||||
var method = GetPaymentMethodDetails();
|
||||
@ -766,7 +753,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
paymentData.Legacy = true;
|
||||
return paymentData;
|
||||
}
|
||||
if (GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
if (GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var paymentData = JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikePaymentData>(CryptoPaymentData);
|
||||
// legacy
|
||||
@ -774,7 +761,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
paymentData.Outpoint = Outpoint;
|
||||
return paymentData;
|
||||
}
|
||||
if(GetpaymentMethodId().PaymentType== PaymentTypes.LightningLike)
|
||||
if(GetPaymentMethodId().PaymentType== PaymentTypes.LightningLike)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
|
||||
}
|
||||
@ -802,7 +789,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
value = value ?? this.GetCryptoPaymentData().GetValue();
|
||||
var to = paymentMethodId;
|
||||
var from = this.GetpaymentMethodId();
|
||||
var from = this.GetPaymentMethodId();
|
||||
if (to == from)
|
||||
return decimal.Round(value.Value, 8);
|
||||
var fromRate = paymentMethods[from].Rate;
|
||||
@ -813,7 +800,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
return otherCurrencyValue;
|
||||
}
|
||||
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
public PaymentMethodId GetPaymentMethodId()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(CryptoPaymentDataType));
|
||||
|
@ -130,7 +130,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
throw new InvalidOperationException("CryptoCode unsupported");
|
||||
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||
|
||||
string address = GetDestination(paymentMethod);
|
||||
string address = GetDestination(paymentMethod, paymentMethod.Network.NBitcoinNetwork);
|
||||
context.AddressInvoices.Add(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
@ -162,12 +162,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private static string GetDestination(PaymentMethod paymentMethod)
|
||||
private static string GetDestination(PaymentMethod paymentMethod, Network network)
|
||||
{
|
||||
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
|
||||
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
|
||||
{
|
||||
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).DepositAddress.ScriptPubKey.Hash.ToString();
|
||||
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).GetDepositAddress(network).ScriptPubKey.Hash.ToString();
|
||||
}
|
||||
///////////////
|
||||
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||
@ -209,7 +209,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
InvoiceDataId = invoiceId,
|
||||
CreatedTime = DateTimeOffset.UtcNow
|
||||
}
|
||||
.Set(GetDestination(currencyData), currencyData.GetId()));
|
||||
.Set(GetDestination(currencyData, network.NBitcoinNetwork), currencyData.GetId()));
|
||||
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
@ -461,6 +461,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a payment to an invoice
|
||||
/// </summary>
|
||||
/// <param name="invoiceId"></param>
|
||||
/// <param name="date"></param>
|
||||
/// <param name="paymentData"></param>
|
||||
/// <param name="cryptoCode"></param>
|
||||
/// <param name="accounted"></param>
|
||||
/// <returns>The PaymentEntity or null if already added</returns>
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
@ -486,7 +495,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
context.Payments.Add(data);
|
||||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch(DbUpdateException) { return null; } // Already exists
|
||||
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
|
||||
return entity;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -1,4 +1,5 @@
|
||||
@model PaymentModel
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model PaymentModel
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = "Payment";
|
||||
@ -12,18 +13,13 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>BTCPay Invoice</title>
|
||||
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.css" rel="stylesheet" />
|
||||
<link href="~/css/css.css" rel="stylesheet" type="text/css">
|
||||
<link href="~/css/normalizer.css" rel="stylesheet" type="text/css">
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="~/vendor/clipboard.js/clipboard.js"></script>
|
||||
<script src="~/vendor/jquery/jquery.js"></script>
|
||||
<script src="~/js/vue.min.js" type="text/javascript"></script>
|
||||
<script src="~/js/vue-qrcode.js" type="text/javascript"></script>
|
||||
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||
</head>
|
||||
<body style="background: #E4E4E4">
|
||||
<noscript>
|
||||
@ -72,7 +68,7 @@
|
||||
<bp-spinner>
|
||||
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
|
||||
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
|
||||
</svg>
|
||||
</svg>
|
||||
</bp-spinner>
|
||||
</div>
|
||||
<div class="timer-row__message">
|
||||
@ -88,6 +84,33 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-details">
|
||||
@if (Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div class="currency-selection">
|
||||
<div class="single-item-order__left">
|
||||
<div style="font-weight: 600;">
|
||||
Pay with
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-item-order__right">
|
||||
<div class="payment__currencies">
|
||||
@foreach (var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
|
||||
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="payment__spinner">
|
||||
<bp-spinner>
|
||||
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
|
||||
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
|
||||
</svg>
|
||||
</bp-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!---->
|
||||
<div class="single-item-order buyerTotalLine">
|
||||
<div class="single-item-order__left">
|
||||
@ -153,8 +176,9 @@
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<qrcode :val="srvModel.invoiceBitcoinUrlQR" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
||||
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
@ -600,23 +624,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
|
||||
@if(Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div style="text-align:center">Accepted here</div>
|
||||
<div style="text-align:center">
|
||||
@foreach(var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.paymentMethodId='@crypto.PaymentMethodId'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" /></a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</invoice>
|
||||
<script type="text/javascript">
|
||||
// TODO: Move all logic from core.js to Vue controller
|
||||
Vue.config.ignoredElements = [
|
||||
'line-items',
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,9 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.money {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section>
|
||||
@ -67,7 +70,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Refund email</th>
|
||||
<td>@Model.RefundEmail</td>
|
||||
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
@ -83,7 +86,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Redirect Url</th>
|
||||
<td>@Model.RedirectUrl</td>
|
||||
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -98,7 +101,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>@Model.BuyerInformation.BuyerEmail</td>
|
||||
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
@ -153,22 +156,22 @@
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Rate</th>
|
||||
<th>Paid</th>
|
||||
<th>Due</th>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Address</th>
|
||||
<th class="money">Rate</th>
|
||||
<th class="money">Paid</th>
|
||||
<th class="money">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.CryptoCode</td>
|
||||
<td>@payment.Rate</td>
|
||||
<td>@payment.Paid</td>
|
||||
<td>@payment.Due</td>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
<td>@payment.Address</td>
|
||||
<td class="money">@payment.Rate</td>
|
||||
<td class="money">@payment.Paid</td>
|
||||
<td class="money">@payment.Due</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@ -181,24 +184,21 @@
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Date</th>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th>Confirmations</th>
|
||||
<th>Replaced</th>
|
||||
<th style="text-align:right;">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.Payments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "text-decoration: line-through;" : "";
|
||||
<tr>
|
||||
<td>@payment.CryptoCode</td>
|
||||
<td>@payment.ReceivedTime</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td>@payment.Confirmations</td>
|
||||
<td>@payment.Replaced</td>
|
||||
<td style="@replaced">@payment.PaymentMethod</td>
|
||||
<td style="@replaced">@payment.DepositAddress</td>
|
||||
<td style="@replaced"><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td style="text-align:right;">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@ -211,20 +211,19 @@
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Address</th>
|
||||
<th>Current</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var address in Model.Addresses)
|
||||
{
|
||||
{
|
||||
var current = address.Current ? "font-weight: bold;" : "";
|
||||
<tr>
|
||||
<td>@address.GetCryptoCode()</td>
|
||||
<td>@address.GetAddress()</td>
|
||||
<td>@(!address.UnAssigned.HasValue)</td>
|
||||
<td style="width:100px;@current">@address.PaymentMethod</td>
|
||||
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -47,8 +47,8 @@
|
||||
<th>OrderId</th>
|
||||
<th>InvoiceId</th>
|
||||
<th>Status</th>
|
||||
<th>Amount</th>
|
||||
<th>Actions</th>
|
||||
<th style="text-align:right">Amount</th>
|
||||
<th style="text-align:right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -56,7 +56,16 @@
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date</td>
|
||||
<td>@invoice.OrderId</td>
|
||||
<td>
|
||||
@if(invoice.RedirectUrl != string.Empty)
|
||||
{
|
||||
<a href="@invoice.RedirectUrl">@invoice.OrderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@invoice.OrderId</span>
|
||||
}
|
||||
</td>
|
||||
<td>@invoice.InvoiceId</td>
|
||||
@if(invoice.Status == "paid")
|
||||
{
|
||||
@ -73,10 +82,15 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<td>@invoice.Status</td>
|
||||
<td >@invoice.Status</td>
|
||||
}
|
||||
<td>@invoice.AmountCurrency</td>
|
||||
<td><a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> - <a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a></td>
|
||||
<td style="text-align:right">@invoice.AmountCurrency</td>
|
||||
<td style="text-align:right">
|
||||
@if(invoice.Status == "new")
|
||||
{
|
||||
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
|
||||
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
@ -58,7 +58,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Substract fees from amount</label>
|
||||
<label>Subtract fees from amount</label>
|
||||
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
|
||||
</div>
|
||||
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
|
@ -27,5 +27,23 @@
|
||||
"wwwroot/vendor/jquery-validate/jquery.validate.js",
|
||||
"wwwroot/vendor/jquery-validate-unobtrusive/jquery.validate.unobtrusive.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/checkout-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.css",
|
||||
"wwwroot/css/css.css",
|
||||
"wwwroot/css/normalizer.css"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/checkout-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/clipboard.js/clipboard.js",
|
||||
"wwwroot/vendor/jquery/jquery.js",
|
||||
"wwwroot/js/vue.min.js",
|
||||
"wwwroot/js/vue-qrcode.js",
|
||||
"wwwroot/js/core.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -8411,6 +8411,17 @@ strong {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.currency-selection {
|
||||
border-bottom: 1px solid #E9E9E9;
|
||||
position: relative;
|
||||
padding: 4px 15px;
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
color: #565D6E;
|
||||
letter-spacing: .45px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.single-item-order {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
@ -10487,6 +10498,7 @@ All mobile class names should be prefixed by m- */
|
||||
min-height: 575px;
|
||||
}
|
||||
|
||||
.paid .currency-selection,
|
||||
.expired .order-details,
|
||||
.archived .order-details {
|
||||
display: none;
|
||||
@ -10881,6 +10893,19 @@ bp-spinner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.payment__spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment__spinner > bp-spinner > svg {
|
||||
margin: auto 0px 0px auto;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
fill: gray;
|
||||
animation: spin 0.55s linear infinite;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
bp-refund-address.ng-valid .bitcoin-logo {
|
||||
opacity: 1;
|
||||
margin-left: 0;
|
||||
|
@ -1,6 +1,10 @@
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<g transform="translate(0.00630876,-0.00301984) scale(0.25,0.25)">
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 64 64"
|
||||
xml:space="preserve"
|
||||
xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<g transform="translate(0.00630876,-0.00301984)">
|
||||
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -1,156 +1,4 @@
|
||||
|
||||
/* TAF
|
||||
|
||||
- Version mobile
|
||||
|
||||
- Réparer le décallage par timer
|
||||
|
||||
- Preparer les variables de l'API
|
||||
|
||||
- Gestion des differents evenements en fonction du status de l'invoice
|
||||
|
||||
- sécuriser les CDN
|
||||
|
||||
*/
|
||||
|
||||
// TODO: Vue controller... complete migrate to it for binding, animations can stay in jQuery
|
||||
Vue.config.ignoredElements = [
|
||||
'line-items',
|
||||
'low-fee-timeline',
|
||||
// Ignoring custom HTML5 elements, eg: bp-spinner
|
||||
/^bp-/
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
}
|
||||
});
|
||||
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
// check if the Document expired
|
||||
if (srvModel.expirationSeconds > 0) {
|
||||
|
||||
progressStart(srvModel.maxTimeSeconds); // Progress bar
|
||||
startTimer(srvModel.expirationSeconds, display); // Timer
|
||||
|
||||
if (!validateEmail(srvModel.customerEmail))
|
||||
emailForm(); // Email form Display
|
||||
else
|
||||
hideEmailForm();
|
||||
}
|
||||
|
||||
|
||||
function hideEmailForm() {
|
||||
$("[role=document]").removeClass("enter-purchaser-email");
|
||||
$("#emailAddressView").removeClass("active");
|
||||
$("placeholder-refundEmail").html(srvModel.customerEmail);
|
||||
|
||||
// Remove Email mode
|
||||
$(".modal-dialog").removeClass("enter-purchaser-email");
|
||||
$("#scan").addClass("active");
|
||||
}
|
||||
// Email Form
|
||||
// Setup Email mode
|
||||
function emailForm() {
|
||||
|
||||
$(".modal-dialog").addClass("enter-purchaser-email");
|
||||
|
||||
$("#emailAddressForm .action-button").click(function () {
|
||||
var emailAddress = $("#emailAddressFormInput").val();
|
||||
if (validateEmail(emailAddress)) {
|
||||
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
|
||||
// Push the email to a server, once the reception is confirmed move on
|
||||
srvModel.customerEmail = emailAddress;
|
||||
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "POST",
|
||||
data: JSON.stringify({ Email: srvModel.customerEmail }),
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function () {
|
||||
hideEmailForm();
|
||||
})
|
||||
.fail(function (jqXHR, textStatus, errorThrown) {
|
||||
|
||||
})
|
||||
.always(function () {
|
||||
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
|
||||
});
|
||||
} else {
|
||||
|
||||
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* =============== Even listeners =============== */
|
||||
|
||||
// Email
|
||||
$("#emailAddressFormInput").change(function () {
|
||||
if ($("#emailAddressForm").hasClass("ng-submitted")) {
|
||||
$("#emailAddressForm").removeClass("ng-submitted");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Scan/Copy Transitions
|
||||
// Scan Tab
|
||||
$("#scan-tab").click(function () {
|
||||
if (!$(this).is(".active")) {
|
||||
$(this).addClass("active");
|
||||
}
|
||||
|
||||
if ($("#copy-tab").is(".active")) {
|
||||
$("#copy-tab").removeClass("active");
|
||||
}
|
||||
|
||||
$(".payment-tabs__slider").removeClass("slide-right");
|
||||
|
||||
if (!$("#scan").is(".active")) {
|
||||
$("#copy").hide();
|
||||
$("#copy").removeClass("active");
|
||||
|
||||
$("#scan").show();
|
||||
$("#scan").addClass("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Main Copy tab
|
||||
$("#copy-tab").click(function () {
|
||||
if (!$(this).is(".active")) {
|
||||
$(this).addClass("active");
|
||||
}
|
||||
|
||||
if ($("#scan-tab").is(".active")) {
|
||||
$("#scan-tab").removeClass("active");
|
||||
}
|
||||
if (!$(".payment-tabs__slider").is("slide-right")) {
|
||||
$(".payment-tabs__slider").addClass("slide-right");
|
||||
}
|
||||
|
||||
if (!$("#copy").is(".active")) {
|
||||
$("#copy").show();
|
||||
$("#copy").addClass("active");
|
||||
|
||||
$("#scan").hide();
|
||||
$("#scan").removeClass("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Payment received
|
||||
// Should connect using webhook ?
|
||||
// If notification received
|
||||
|
||||
onDataCallback(srvModel);
|
||||
|
||||
// public methods
|
||||
function onDataCallback(jsonData) {
|
||||
var newStatus = jsonData.status;
|
||||
|
||||
@ -187,21 +35,44 @@ function onDataCallback(jsonData) {
|
||||
$("#emailAddressView").removeClass("active");
|
||||
$(".modal-dialog").addClass("expired");
|
||||
$("#expired").addClass("active");
|
||||
|
||||
if ($("#scan").hasClass("active")) {
|
||||
$("#scan").removeClass("active");
|
||||
} else if ($("#copy").hasClass("active")) {
|
||||
$("#copy").removeClass("active");
|
||||
}
|
||||
}
|
||||
|
||||
if (checkoutCtrl.srvModel.status !== newStatus) {
|
||||
window.parent.postMessage({ "invoiceId": srvModel.invoiceId, "status": newStatus }, "*");
|
||||
}
|
||||
|
||||
// restoring qr code view only when currency is switched
|
||||
if (jsonData.paymentMethodId == srvModel.paymentMethodId) {
|
||||
$(".payment__currencies").show();
|
||||
$(".payment__spinner").hide();
|
||||
}
|
||||
|
||||
// updating ui
|
||||
checkoutCtrl.srvModel = jsonData;
|
||||
}
|
||||
|
||||
function changeCurrency(currency) {
|
||||
if (srvModel.paymentMethodId != currency) {
|
||||
$(".payment__currencies").hide();
|
||||
$(".payment__spinner").show();
|
||||
srvModel.paymentMethodId = currency;
|
||||
fetchStatus();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function fetchStatus() {
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.paymentMethodId + "/status";
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "GET"
|
||||
type: "GET",
|
||||
cache: false
|
||||
}).done(function (data) {
|
||||
onDataCallback(data);
|
||||
}).fail(function (jqXHR, textStatus, errorThrown) {
|
||||
@ -209,123 +80,261 @@ function fetchStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
||||
if (supportsWebSockets) {
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
|
||||
path = path.replace("https://", "wss://");
|
||||
path = path.replace("http://", "ws://");
|
||||
try {
|
||||
var socket = new WebSocket(path);
|
||||
socket.onmessage = function (e) {
|
||||
fetchStatus();
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Error while connecting to websocket for invoice notifications");
|
||||
}
|
||||
}
|
||||
// private methods
|
||||
$(document).ready(function () {
|
||||
// initialize
|
||||
onDataCallback(srvModel);
|
||||
|
||||
var watcher = setInterval(function () {
|
||||
fetchStatus();
|
||||
}, 2000);
|
||||
/* TAF
|
||||
|
||||
- Version mobile
|
||||
|
||||
- Réparer le décallage par timer
|
||||
|
||||
- Preparer les variables de l'API
|
||||
|
||||
- Gestion des differents evenements en fonction du status de l'invoice
|
||||
|
||||
- sécuriser les CDN
|
||||
|
||||
*/
|
||||
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
// check if the Document expired
|
||||
if (srvModel.expirationSeconds > 0) {
|
||||
progressStart(srvModel.maxTimeSeconds); // Progress bar
|
||||
startTimer(srvModel.expirationSeconds, display); // Timer
|
||||
|
||||
if (!validateEmail(srvModel.customerEmail))
|
||||
emailForm(); // Email form Display
|
||||
else
|
||||
hideEmailForm();
|
||||
}
|
||||
|
||||
|
||||
function hideEmailForm() {
|
||||
$("[role=document]").removeClass("enter-purchaser-email");
|
||||
$("#emailAddressView").removeClass("active");
|
||||
$("placeholder-refundEmail").html(srvModel.customerEmail);
|
||||
|
||||
// Remove Email mode
|
||||
$(".modal-dialog").removeClass("enter-purchaser-email");
|
||||
$("#scan").addClass("active");
|
||||
}
|
||||
// Email Form
|
||||
// Setup Email mode
|
||||
function emailForm() {
|
||||
$(".modal-dialog").addClass("enter-purchaser-email");
|
||||
|
||||
$("#emailAddressForm .action-button").click(function () {
|
||||
var emailAddress = $("#emailAddressFormInput").val();
|
||||
if (validateEmail(emailAddress)) {
|
||||
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
|
||||
// Push the email to a server, once the reception is confirmed move on
|
||||
srvModel.customerEmail = emailAddress;
|
||||
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: "POST",
|
||||
data: JSON.stringify({ Email: srvModel.customerEmail }),
|
||||
contentType: "application/json; charset=utf-8"
|
||||
}).done(function () {
|
||||
hideEmailForm();
|
||||
})
|
||||
.fail(function (jqXHR, textStatus, errorThrown) {
|
||||
|
||||
})
|
||||
.always(function () {
|
||||
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
|
||||
});
|
||||
} else {
|
||||
|
||||
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* =============== Even listeners =============== */
|
||||
|
||||
// Email
|
||||
$("#emailAddressFormInput").change(function () {
|
||||
if ($("#emailAddressForm").hasClass("ng-submitted")) {
|
||||
$("#emailAddressForm").removeClass("ng-submitted");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Scan/Copy Transitions
|
||||
// Scan Tab
|
||||
$("#scan-tab").click(function () {
|
||||
if (!$(this).is(".active")) {
|
||||
$(this).addClass("active");
|
||||
}
|
||||
|
||||
if ($("#copy-tab").is(".active")) {
|
||||
$("#copy-tab").removeClass("active");
|
||||
}
|
||||
|
||||
$(".payment-tabs__slider").removeClass("slide-right");
|
||||
|
||||
if (!$("#scan").is(".active")) {
|
||||
$("#copy").hide();
|
||||
$("#copy").removeClass("active");
|
||||
|
||||
$("#scan").show();
|
||||
$("#scan").addClass("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Main Copy tab
|
||||
$("#copy-tab").click(function () {
|
||||
if (!$(this).is(".active")) {
|
||||
$(this).addClass("active");
|
||||
}
|
||||
|
||||
if ($("#scan-tab").is(".active")) {
|
||||
$("#scan-tab").removeClass("active");
|
||||
}
|
||||
if (!$(".payment-tabs__slider").is("slide-right")) {
|
||||
$(".payment-tabs__slider").addClass("slide-right");
|
||||
}
|
||||
|
||||
if (!$("#copy").is(".active")) {
|
||||
$("#copy").show();
|
||||
$("#copy").addClass("active");
|
||||
|
||||
$("#scan").hide();
|
||||
$("#scan").removeClass("active");
|
||||
}
|
||||
});
|
||||
|
||||
// Payment received
|
||||
// Should connect using webhook ?
|
||||
// If notification received
|
||||
|
||||
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
||||
if (supportsWebSockets) {
|
||||
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
|
||||
path = path.replace("https://", "wss://");
|
||||
path = path.replace("http://", "ws://");
|
||||
try {
|
||||
var socket = new WebSocket(path);
|
||||
socket.onmessage = function (e) {
|
||||
fetchStatus();
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Error while connecting to websocket for invoice notifications");
|
||||
}
|
||||
}
|
||||
|
||||
var watcher = setInterval(function () {
|
||||
fetchStatus();
|
||||
}, 2000);
|
||||
|
||||
$(".menu__item").click(function () {
|
||||
$(".menu__scroll .menu__item").removeClass("selected");
|
||||
$(this).addClass("selected");
|
||||
language();
|
||||
$(".selector span").text($(".selected").text());
|
||||
// function to load contents in different language should go there
|
||||
});
|
||||
|
||||
// Validate Email address
|
||||
function validateEmail(email) {
|
||||
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
// Expand Line-Items
|
||||
$(".buyerTotalLine").click(function () {
|
||||
$("line-items").toggleClass("expanded");
|
||||
$(".buyerTotalLine").toggleClass("expanded");
|
||||
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
|
||||
});
|
||||
|
||||
// Timer Countdown
|
||||
function startTimer(duration, display) {
|
||||
var timer = duration, minutes, seconds;
|
||||
var timeout = setInterval(function () {
|
||||
minutes = parseInt(timer / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
display.text(minutes + ":" + seconds);
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
function progressStart(timerMax) {
|
||||
var end = new Date(); // Setup Time Variable, should come from server
|
||||
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
|
||||
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
|
||||
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
|
||||
animateUpdate(); //Launch it
|
||||
|
||||
function updateProgress(percentage) {
|
||||
$('.timer-row__progress-bar').css("width", percentage + "%");
|
||||
}
|
||||
|
||||
function animateUpdate() {
|
||||
|
||||
var now = new Date();
|
||||
var timeDiff = end.getTime() - now.getTime();
|
||||
var perc = 100 - Math.round(timeDiff / timerMax * 100);
|
||||
|
||||
if (perc === 75 && (status === "paidPartial" || status === "new")) {
|
||||
$(".timer-row").addClass("expiring-soon");
|
||||
$(".timer-row__message span").html("Invoice expiring soon ...");
|
||||
updateProgress(perc);
|
||||
}
|
||||
if (perc <= 100) {
|
||||
updateProgress(perc);
|
||||
setTimeout(animateUpdate, timeoutVal);
|
||||
}
|
||||
if (perc >= 100 && status === "expired") {
|
||||
onDataCallback(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Copy
|
||||
// Amount
|
||||
var copyAmount = new Clipboard('.manual-box__amount__value', {
|
||||
target: function () {
|
||||
var $el = $(".manual-box__amount__value");
|
||||
$el.removeClass("copy-cursor").addClass("copied");
|
||||
setTimeout(function () { $el.removeClass("copied").addClass("copy-cursor"); }, 500);
|
||||
return document.querySelector('.manual-box__amount__value span');
|
||||
}
|
||||
});
|
||||
// Address
|
||||
var copyAddress = new Clipboard('.manual-box__address__value', {
|
||||
target: function () {
|
||||
var $elm = $(".manual-box__address__value");
|
||||
$elm.removeClass("copy-cursor").addClass("copied");
|
||||
setTimeout(function () { $elm.removeClass("copied").addClass("copy-cursor"); }, 500);
|
||||
return document.querySelector('.manual-box__address__value .manual-box__address__wrapper .manual-box__address__wrapper__value');
|
||||
}
|
||||
});
|
||||
|
||||
// Disable enter key
|
||||
$(document).keypress(
|
||||
function (event) {
|
||||
if (event.which === '13') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$(".menu__item").click(function () {
|
||||
$(".menu__scroll .menu__item").removeClass("selected");
|
||||
$(this).addClass("selected");
|
||||
language();
|
||||
$(".selector span").text($(".selected").text());
|
||||
// function to load contents in different language should go there
|
||||
});
|
||||
|
||||
// Validate Email address
|
||||
function validateEmail(email) {
|
||||
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
// Expand Line-Items
|
||||
$(".buyerTotalLine").click(function () {
|
||||
$("line-items").toggleClass("expanded");
|
||||
$(".buyerTotalLine").toggleClass("expanded");
|
||||
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
|
||||
});
|
||||
|
||||
// Timer Countdown
|
||||
function startTimer(duration, display) {
|
||||
var timer = duration, minutes, seconds;
|
||||
var timeout = setInterval(function () {
|
||||
minutes = parseInt(timer / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
display.text(minutes + ":" + seconds);
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
function progressStart(timerMax) {
|
||||
var end = new Date(); // Setup Time Variable, should come from server
|
||||
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
|
||||
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
|
||||
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
|
||||
animateUpdate(); //Launch it
|
||||
|
||||
function updateProgress(percentage) {
|
||||
$('.timer-row__progress-bar').css("width", percentage + "%");
|
||||
}
|
||||
|
||||
function animateUpdate() {
|
||||
|
||||
var now = new Date();
|
||||
var timeDiff = end.getTime() - now.getTime();
|
||||
var perc = 100 - Math.round(timeDiff / timerMax * 100);
|
||||
|
||||
if (perc === 75 && (status === "paidPartial" || status === "new")) {
|
||||
$(".timer-row").addClass("expiring-soon");
|
||||
$(".timer-row__message span").html("Invoice expiring soon ...");
|
||||
updateProgress(perc);
|
||||
}
|
||||
if (perc <= 100) {
|
||||
updateProgress(perc);
|
||||
setTimeout(animateUpdate, timeoutVal);
|
||||
}
|
||||
if (perc >= 100 && status === "expired") {
|
||||
onDataCallback(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manual Copy
|
||||
// Amount
|
||||
var copyAmount = new Clipboard('.manual-box__amount__value', {
|
||||
target: function () {
|
||||
var $el = $(".manual-box__amount__value");
|
||||
$el.removeClass("copy-cursor").addClass("copied");
|
||||
setTimeout(function () { $el.removeClass("copied").addClass("copy-cursor"); }, 500);
|
||||
return document.querySelector('.manual-box__amount__value span');
|
||||
}
|
||||
});
|
||||
// Address
|
||||
var copyAddress = new Clipboard('.manual-box__address__value', {
|
||||
target: function () {
|
||||
var $elm = $(".manual-box__address__value");
|
||||
$elm.removeClass("copy-cursor").addClass("copied");
|
||||
setTimeout(function () { $elm.removeClass("copied").addClass("copy-cursor"); }, 500);
|
||||
return document.querySelector('.manual-box__address__value .manual-box__address__wrapper .manual-box__address__wrapper__value');
|
||||
}
|
||||
});
|
||||
|
||||
// Disable enter key
|
||||
$(document).keypress(
|
||||
function (event) {
|
||||
if (event.which === '13') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
36
README.md
36
README.md
@ -23,3 +23,39 @@ This solution is for you if:
|
||||
## Documentation
|
||||
|
||||
Please check out our [complete documentation](https://github.com/btcpayserver/btcpayserver-doc) for more details.
|
||||
|
||||
You can also checkout [The Merchants Guide to accepting Bitcoin directly with no intermediates through BTCPay](https://www.reddit.com/r/Bitcoin/comments/81h1oy/the_merchants_guide_to_accepting_bitcoin_directly/).
|
||||
|
||||
## How to build
|
||||
|
||||
While the documentation advise using docker-compose, you may want to build yourself outside of development purpose.
|
||||
|
||||
First install .NET Core SDK as specified by [Microsoft website](https://www.microsoft.com/net/download).
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
.\build.ps1
|
||||
```
|
||||
|
||||
On linux:
|
||||
```
|
||||
./build.sh
|
||||
```
|
||||
|
||||
## How to run
|
||||
|
||||
Use the `run` scripts to run BTCPayServer, this example show how to print the available command line arguments of BTCPayServer.
|
||||
|
||||
On Powershell:
|
||||
```
|
||||
.\run.ps1 --help
|
||||
```
|
||||
|
||||
On linux:
|
||||
```
|
||||
./run.sh --help
|
||||
```
|
||||
|
||||
## Other dependencies
|
||||
|
||||
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Deployment.md).
|
||||
|
1
build.ps1
Executable file
1
build.ps1
Executable file
@ -0,0 +1 @@
|
||||
dotnet build -c Release .\BTCPayServer\BTCPayServer.csproj
|
3
build.sh
Executable file
3
build.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
dotnet build -c Release BTCPayServer/BTCPayServer.csproj
|
1
run.ps1
Executable file
1
run.ps1
Executable file
@ -0,0 +1 @@
|
||||
dotnet run --no-launch-profile --no-build -c Release -p .\BTCPayServer\BTCPayServer.csproj -- $args
|
Reference in New Issue
Block a user