Compare commits
178 Commits
Author | SHA1 | Date | |
---|---|---|---|
0c8f37ca19 | |||
5dea0312ac | |||
f074007f67 | |||
88818ece29 | |||
fa0fa28949 | |||
08e31f6fe8 | |||
b976adeefe | |||
53c53b98e6 | |||
a171e00280 | |||
46f94d7175 | |||
2e555cac22 | |||
0d91b3286a | |||
396432b873 | |||
15c58434e8 | |||
daad1bdd25 | |||
c60966c725 | |||
fb57d8c3ce | |||
799ce74f65 | |||
8e38d7ceb4 | |||
a1c22e8071 | |||
6d8acf54d6 | |||
a500a89138 | |||
c6d44e7a89 | |||
9eb3aad072 | |||
9355454953 | |||
6467f06c54 | |||
b9b4b5ea39 | |||
e23243565f | |||
d3420532ae | |||
ade1b9d4eb | |||
fc278d12fc | |||
8e5ec822dc | |||
26aac66a76 | |||
a562e90bdb | |||
a0f3698701 | |||
02163f9482 | |||
b74fe171e2 | |||
2785bb4d9b | |||
5eac84d3a3 | |||
a0a2ab6fcd | |||
7730ead8e4 | |||
8eee0dd14c | |||
7dd88d8d8f | |||
56d1d3e645 | |||
c2308675b2 | |||
cb866a1c05 | |||
95290e8331 | |||
f5e62c775b | |||
f533309b49 | |||
d1c70a7cb3 | |||
2f8590ca7a | |||
08badbde56 | |||
8e38da80e0 | |||
cd2e3350b0 | |||
a0d2790491 | |||
8ca99e5635 | |||
5a2563ca7f | |||
a23cd28531 | |||
58a967b59e | |||
9bf0c20198 | |||
6b7ac0e000 | |||
188c0a9a86 | |||
c49479c8ad | |||
2072b6e136 | |||
08d82390b0 | |||
b845a545e2 | |||
db958b2401 | |||
7266420eec | |||
f36fbe7a76 | |||
8e279b110c | |||
d626870e46 | |||
df49b094d5 | |||
7d17bf7f2a | |||
e51f3dd1ae | |||
b810b88c6c | |||
39b34ff4ed | |||
f72fd63113 | |||
97eedc2c9f | |||
db222c53e3 | |||
61e919b88d | |||
d14040c142 | |||
13a3a581d8 | |||
f6dbae1cef | |||
ccbcda86ac | |||
b74e8cf756 | |||
8f8266f15d | |||
ab8d3f5813 | |||
08220dbea5 | |||
3b2cf2f1de | |||
c3beca27be | |||
28b820241f | |||
e985224092 | |||
f1c467aa7d | |||
ae7cfe90ab | |||
718a36ddd0 | |||
c0b903d79c | |||
48eaf906b0 | |||
f42fde970a | |||
59afebaa57 | |||
3e06e45054 | |||
fe55acb268 | |||
710dbb51f4 | |||
d426d66819 | |||
265cddc38b | |||
21b91ac8f7 | |||
e656813844 | |||
e8730f74be | |||
6d611d7d05 | |||
392f3a16f1 | |||
2b2e12b290 | |||
73cc75fe66 | |||
cc186fc8b3 | |||
632ad81b94 | |||
1d243910ae | |||
e624649cd8 | |||
e6ca07e9b5 | |||
b3d6435772 | |||
806474c8c6 | |||
1524fb4499 | |||
14b70ff35e | |||
c36a900627 | |||
d3befb5b86 | |||
7f0ce1f802 | |||
8342ad9175 | |||
acb2407654 | |||
b8a4f0c012 | |||
57bb3b231c | |||
e5d626e0fd | |||
e2c4c913ff | |||
09f97915d6 | |||
81328b2667 | |||
0d8affc68d | |||
da77d278fb | |||
5e9f6f3542 | |||
f337470f09 | |||
636224d0c8 | |||
b28b3ef4ff | |||
9e2e102ec4 | |||
9e16b83202 | |||
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 |
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,5 +1,7 @@
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
@ -84,7 +86,7 @@ namespace BTCPayServer.Tests
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"internallightningnode={IntegratedLightning.AbsoluteUri}");
|
||||
config.AppendLine($"btc.lightning={IntegratedLightning.AbsoluteUri}");
|
||||
|
||||
if (Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
@ -118,6 +120,7 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
((LightningLikePaymentHandler)_Host.Services.GetService(typeof(IPaymentMethodHandler<LightningSupportedPaymentMethod>))).SkipP2PTest = !InContainer;
|
||||
}
|
||||
|
||||
public string HostName
|
||||
@ -127,6 +130,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
23
BTCPayServer.Tests/LightningDTester.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class LightningDTester
|
||||
{
|
||||
ServerTester parent;
|
||||
public LightningDTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost, Network network)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new CLightningRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), network);
|
||||
}
|
||||
|
||||
public CLightningRPCClient RPC { get; }
|
||||
public string P2PHost { get; }
|
||||
|
||||
}
|
||||
}
|
@ -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,9 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -56,8 +57,10 @@ namespace BTCPayServer.Tests
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
|
||||
CustomerLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "tcp://127.0.0.1:30992/")), btc);
|
||||
MerchantLightningD = new CLightningRPCClient(new Uri(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "tcp://127.0.0.1:30993/")), btc);
|
||||
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "merchant_lightningd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
@ -68,60 +71,71 @@ namespace BTCPayServer.Tests
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.InContainer = bool.Parse(GetEnvironment("TESTS_INCONTAINER", "false"));
|
||||
PayTester.Start();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This will setup a channel going from customer to merchant
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PrepareLightningAsync()
|
||||
{
|
||||
// Activate segwit
|
||||
var blockCount = ExplorerNode.GetBlockCountAsync();
|
||||
// Fetch node info, but that in cache
|
||||
var merchantInfo = MerchantCharge.Client.GetInfoAsync();
|
||||
var customer = CustomerEclair.GetNodeInfoAsync();
|
||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||
|
||||
var info = await merchantInfo;
|
||||
var clightning = new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port);
|
||||
var connect = CustomerEclair.RPC.ConnectAsync(clightning);
|
||||
await Task.WhenAll(blockCount, customer, channels, connect);
|
||||
|
||||
// If the channel is not created, let's do it
|
||||
if (channels.Result.Length == 0)
|
||||
while (true)
|
||||
{
|
||||
var c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
bool generated = false;
|
||||
bool createdChannel = false;
|
||||
CancellationTokenSource timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
while (c.Length == 0 || c[0].State != "NORMAL")
|
||||
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !skippedStates.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
switch (channel?.State)
|
||||
{
|
||||
if (timeout.IsCancellationRequested)
|
||||
{
|
||||
timeout = new CancellationTokenSource();
|
||||
timeout.CancelAfter(10000);
|
||||
createdChannel = c.Length == 0;
|
||||
generated = false;
|
||||
}
|
||||
if (!createdChannel)
|
||||
{
|
||||
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
|
||||
createdChannel = true;
|
||||
}
|
||||
if (!generated && c.Length != 0 && c[0].State == "WAIT_FOR_FUNDING_CONFIRMED")
|
||||
{
|
||||
ExplorerNode.Generate(6);
|
||||
generated = true;
|
||||
}
|
||||
c = (await CustomerEclair.RPC.ChannelsAsync());
|
||||
case null:
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var clightning = new NodeInfo(merchantInfo.Id, MerchantCharge.P2PHost, merchantInfo.Port);
|
||||
await CustomerLightningD.ConnectAsync(clightning);
|
||||
var address = await CustomerLightningD.NewAddressAsync();
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await Task.Delay(1000);
|
||||
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
default:
|
||||
throw new NotSupportedException(channel?.State ?? "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetInfoResponse> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -135,11 +149,11 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||
await CustomerEclair.RPC.SendAsync(bolt11);
|
||||
await CustomerLightningD.SendAsync(bolt11);
|
||||
}
|
||||
|
||||
public EclairTester MerchantEclair { get; set; }
|
||||
public EclairTester CustomerEclair { get; set; }
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -11,6 +12,8 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -53,10 +56,12 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task<StoresController> CreateStoreAsync()
|
||||
{
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var store = parent.PayTester.GetController<UserStoresController>(UserId);
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
return store;
|
||||
var store2 = parent.PayTester.GetController<StoresController>(UserId);
|
||||
store2.CreatedStoreId = store.CreatedStoreId;
|
||||
return store2;
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
@ -77,11 +82,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
});
|
||||
}, cryptoCode);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
@ -111,20 +114,23 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode)
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode)
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save");
|
||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
|
||||
: throw new NotSupportedException(connectionType.ToString())
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Payments.Lightning.Eclair;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using System.Threading.Tasks;
|
||||
@ -45,7 +44,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue2()
|
||||
{
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest);
|
||||
var dummy = new Key().PubKey.GetAddress(Network.RegTest).ToString();
|
||||
#pragma warning disable CS0618
|
||||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
@ -304,24 +303,95 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, "BTC").GetAwaiter().GetResult());
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "test").GetAwaiter().GetResult();
|
||||
}, "test", "BTC").GetAwaiter().GetResult();
|
||||
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(storeController.ModelState.IsValid);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save").GetAwaiter().GetResult());
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
|
||||
Assert.Single(storeVm.LightningNodes);
|
||||
Assert.Single(storeVm.LightningNodes.Where(l => !string.IsNullOrEmpty(l.Address)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseLightningURL()
|
||||
{
|
||||
LightningConnectionString conn = null;
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("unix://test/a", out conn));
|
||||
Assert.Equal("unix://test/a", conn.ToString());
|
||||
Assert.Equal("unix://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("unix://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("tcp://test/a", out conn));
|
||||
Assert.Equal("tcp://test/a", conn.ToString());
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("tcp://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.CLightning, conn.ConnectionType);
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse("http://aaa:bbb@test/a", out conn));
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToString());
|
||||
Assert.Equal("http://aaa:bbb@test/a", conn.ToUri(true).AbsoluteUri);
|
||||
Assert.Equal("http://test/a", conn.ToUri(false).AbsoluteUri);
|
||||
Assert.Equal(LightningConnectionType.Charge, conn.ConnectionType);
|
||||
Assert.Equal("aaa", conn.Username);
|
||||
Assert.Equal("bbb", conn.Password);
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", out conn));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPayment2()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 0.01,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,7 +405,7 @@ namespace BTCPayServer.Tests
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC");
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
@ -347,7 +417,6 @@ namespace BTCPayServer.Tests
|
||||
ItemDesc = "Some description"
|
||||
});
|
||||
|
||||
|
||||
tester.SendLightningPayment(invoice);
|
||||
|
||||
Eventually(() =>
|
||||
@ -737,6 +806,66 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.50 USD", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.50 usd", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1 usd", out result));
|
||||
Assert.Equal("1 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1usd", out result));
|
||||
Assert.Equal("1 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1.501", out result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
|
||||
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
|
||||
// Passing electrum stuff
|
||||
// Native
|
||||
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
|
||||
// P2SH
|
||||
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
|
||||
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
|
||||
////////////////
|
||||
|
||||
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[legacy]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse($"{tpub}-[legacy]");
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
|
@ -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,14 +17,19 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
TEST_ECLAIR: http://eclair-cli:gpwefwmmewci@eclair:8080/
|
||||
TEST_CHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
TEST_MERCHANTLIGHTNINGD: "/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: http://api-token:foiewnccewuify@lightning-charged:9112/
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
- dev
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
volumes:
|
||||
- "customer_lightningd_datadir:/etc/customer_lightningd_datadir"
|
||||
- "merchant_lightningd_datadir:/etc/merchant_lightningd_datadir"
|
||||
|
||||
# The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services
|
||||
dev:
|
||||
@ -36,11 +41,12 @@ services:
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- eclair
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.16
|
||||
image: nicolasdorier/nbxplorer:1.0.1.24
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -64,7 +70,6 @@ services:
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
@ -75,11 +80,6 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:29000
|
||||
zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
txindex=1
|
||||
# Eclair is still using addwitnessaddress
|
||||
deprecatedrpc=addwitnessaddress
|
||||
ports:
|
||||
- "43782:43782"
|
||||
expose:
|
||||
@ -88,8 +88,29 @@ services:
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
ipaddr=customer_lightningd
|
||||
log-level=debug
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "customer_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.1
|
||||
image: shesek/lightning-charge:0.3.5
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -97,6 +118,8 @@ services:
|
||||
BITCOIND_RPCCONNECT: bitcoind
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "lightning_charge_datadir:/data"
|
||||
- "merchant_lightningd_datadir:/etc/lightning"
|
||||
expose:
|
||||
- "9112" # Charge
|
||||
- "9735" # Lightning
|
||||
@ -104,33 +127,31 @@ services:
|
||||
- "54938:9112" # Charge
|
||||
links:
|
||||
- bitcoind
|
||||
- merchant_lightningd
|
||||
|
||||
eclair:
|
||||
image: acinq/eclair@sha256:758eaf02683046a096ee03390d3a54df8fcfca50883f7560ab946a36ee4e81d8
|
||||
environment:
|
||||
JAVA_OPTS: >
|
||||
-Xmx512m
|
||||
-Declair.printToConsole
|
||||
-Declair.bitcoind.host=bitcoind
|
||||
-Declair.bitcoind.rpcport=43782
|
||||
-Declair.bitcoind.rpcuser=ceiwHEbqWI83
|
||||
-Declair.bitcoind.rpcpassword=DwubwWsoo3
|
||||
-Declair.bitcoind.zmq=tcp://bitcoind:29000
|
||||
-Declair.api.enabled=true
|
||||
-Declair.api.password=gpwefwmmewci
|
||||
-Declair.chain=regtest
|
||||
-Declair.api.binding-ip=0.0.0.0
|
||||
links:
|
||||
- bitcoind
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
ipaddr=merchant_lightningd
|
||||
network=regtest
|
||||
log-level=debug
|
||||
ports:
|
||||
- "30992:8080" # api port
|
||||
- "30993:9835" # api port
|
||||
expose:
|
||||
- "9735" # server port
|
||||
- "8080" # api port
|
||||
- "9835" # api port
|
||||
volumes:
|
||||
- "bitcoin_datadir:/etc/bitcoin"
|
||||
- "merchant_lightningd_datadir:/root/.lightning"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
image: nicolasdorier/docker-litecoin:0.15.1
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
@ -155,3 +176,6 @@ services:
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
|
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" "$@"
|
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
1
BTCPayServer.Tests/docker-merchant-lightning-cli.ps1
Executable file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
|
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
3
BTCPayServer.Tests/docker-merchant-lightning-cli.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
|
@ -68,7 +68,6 @@ namespace BTCPayServer
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string CLightningNetworkName { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -28,10 +28,7 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
|
||||
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
|
||||
ChainType == ChainType.Test ? "testnet" :
|
||||
ChainType == ChainType.Regtest ? "regtest" : null
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,7 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
|
||||
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
|
||||
ChainType == ChainType.Test ? "litecoin-testnet" : null
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,22 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.41</Version>
|
||||
<Version>1.0.1.71</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<Content Remove="Build\dockerfiles\**" />
|
||||
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<EmbeddedResource Remove="Build\dockerfiles\**" />
|
||||
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
<None Remove="Build\dockerfiles\**" />
|
||||
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
|
||||
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Currencies.txt" />
|
||||
@ -31,10 +35,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" />
|
||||
@ -56,7 +60,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\js\core.js" />
|
||||
<None Include="wwwroot\js\checkout\core.js" />
|
||||
<None Include="wwwroot\js\creative.js" />
|
||||
<None Include="wwwroot\js\creative.min.js" />
|
||||
<None Include="wwwroot\js\site.js" />
|
||||
|
@ -10,6 +10,7 @@ using System.Text;
|
||||
using StandardConfiguration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -73,6 +74,17 @@ namespace BTCPayServer.Configuration
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if(lightning.Length != 0)
|
||||
{
|
||||
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
|
||||
{
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, you need to pass either " +
|
||||
$"the absolute path to the unix socket of a running CLightning instance (eg. /root/.lightning/lightning-rpc), " +
|
||||
$"or the url to a charge server with crendetials (eg. https://apitoken@API_TOKEN_SECRET:charge.example.com/)");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
@ -80,10 +92,12 @@ namespace BTCPayServer.Configuration
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
InternalLightningNode = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
|
||||
if(old != null)
|
||||
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
|
||||
}
|
||||
|
||||
public Uri InternalLightningNode { get; set; }
|
||||
public Dictionary<string, LightningConnectionString> InternalLightningByCryptoCode { get; set; } = new Dictionary<string, LightningConnectionString>();
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public string PostgresConnectionString
|
||||
|
@ -36,9 +36,9 @@ namespace BTCPayServer.Configuration
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--internallightningnode", $"An internal lightning node which can be used without https requirement and easily configured by the admin (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
return app;
|
||||
}
|
||||
@ -105,6 +105,8 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=/root/.lightning/lightning-rpc");
|
||||
builder.AppendLine($"#{n.CryptoCode}.lightning=https://apitoken:API_TOKEN_SECRET@charge.example.com/");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -68,16 +69,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
|
||||
var paymentMethodId = data.GetId();
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentMethodId.CryptoCode}";
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if(onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
||||
if (onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
@ -86,25 +87,29 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var payments = invoice
|
||||
.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
|
||||
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
paymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(paymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
}
|
||||
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
@ -112,7 +117,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
@ -121,12 +126,32 @@ namespace BTCPayServer.Controllers
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
model.Addresses = invoice.HistoricalAddresses;
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string ToString(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var type = paymentMethodId.PaymentType.ToString();
|
||||
switch (paymentMethodId.PaymentType)
|
||||
{
|
||||
case PaymentTypes.BTCLike:
|
||||
type = "On-Chain";
|
||||
break;
|
||||
case PaymentTypes.LightningLike:
|
||||
type = "Off-Chain";
|
||||
break;
|
||||
}
|
||||
return $"{paymentMethodId.CryptoCode} ({type})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
@ -166,7 +191,7 @@ namespace BTCPayServer.Controllers
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if(!isDefaultCrypto)
|
||||
if (!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
@ -177,16 +202,20 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
@ -201,26 +230,28 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||
throw new NotSupportedException(),
|
||||
PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo,
|
||||
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
||||
throw new NotSupportedException(),
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetpaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
@ -348,6 +379,7 @@ namespace BTCPayServer.Controllers
|
||||
Date = Prettify(invoice.InvoiceTime),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
|
||||
});
|
||||
}
|
||||
@ -361,7 +393,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var ago = DateTime.UtcNow - invoiceTime;
|
||||
|
||||
if(ago.TotalMinutes < 1)
|
||||
if (ago.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
|
||||
}
|
||||
@ -391,7 +423,7 @@ namespace BTCPayServer.Controllers
|
||||
if (stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to create at least one store before creating a transaction";
|
||||
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
return View(new CreateInvoiceModel() { Stores = stores });
|
||||
}
|
||||
@ -408,9 +440,18 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
|
||||
StatusMessage = null;
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of this store to create an invoice";
|
||||
}
|
||||
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
|
||||
}
|
||||
|
||||
if(StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
|
@ -79,33 +79,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
|
||||
IsAvailable: Task.FromResult(false)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(c =>
|
||||
{
|
||||
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
|
||||
return c;
|
||||
})
|
||||
.ToList();
|
||||
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
|
||||
{
|
||||
if(!await supportedPaymentMethod.IsAvailable)
|
||||
{
|
||||
supportedPaymentMethods.Remove(supportedPaymentMethod);
|
||||
}
|
||||
}
|
||||
if (supportedPaymentMethods.Count == 0)
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
@ -133,33 +110,54 @@ namespace BTCPayServer.Controllers
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
var methods = supportedPaymentMethods
|
||||
.Select(async o =>
|
||||
{
|
||||
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
});
|
||||
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var method in methods)
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
paymentMethods.Add(await method);
|
||||
try
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
}
|
||||
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach(var error in paymentMethodErrors)
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
#pragma warning disable CS0618
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||
@ -177,15 +175,58 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob)
|
||||
{
|
||||
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
// Check if Lightning Max value is exceeded
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
var lightningMaxValue = storeBlob.LightningMaxValue;
|
||||
var lightningMaxValueRate = 0.0m;
|
||||
if (lightningMaxValue.Currency == entity.ProductInformation.Currency)
|
||||
lightningMaxValueRate = paymentMethod.Rate;
|
||||
else
|
||||
lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency);
|
||||
|
||||
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate);
|
||||
if (paymentMethod.Calculate().Due > lightningMaxValueCrypto)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("Lightning max value exceeded");
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -43,6 +43,50 @@ namespace BTCPayServer.Controllers
|
||||
return View(users);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
|
||||
private static bool IsAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var isAdmin = IsAdmin(roles);
|
||||
bool updated = false;
|
||||
|
||||
if(isAdmin != viewModel.IsAdmin)
|
||||
{
|
||||
if (viewModel.IsAdmin)
|
||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
else
|
||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||
updated = true;
|
||||
}
|
||||
if(updated)
|
||||
{
|
||||
viewModel.StatusMessage = "User successfully updated";
|
||||
}
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
|
@ -20,40 +20,53 @@ namespace BTCPayServer.Controllers
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/derivations")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string cryptoCode)
|
||||
{
|
||||
selectedScheme = selectedScheme ?? "BTC";
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
|
||||
{
|
||||
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
|
||||
}
|
||||
|
||||
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm)
|
||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||
{
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, vm.CryptoCurrency);
|
||||
var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
return NotFound();
|
||||
}
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
@ -62,10 +75,9 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -74,8 +86,38 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (!vm.Confirmation && strategy != null)
|
||||
return ShowAddresses(vm, strategy);
|
||||
|
||||
if (vm.Confirmation)
|
||||
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -93,23 +135,24 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
@ -130,6 +173,8 @@ namespace BTCPayServer.Controllers
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// getxpub
|
||||
int account = 0,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
@ -197,7 +242,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for substract fees"); }
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
@ -205,7 +250,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
result = await hw.GetExtPubKey(network);
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account);
|
||||
;
|
||||
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
|
||||
result = getxpubResult;
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
@ -224,17 +272,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 });
|
||||
|
@ -8,38 +8,66 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using System.Net;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/lightning")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)
|
||||
{
|
||||
selectedCrypto = selectedCrypto ?? "BTC";
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
|
||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private LightningConnectionString GetInternalLighningNode(string cryptoCode)
|
||||
{
|
||||
if (_BtcpayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var connectionString))
|
||||
{
|
||||
return CanUseInternalLightning() ? connectionString : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/lightning/{cryptoCode}")]
|
||||
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode)
|
||||
{
|
||||
vm.CryptoCode = cryptoCode;
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
|
||||
vm.InternalLightningNode = GetInternalLightningNodeIfAuthorized();
|
||||
if (network == null || network.CLightningNetworkName == null)
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -47,40 +75,39 @@ namespace BTCPayServer.Controllers
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.Url))
|
||||
{
|
||||
Uri uri;
|
||||
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
|
||||
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
|
||||
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (uri.Scheme != "https")
|
||||
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
|
||||
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
isLocal;
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
|
||||
{
|
||||
var internalNode = GetInternalLightningNodeIfAuthorized();
|
||||
if (internalNode == null || GetDomain(internalNode) != GetDomain(uri.AbsoluteUri))
|
||||
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanUseInternalLightning() && GetDomain(_BtcpayServerOptions.InternalLightningNode.AbsoluteUri) == GetDomain(uri.AbsoluteUri))
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||
{
|
||||
CryptoCode = paymentMethodId.CryptoCode
|
||||
};
|
||||
paymentMethod.SetLightningChargeUrl(uri);
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
if (command == "save")
|
||||
{
|
||||
@ -99,36 +126,21 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
await handler.Test(paymentMethod, network);
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
vm.StatusMessage = "Connection to the lightning node succeed";
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInternalLightningNodeIfAuthorized()
|
||||
{
|
||||
if (_BtcpayServerOptions.InternalLightningNode != null &&
|
||||
CanUseInternalLightning())
|
||||
{
|
||||
return _BtcpayServerOptions.InternalLightningNode.AbsoluteUri;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool CanUseInternalLightning()
|
||||
{
|
||||
return (_BTCPayEnv.IsDevelopping || User.IsInRole(Roles.ServerAdmin));
|
||||
}
|
||||
|
||||
string GetDomain(string uri)
|
||||
{
|
||||
return new UriBuilder(uri).Host;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
@ -26,11 +27,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[Authorize(Policy = "CanAccessStore")]
|
||||
[Authorize(Policy = StorePolicies.OwnStore)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class StoresController : Controller
|
||||
{
|
||||
public string CreatedStoreId { get; set; }
|
||||
public StoresController(
|
||||
NBXplorerDashboard dashboard,
|
||||
IServiceProvider serviceProvider,
|
||||
BTCPayServerOptions btcpayServerOptions,
|
||||
BTCPayServerEnvironment btcpayEnv,
|
||||
@ -43,11 +46,14 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
@ -59,6 +65,7 @@ namespace BTCPayServer.Controllers
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
}
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
IServiceProvider _ServiceProvider;
|
||||
@ -71,6 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository _Repo;
|
||||
TokenRepository _TokenRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
private LanguageService _LangService;
|
||||
IHostingEnvironment _Env;
|
||||
|
||||
[TempData]
|
||||
@ -80,41 +88,15 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public IActionResult CreateStore()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
public string CreatedStoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet")]
|
||||
public async Task<IActionResult> Wallet(string storeId)
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -124,76 +106,144 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
result.StatusMessage = StatusMessage;
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var balances = stores
|
||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
Balances = balances[i].Select(t => t.Result).ToArray()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
Email = u.Email,
|
||||
Id = u.Id,
|
||||
Role = u.Role
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(storeId, vm);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var user = await _UserManager.FindByEmailAsync(vm.Email);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||
return View(vm);
|
||||
}
|
||||
if (!StoreRoles.AllRoles.Contains(vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||
return View(vm);
|
||||
}
|
||||
StatusMessage = "User added successfully";
|
||||
return RedirectToAction(nameof(StoreUsers));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Title = $"Remove store user",
|
||||
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
|
||||
{
|
||||
await _Repo.RemoveStoreUser(storeId, userId);
|
||||
StatusMessage = "User removed successfully";
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
StatusMessage = "Store removed successfully";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue currencyValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value");
|
||||
}
|
||||
}
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
bool needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.LightningMaxValue = currencyValue;
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutExperience), new
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -208,12 +258,10 @@ namespace BTCPayServer.Controllers
|
||||
var vm = new StoreViewModel();
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.StatusMessage = StatusMessage;
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
@ -221,27 +269,36 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
{
|
||||
foreach(var strategy in store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>())
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.ToDictionary(c => c.Network.CryptoCode);
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
var strategy = derivationByCryptoCode.TryGet(network.CryptoCode);
|
||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||
{
|
||||
Crypto = strategy.PaymentId.CryptoCode,
|
||||
Value = strategy.DerivationStrategyBase.ToString()
|
||||
Crypto = network.CryptoCode,
|
||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
foreach(var lightning in store
|
||||
var lightningByCryptoCode = store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>()
|
||||
.ToDictionary(c => c.CryptoCode);
|
||||
|
||||
foreach (var network in _NetworkProvider.GetAll())
|
||||
{
|
||||
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
|
||||
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||
{
|
||||
CryptoCode = lightning.CryptoCode,
|
||||
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
|
||||
CryptoCode = network.CryptoCode,
|
||||
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -278,13 +335,6 @@ namespace BTCPayServer.Controllers
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
@ -325,41 +375,11 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
if (format == "Electrum")
|
||||
{
|
||||
//Unsupported Electrum
|
||||
//var p2wsh_p2sh = 0x295b43fU;
|
||||
//var p2wsh = 0x2aa7ed3U;
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var data = Encoders.Base58Check.DecodeData(derivationScheme);
|
||||
if (data.Length < 4)
|
||||
throw new FormatException("data.Length < 4");
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
|
||||
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
|
||||
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
data[i] = standardPrefix[i];
|
||||
|
||||
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
|
||||
foreach (var label in labels)
|
||||
{
|
||||
derivationScheme = derivationScheme + $"-[{label}]";
|
||||
}
|
||||
}
|
||||
|
||||
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -389,15 +409,17 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
model.Label = model.Label ?? String.Empty;
|
||||
if (storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
|
||||
storeId = model.StoreId ?? storeId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
{
|
||||
storeId = model.StoreId;
|
||||
var userId = GetUserId();
|
||||
if (userId == null)
|
||||
return Unauthorized();
|
||||
var store = await _Repo.FindStore(storeId, userId);
|
||||
if (store == null)
|
||||
return Unauthorized();
|
||||
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
var tokenRequest = new TokenRequest()
|
||||
@ -477,11 +499,13 @@ namespace BTCPayServer.Controllers
|
||||
[Route("/api-access-request")]
|
||||
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
return NotFound();
|
||||
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
|
||||
if (pairing == null)
|
||||
{
|
||||
StatusMessage = "Unknown pairing code";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -503,7 +527,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("api-access-request")]
|
||||
[Route("/api-access-request")]
|
||||
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
|
||||
{
|
||||
if (pairingCode == null)
|
||||
@ -513,10 +537,16 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
{
|
||||
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
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
|
||||
|
147
BTCPayServer/Controllers/UserStoresController.cs
Normal file
147
BTCPayServer/Controllers/UserStoresController.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("stores")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public partial class UserStoresController : Controller
|
||||
{
|
||||
private StoreRepository _Repo;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private BTCPayWalletProvider _WalletProvider;
|
||||
|
||||
public UserStoresController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_Repo = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
public IActionResult CreateStore()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
public string CreatedStoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _Repo.RemoveStore(storeId, userId);
|
||||
StatusMessage = "Store removed successfully";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
[TempData]
|
||||
public string StatusMessage { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
|
||||
var balances = stores
|
||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
result.Stores.Add(new StoresViewModel.StoreViewModel()
|
||||
{
|
||||
Id = store.Id,
|
||||
Name = store.StoreName,
|
||||
WebSite = store.StoreWebsite,
|
||||
IsOwner = store.Role == StoreRoles.Owner,
|
||||
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
||||
});
|
||||
}
|
||||
return View(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
}
|
||||
}
|
||||
}
|
44
BTCPayServer/CurrencyValue.cs
Normal file
44
BTCPayServer/CurrencyValue.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class CurrencyValue
|
||||
{
|
||||
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
|
||||
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
|
||||
public static bool TryParse(string str, out CurrencyValue value)
|
||||
{
|
||||
value = null;
|
||||
var match = _Regex.Match(str);
|
||||
if (!match.Success ||
|
||||
!decimal.TryParse(match.Groups[1].Value, out var v))
|
||||
return false;
|
||||
|
||||
var currency = match.Groups.Last().Value.ToUpperInvariant();
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
v = Math.Round(v, currencyData.Divisibility);
|
||||
value = new CurrencyValue()
|
||||
{
|
||||
Value = v,
|
||||
Currency = currency
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public decimal Value { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -118,7 +119,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
else if (!existing)
|
||||
else if (!existing && supportedPaymentMethod != null)
|
||||
strategies.Add(new JProperty(supportedPaymentMethod.PaymentId.ToString(), PaymentMethodExtensions.Serialize(supportedPaymentMethod)));
|
||||
DerivationStrategies = strategies.ToString();
|
||||
#pragma warning restore CS0618
|
||||
@ -213,6 +214,11 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
@ -249,6 +255,14 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomCSS { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
|
180
BTCPayServer/DerivationSchemeParser.cs
Normal file
180
BTCPayServer/DerivationSchemeParser.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public ChainType ChainType { get; set; }
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
ChainType = chainType;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
|
||||
HashSet<string> hintedLabels = new HashSet<string>();
|
||||
|
||||
var hintDestination = HintScriptPubKey?.GetDestination();
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is KeyId)
|
||||
{
|
||||
hintedLabels.Add("legacy");
|
||||
}
|
||||
if (hintDestination is ScriptId)
|
||||
{
|
||||
hintedLabels.Add("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = new DerivationStrategyFactory(Network).Parse(str);
|
||||
return FindMatch(hintedLabels, result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (IsLabel(parts[i]))
|
||||
{
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Encoders.Base58Check.DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is WitKeyId)
|
||||
{
|
||||
hintedLabels.Remove("legacy");
|
||||
hintedLabels.Remove("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
str = string.Join('-', parts.Where(p => !IsLabel(p)));
|
||||
foreach (var label in hintedLabels)
|
||||
{
|
||||
str = $"{str}-[{label}]";
|
||||
}
|
||||
|
||||
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
|
||||
}
|
||||
|
||||
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
|
||||
{
|
||||
var facto = new DerivationStrategyFactory(Network);
|
||||
var firstKeyPath = new KeyPath("0/0");
|
||||
if (HintScriptPubKey == null)
|
||||
return result;
|
||||
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
|
||||
return result;
|
||||
|
||||
if (result is MultisigDerivationStrategy)
|
||||
hintLabels.Add("keeporder");
|
||||
|
||||
var resultNoLabels = result.ToString();
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
throw new FormatException("Could not find any match");
|
||||
}
|
||||
|
||||
private static bool IsLabel(string v)
|
||||
{
|
||||
return v.StartsWith('[') && v.EndsWith(']');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
||||
if (minimumItems == 0) // Optimize default case
|
||||
listOfLists.Add(new List<T>());
|
||||
|
||||
for (int i = 1; i <= nonEmptyCombinations; i++)
|
||||
{
|
||||
List<T> thisCombination = new List<T>(inputList.Count);
|
||||
for (int j = 0; j < inputList.Count; j++)
|
||||
{
|
||||
if ((i >> j & 1) == 1)
|
||||
thisCombination.Add(inputList[j]);
|
||||
}
|
||||
|
||||
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
|
||||
listOfLists.Add(thisCombination);
|
||||
}
|
||||
|
||||
return listOfLists;
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -126,6 +126,7 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
return dbContext;
|
||||
});
|
||||
services.TryAddSingleton<Payments.Lightning.LightningClientFactory>();
|
||||
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
@ -133,6 +134,7 @@ namespace BTCPayServer.Hosting
|
||||
return opts.NetworkProvider;
|
||||
});
|
||||
|
||||
services.TryAddSingleton<LanguageService>();
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
@ -147,7 +149,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
@ -172,14 +174,14 @@ namespace BTCPayServer.Hosting
|
||||
|
||||
services.AddAuthorization(o =>
|
||||
{
|
||||
o.AddPolicy("CanAccessStore", builder =>
|
||||
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
|
||||
});
|
||||
|
||||
o.AddPolicy("OwnStore", builder =>
|
||||
o.AddPolicy(StorePolicies.OwnStore, builder =>
|
||||
{
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
|
||||
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
|
||||
});
|
||||
});
|
||||
|
||||
|
38
BTCPayServer/JsonConverters/CurrencyValueJsonConverter.cs
Normal file
38
BTCPayServer/JsonConverters/CurrencyValueJsonConverter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class CurrencyValueJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
CurrencyValue.TryParse((string)reader.Value, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if(value != null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
38
BTCPayServer/JsonConverters/UriJsonConverter.cs
Normal file
38
BTCPayServer/JsonConverters/UriJsonConverter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class UriJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(Uri).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
Uri.TryCreate((string)reader.Value, UriKind.Absolute, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(((Uri)value).AbsoluteUri);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -13,7 +13,11 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public bool IsLightning { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
@ -43,5 +47,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public int MaxTimeMinutes { get; internal set; }
|
||||
public string PaymentType { get; internal set; }
|
||||
public string PaymentMethodId { get; internal set; }
|
||||
|
||||
public bool AllowCoinConversion { get; set; }
|
||||
public string PeerInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
17
BTCPayServer/Models/ServerViewModels/UserViewModel.cs
Normal file
17
BTCPayServer/Models/ServerViewModels/UserViewModel.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class UserViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
[Display(Name = "Is admin")]
|
||||
public bool IsAdmin { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class CheckoutExperienceViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList Languages { get; set; }
|
||||
|
||||
[Display(Name = "Default crypto currency on checkout")]
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||
[Url]
|
||||
public Uri CustomCSS { get; set; }
|
||||
[Display(Name = "Link to a custom logo")]
|
||||
[Url]
|
||||
public Uri CustomLogo { get; set; }
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultCryptoCurrency = chosen.Name;
|
||||
}
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
defaultLang = defaultLang ?? "en-US";
|
||||
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
|
||||
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultLang = chosen.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,20 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class DerivationSchemeViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public DerivationSchemeViewModel()
|
||||
{
|
||||
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
|
||||
DerivationSchemeFormat = btcPay.Value;
|
||||
DerivationSchemeFormats = new SelectList(new Format[]
|
||||
{
|
||||
btcPay,
|
||||
new Format { Name = "Electrum", Value = "Electrum" },
|
||||
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
|
||||
}
|
||||
public string DerivationScheme
|
||||
{
|
||||
@ -34,33 +22,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
[Display(Name = "Derivation Scheme format")]
|
||||
public string DerivationSchemeFormat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
[Display(Name = "Hint address")]
|
||||
public string HintAddress { get; set; }
|
||||
public bool Confirmation { get; set; }
|
||||
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList DerivationSchemeFormats { get; set; }
|
||||
|
||||
public string ServerUrl { get; set; }
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
public string StatusMessage { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class LightningNodeViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
[Display(Name = "Lightning charge url")]
|
||||
public string Url
|
||||
{
|
||||
@ -21,24 +16,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
public string InternalLightningNode { get; internal set; }
|
||||
|
||||
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
|
||||
{
|
||||
var choices = networkProvider.GetAll()
|
||||
.Where(n => n.CLightningNetworkName != null)
|
||||
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal file
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreUsersViewModel
|
||||
{
|
||||
public class StoreUserViewModel
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string Role { get; set; }
|
||||
public string Id { get; set; }
|
||||
}
|
||||
public StoreUsersViewModel()
|
||||
{
|
||||
Role = StoreRoles.Guest;
|
||||
}
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string Role { get; set; }
|
||||
public List<StoreUserViewModel> Users { get; set; }
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
@ -16,11 +17,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string Crypto { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public StoreViewModel()
|
||||
{
|
||||
|
||||
@ -94,15 +91,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
|
||||
[Display(Name = "Default crypto currency on checkout")]
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
|
||||
public class LightningNode
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
@ -113,12 +101,5 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
} = new List<LightningNode>();
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == defaultCrypto) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultCryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoresViewModel
|
||||
{
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<StoreViewModel> Stores
|
||||
{
|
||||
get; set;
|
||||
} = new List<StoreViewModel>();
|
||||
|
||||
public class StoreViewModel
|
||||
{
|
||||
public string Name
|
||||
@ -32,6 +29,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool IsOwner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string[] Balances
|
||||
{
|
||||
get; set;
|
||||
|
@ -10,25 +10,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public class WalletModel
|
||||
{
|
||||
public string ServerUrl { get; set; }
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
@ -29,18 +29,15 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_ExplorerProvider.IsAvailable(network))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
|
||||
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
|
||||
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;
|
||||
}
|
||||
|
||||
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -11,14 +11,6 @@ namespace BTCPayServer.Payments
|
||||
/// </summary>
|
||||
public interface IPaymentMethodHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the dependencies for a specific payment method are satisfied.
|
||||
/// </summary>
|
||||
/// <param name="supportedPaymentMethod"></param>
|
||||
/// <param name="network"></param>
|
||||
/// <returns>true if this payment method is available</returns>
|
||||
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
/// <summary>
|
||||
/// Create needed to track payments of this invoice
|
||||
/// </summary>
|
||||
@ -31,7 +23,6 @@ namespace BTCPayServer.Payments
|
||||
|
||||
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
|
||||
{
|
||||
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
|
||||
}
|
||||
|
||||
@ -47,16 +38,5 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
throw new NotSupportedException("Invalid supportedPaymentMethod");
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if(supportedPaymentMethod is T method)
|
||||
{
|
||||
return IsAvailable(method, network);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class CLightningInvoice
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
[JsonProperty("payment_hash")]
|
||||
public uint256 PaymentHash { get; set; }
|
||||
|
||||
[JsonProperty("msatoshi")]
|
||||
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||
public LightMoney MilliSatoshi { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
[JsonProperty("expiry_time")]
|
||||
public DateTimeOffset ExpiryTime { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
[JsonProperty("expires_at")]
|
||||
public DateTimeOffset ExpiryAt { get; set; }
|
||||
[JsonProperty("bolt11")]
|
||||
public string BOLT11 { get; set; }
|
||||
[JsonProperty("pay_index")]
|
||||
public int? PayIndex { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Status { get; set; }
|
||||
[JsonProperty("paid_at")]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Mono.Unix;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class LightningRPCException : Exception
|
||||
{
|
||||
public LightningRPCException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class CLightningRPCClient : ILightningInvoiceClient, ILightningListenInvoiceSession
|
||||
{
|
||||
public Network Network { get; private set; }
|
||||
public Uri Address { get; private set; }
|
||||
|
||||
public CLightningRPCClient(Uri address, Network network)
|
||||
{
|
||||
if (address == null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if(address.Scheme == "file")
|
||||
{
|
||||
address = new UriBuilder(address) { Scheme = "unix" }.Uri;
|
||||
}
|
||||
Address = address;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
|
||||
}
|
||||
|
||||
public Task SendAsync(string bolt11)
|
||||
{
|
||||
return SendCommandAsync<object>("pay", new[] { bolt11 }, true);
|
||||
}
|
||||
|
||||
public async Task<PeerInfo[]> ListPeersAsync()
|
||||
{
|
||||
var peers = await SendCommandAsync<PeerInfo[]>("listpeers", isArray: true);
|
||||
foreach (var peer in peers)
|
||||
{
|
||||
peer.Channels = peer.Channels ?? Array.Empty<ChannelInfo>();
|
||||
}
|
||||
return peers;
|
||||
}
|
||||
|
||||
public Task FundChannelAsync(NodeInfo nodeInfo, Money money)
|
||||
{
|
||||
return SendCommandAsync<object>("fundchannel", new object[] { nodeInfo.NodeId, money.Satoshi }, true);
|
||||
}
|
||||
|
||||
public Task ConnectAsync(NodeInfo nodeInfo)
|
||||
{
|
||||
return SendCommandAsync<object>("connect", new[] { $"{nodeInfo.NodeId}@{nodeInfo.Host}:{nodeInfo.Port}" }, true);
|
||||
}
|
||||
|
||||
static Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<T> SendCommandAsync<T>(string command, object[] parameters = null, bool noReturn = false, bool isArray = false, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
parameters = parameters ?? Array.Empty<string>();
|
||||
using (Socket socket = await Connect())
|
||||
{
|
||||
using (var networkStream = new NetworkStream(socket))
|
||||
{
|
||||
using (var textWriter = new StreamWriter(networkStream, UTF8, 1024 * 10, true))
|
||||
{
|
||||
using (var jsonWriter = new JsonTextWriter(textWriter))
|
||||
{
|
||||
var req = new JObject();
|
||||
req.Add("id", 0);
|
||||
req.Add("method", command);
|
||||
req.Add("params", new JArray(parameters));
|
||||
await req.WriteToAsync(jsonWriter, cancellation);
|
||||
await jsonWriter.FlushAsync(cancellation);
|
||||
}
|
||||
await textWriter.FlushAsync();
|
||||
}
|
||||
await networkStream.FlushAsync(cancellation);
|
||||
using (var textReader = new StreamReader(networkStream, UTF8, false, 1024 * 10, true))
|
||||
{
|
||||
using (var jsonReader = new JsonTextReader(textReader))
|
||||
{
|
||||
var resultAsync = JObject.LoadAsync(jsonReader, cancellation);
|
||||
|
||||
// without this hack resultAsync is blocking even if cancellation happen
|
||||
using (cancellation.Register(() => { socket.Dispose(); }))
|
||||
{
|
||||
var result = await resultAsync;
|
||||
var error = result.Property("error");
|
||||
if (error != null)
|
||||
{
|
||||
throw new LightningRPCException(error.Value["message"].Value<string>());
|
||||
}
|
||||
if (noReturn)
|
||||
return default(T);
|
||||
if (isArray)
|
||||
{
|
||||
return result["result"].Children().First().Children().First().ToObject<T>();
|
||||
}
|
||||
return result["result"].ToObject<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Socket> Connect()
|
||||
{
|
||||
Socket socket = null;
|
||||
EndPoint endpoint = null;
|
||||
if (Address.Scheme == "tcp" || Address.Scheme == "tcp")
|
||||
{
|
||||
var domain = Address.DnsSafeHost;
|
||||
if (!IPAddress.TryParse(domain, out IPAddress address))
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(domain)).FirstOrDefault();
|
||||
if (address == null)
|
||||
throw new Exception("Host not found");
|
||||
}
|
||||
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
endpoint = new IPEndPoint(address, Address.Port);
|
||||
}
|
||||
else if (Address.Scheme == "unix")
|
||||
{
|
||||
var path = Address.AbsoluteUri.Remove(0, "unix:".Length);
|
||||
if (!path.StartsWith('/'))
|
||||
path = "/" + path;
|
||||
while (path.Length >= 2 && (path[0] != '/' || path[1] == '/'))
|
||||
{
|
||||
path = path.Remove(0, 1);
|
||||
}
|
||||
if (path.Length < 2)
|
||||
throw new FormatException("Invalid unix url");
|
||||
socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
|
||||
endpoint = new UnixEndPoint(path);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Protocol {Address.Scheme} for clightning not supported");
|
||||
|
||||
await socket.ConnectAsync(endpoint);
|
||||
return socket;
|
||||
}
|
||||
|
||||
public async Task<BitcoinAddress> NewAddressAsync()
|
||||
{
|
||||
var obj = await SendCommandAsync<JObject>("newaddr");
|
||||
return BitcoinAddress.Create(obj.Property("address").Value.Value<string>(), Network);
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
|
||||
if (invoices.Length == 0)
|
||||
return null;
|
||||
return ToLightningInvoice(invoices[0]);
|
||||
}
|
||||
|
||||
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
|
||||
invoice.Label = id;
|
||||
invoice.MilliSatoshi = amount;
|
||||
invoice.Status = "unpaid";
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoice.Label,
|
||||
Amount = invoice.MilliSatoshi,
|
||||
BOLT11 = invoice.BOLT11,
|
||||
Status = invoice.Status,
|
||||
PaidAt = invoice.PaidAt
|
||||
};
|
||||
}
|
||||
|
||||
Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
|
||||
{
|
||||
return Task.FromResult<ILightningListenInvoiceSession>(this);
|
||||
}
|
||||
long lastInvoiceIndex = 99999999999;
|
||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
|
||||
lastInvoiceIndex = invoice.PayIndex.Value;
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
||||
{
|
||||
var info = await GetInfoAsync(cancellation);
|
||||
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
};
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class NodeInfo
|
||||
{
|
||||
@ -20,5 +20,10 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||
public string NodeId { get; private set; }
|
||||
public string Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}@{Host}:{Port}";
|
||||
}
|
||||
}
|
||||
}
|
60
BTCPayServer/Payments/Lightning/CLightning/PeerInfo.cs
Normal file
60
BTCPayServer/Payments/Lightning/CLightning/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
|
||||
{
|
||||
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; }
|
||||
|
||||
}
|
||||
}
|
140
BTCPayServer/Payments/Lightning/CLightning/UnixEndPoint.cs
Normal file
140
BTCPayServer/Payments/Lightning/CLightning/UnixEndPoint.cs
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets.
|
||||
//
|
||||
// Authors:
|
||||
// Gonzalo Paniagua Javier (gonzalo@ximian.com)
|
||||
//
|
||||
// (C) 2003 Ximian, Inc (http://www.ximian.com)
|
||||
//
|
||||
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace Mono.Unix
|
||||
{
|
||||
[Serializable]
|
||||
public class UnixEndPoint : EndPoint
|
||||
{
|
||||
string filename;
|
||||
|
||||
public UnixEndPoint(string filename)
|
||||
{
|
||||
if (filename == null)
|
||||
throw new ArgumentNullException("filename");
|
||||
|
||||
if (filename.Length == 0)
|
||||
throw new ArgumentException("Cannot be empty.", "filename");
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public string Filename
|
||||
{
|
||||
get
|
||||
{
|
||||
return (filename);
|
||||
}
|
||||
set
|
||||
{
|
||||
filename = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override AddressFamily AddressFamily
|
||||
{
|
||||
get { return AddressFamily.Unix; }
|
||||
}
|
||||
|
||||
public override EndPoint Create(SocketAddress socketAddress)
|
||||
{
|
||||
/*
|
||||
* Should also check this
|
||||
*
|
||||
int addr = (int) AddressFamily.Unix;
|
||||
if (socketAddress [0] != (addr & 0xFF))
|
||||
throw new ArgumentException ("socketAddress is not a unix socket address.");
|
||||
|
||||
if (socketAddress [1] != ((addr & 0xFF00) >> 8))
|
||||
throw new ArgumentException ("socketAddress is not a unix socket address.");
|
||||
*/
|
||||
|
||||
if (socketAddress.Size == 2)
|
||||
{
|
||||
// Empty filename.
|
||||
// Probably from RemoteEndPoint which on linux does not return the file name.
|
||||
UnixEndPoint uep = new UnixEndPoint("a");
|
||||
uep.filename = "";
|
||||
return uep;
|
||||
}
|
||||
int size = socketAddress.Size - 2;
|
||||
byte[] bytes = new byte[size];
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = socketAddress[i + 2];
|
||||
// There may be junk after the null terminator, so ignore it all.
|
||||
if (bytes[i] == 0)
|
||||
{
|
||||
size = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string name = Encoding.Default.GetString(bytes, 0, size);
|
||||
return new UnixEndPoint(name);
|
||||
}
|
||||
|
||||
public override SocketAddress Serialize()
|
||||
{
|
||||
byte[] bytes = Encoding.Default.GetBytes(filename);
|
||||
SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1);
|
||||
// sa [0] -> family low byte, sa [1] -> family high byte
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
sa[2 + i] = bytes[i];
|
||||
|
||||
//NULL suffix for non-abstract path
|
||||
sa[2 + bytes.Length] = 0;
|
||||
|
||||
return sa;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (filename);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return filename.GetHashCode(StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override bool Equals(object o)
|
||||
{
|
||||
UnixEndPoint other = o as UnixEndPoint;
|
||||
if (other == null)
|
||||
return false;
|
||||
|
||||
return (other.filename == filename);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,9 +13,9 @@ using NBitcoin;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class ChargeClient
|
||||
public class ChargeClient : ILightningInvoiceClient
|
||||
{
|
||||
private Uri _Uri;
|
||||
public Uri Uri
|
||||
@ -126,5 +126,48 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
uri += "/";
|
||||
return new Uri(uri + partialUrl);
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await GetInvoice(invoiceId, cancellation);
|
||||
return ChargeClient.ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
async Task<ILightningListenInvoiceSession> ILightningInvoiceClient.Listen(CancellationToken cancellation)
|
||||
{
|
||||
return await Listen(cancellation);
|
||||
}
|
||||
|
||||
internal static LightningInvoice ToLightningInvoice(ChargeInvoice invoice)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
Id = invoice.Id ?? invoice.Label,
|
||||
Amount = invoice.MilliSatoshi,
|
||||
BOLT11 = invoice.PaymentRequest,
|
||||
PaidAt = invoice.PaidAt,
|
||||
Status = invoice.Status
|
||||
};
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amont = amount, Expiry = expiry });
|
||||
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
||||
}
|
||||
|
||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
||||
{
|
||||
var info = await GetInfoAsync(cancellation);
|
||||
var address = info.Address.Select(a => a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class ChargeInvoice
|
||||
{
|
||||
@ -27,8 +27,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
|
||||
[JsonProperty("payreq")]
|
||||
public string PaymentRequest { get; set; }
|
||||
public string Label { get; set; }
|
||||
}
|
||||
public class ChargeSession : IDisposable
|
||||
public class ChargeSession : ILightningListenInvoiceSession
|
||||
{
|
||||
private ClientWebSocket socket;
|
||||
|
||||
@ -42,7 +43,7 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
}
|
||||
|
||||
ArraySegment<byte> _Buffer;
|
||||
public async Task<ChargeInvoice> NextEvent(CancellationToken cancellation = default(CancellationToken))
|
||||
public async Task<ChargeInvoice> WaitInvoice(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var buffer = _Buffer;
|
||||
var array = _Buffer.Array;
|
||||
@ -120,5 +121,15 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
await this.socket.CloseSocket();
|
||||
}
|
||||
|
||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken token)
|
||||
{
|
||||
return ChargeClient.ToLightningInvoice(await WaitInvoice(token));
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
public class CreateInvoiceResponse
|
||||
{
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
namespace BTCPayServer.Payments.Lightning.Charge
|
||||
{
|
||||
//[{"type":"ipv4","address":"52.166.90.122","port":9735}]
|
||||
public class GetInfoResponse
|
@ -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; }
|
||||
}
|
||||
}
|
41
BTCPayServer/Payments/Lightning/ILightningInvoiceClient.cs
Normal file
41
BTCPayServer/Payments/Lightning/ILightningInvoiceClient.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningInvoice
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string BOLT11 { get; set; }
|
||||
public DateTimeOffset? PaidAt
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public LightMoney Amount { get; set; }
|
||||
}
|
||||
|
||||
public class LightningNodeInformation
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public int P2PPort { get; internal set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
public interface ILightningInvoiceClient
|
||||
{
|
||||
Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken));
|
||||
Task<LightningInvoice> CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation = default(CancellationToken));
|
||||
Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken));
|
||||
Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken));
|
||||
}
|
||||
|
||||
public interface ILightningListenInvoiceSession : IDisposable
|
||||
{
|
||||
Task<LightningInvoice> WaitInvoice(CancellationToken cancellation);
|
||||
}
|
||||
}
|
27
BTCPayServer/Payments/Lightning/LightningClientFactory.cs
Normal file
27
BTCPayServer/Payments/Lightning/LightningClientFactory.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningClientFactory
|
||||
{
|
||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var uri = supportedPaymentMethod.GetLightningUrl();
|
||||
if (uri.ConnectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
|
||||
}
|
||||
else if (uri.ConnectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
|
||||
}
|
||||
}
|
||||
}
|
110
BTCPayServer/Payments/Lightning/LightningConnectionString.cs
Normal file
110
BTCPayServer/Payments/Lightning/LightningConnectionString.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public enum LightningConnectionType
|
||||
{
|
||||
Charge,
|
||||
CLightning
|
||||
}
|
||||
public class LightningConnectionString
|
||||
{
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString)
|
||||
{
|
||||
return TryParse(str, out connectionString, out var error);
|
||||
}
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
if (str.StartsWith('/'))
|
||||
str = "unix:" + str;
|
||||
var result = new LightningConnectionString();
|
||||
connectionString = null;
|
||||
error = null;
|
||||
|
||||
Uri uri;
|
||||
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
{
|
||||
error = "Invalid URL";
|
||||
return false;
|
||||
}
|
||||
|
||||
var supportedDomains = new string[] { "unix", "tcp", "http", "https" };
|
||||
if (!supportedDomains.Contains(uri.Scheme))
|
||||
{
|
||||
var protocols = String.Join(",", supportedDomains);
|
||||
error = $"The url support the following protocols {protocols}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "unix")
|
||||
{
|
||||
str = uri.AbsoluteUri.Substring("unix:".Length);
|
||||
while (str.Length >= 1 && str[0] == '/')
|
||||
{
|
||||
str = str.Substring(1);
|
||||
}
|
||||
uri = new Uri("unix://" + str, UriKind.Absolute);
|
||||
}
|
||||
|
||||
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||||
{
|
||||
var parts = uri.UserInfo.Split(':');
|
||||
if (string.IsNullOrEmpty(uri.UserInfo) || parts.Length != 2)
|
||||
{
|
||||
error = "The url is missing user and password";
|
||||
return false;
|
||||
}
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
error = "The url should not have user information";
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
public LightningConnectionString()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public Uri BaseUri { get; set; }
|
||||
|
||||
public LightningConnectionType ConnectionType
|
||||
{
|
||||
get
|
||||
{
|
||||
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
|
||||
: LightningConnectionType.CLightning;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri ToUri(bool withCredentials)
|
||||
{
|
||||
if (withCredentials)
|
||||
{
|
||||
return new UriBuilder(BaseUri) { UserName = Username ?? "", Password = Password ?? "" }.Uri;
|
||||
}
|
||||
else
|
||||
{
|
||||
return BaseUri;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ToUri(true).AbsoluteUri;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
@ -12,89 +14,95 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
|
||||
{
|
||||
ExplorerClientProvider _ExplorerClientProvider;
|
||||
public LightningLikePaymentHandler(ExplorerClientProvider explorerClientProvider)
|
||||
NBXplorerDashboard _Dashboard;
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningLikePaymentHandler(
|
||||
LightningClientFactory lightningClientFactory,
|
||||
NBXplorerDashboard dashboard)
|
||||
{
|
||||
_ExplorerClientProvider = explorerClientProvider;
|
||||
_LightningClientFactory = lightningClientFactory;
|
||||
_Dashboard = dashboard;
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var test = Test(supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = invoice.ProductInformation.Price / paymentMethod.Rate;
|
||||
var client = GetClient(supportedPaymentMethod, network);
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
|
||||
if (expiry < TimeSpan.Zero)
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
|
||||
LightningInvoice lightningInvoice = null;
|
||||
try
|
||||
{
|
||||
Amont = new LightMoney(due, LightMoneyUnit.BTC),
|
||||
Expiry = expiry < TimeSpan.Zero ? TimeSpan.FromSeconds(1) : expiry
|
||||
});
|
||||
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
||||
}
|
||||
var nodeInfo = await test;
|
||||
return new LightningLikePaymentMethodDetails()
|
||||
{
|
||||
BOLT11 = lightningInvoice.PayReq,
|
||||
InvoiceId = lightningInvoice.Id
|
||||
BOLT11 = lightningInvoice.BOLT11,
|
||||
InvoiceId = lightningInvoice.Id,
|
||||
NodeInfo = nodeInfo.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Test(supportedPaymentMethod, network);
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Used for testing
|
||||
/// </summary>
|
||||
public bool SkipP2PTest { get; set; }
|
||||
|
||||
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_ExplorerClientProvider.IsAvailable(network))
|
||||
throw new Exception($"Full node not available");
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
||||
var explorerClient = _ExplorerClientProvider.GetExplorerClient(network);
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
var client = GetClient(supportedPaymentMethod, network);
|
||||
var status = explorerClient.GetStatusAsync();
|
||||
GetInfoResponse info = null;
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
LightningNodeInformation info = null;
|
||||
try
|
||||
{
|
||||
|
||||
info = await client.GetInfoAsync(cts.Token);
|
||||
info = await client.GetInfo(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
var address = info.Address.Select(a=>a.Address).FirstOrDefault();
|
||||
var port = info.Port;
|
||||
address = address ?? client.Uri.DnsSafeHost;
|
||||
|
||||
if (info.Network != network.CLightningNetworkName)
|
||||
if (info.Address == null)
|
||||
{
|
||||
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
|
||||
var blocksGap = Math.Abs(info.BlockHeight - (await status).ChainHeight);
|
||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||
if (blocksGap > 10)
|
||||
{
|
||||
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
|
||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await TestConnection(address, port, cts.Token);
|
||||
if (!SkipP2PTest)
|
||||
{
|
||||
await TestConnection(info.Address, info.P2PPort, cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning node via {address}:{port} ({ex.Message})");
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
|
||||
}
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
}
|
||||
|
||||
private static ChargeClient GetClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||
private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||
{
|
||||
IPAddress address = null;
|
||||
try
|
||||
@ -103,25 +111,16 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
|
||||
}
|
||||
catch { }
|
||||
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
|
||||
}
|
||||
|
||||
if (address == null)
|
||||
throw new Exception($"DNS did not resolved {addressStr}");
|
||||
throw new PaymentMethodUnavailableException($"DNS did not resolved {addressStr}");
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
{
|
||||
try
|
||||
{
|
||||
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
|
||||
}
|
||||
catch { return false; }
|
||||
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Task WithTimeout(Task task, CancellationToken token)
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public string BOLT11 { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string NodeInfo { get; set; }
|
||||
|
||||
public string GetPaymentDestination()
|
||||
{
|
||||
|
@ -6,14 +6,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class ChargeListener : IHostedService
|
||||
public class LightningListener : IHostedService
|
||||
{
|
||||
class ListenedInvoice
|
||||
{
|
||||
@ -28,13 +27,16 @@ namespace BTCPayServer.Payments.Lightning
|
||||
EventAggregator _Aggregator;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
public ChargeListener(EventAggregator aggregator,
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningListener(EventAggregator aggregator,
|
||||
InvoiceRepository invoiceRepository,
|
||||
LightningClientFactory lightningClientFactory,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_Aggregator = aggregator;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_LightningClientFactory = lightningClientFactory;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
@ -62,7 +64,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (Listening(invoiceId))
|
||||
return;
|
||||
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike))
|
||||
@ -78,7 +79,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
var listenedInvoice = new ListenedInvoice()
|
||||
{
|
||||
Uri = lightningSupportedMethod.GetLightningChargeUrl(false).AbsoluteUri,
|
||||
Uri = lightningSupportedMethod.GetLightningUrl().BaseUri.AbsoluteUri,
|
||||
PaymentMethodDetails = lightningMethod,
|
||||
SupportedPaymentMethod = lightningSupportedMethod,
|
||||
PaymentMethod = paymentMethod,
|
||||
@ -88,11 +89,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
if (poll)
|
||||
{
|
||||
var charge = GetChargeClient(lightningSupportedMethod, network);
|
||||
var charge = _LightningClientFactory.CreateClient(lightningSupportedMethod, network);
|
||||
var chargeInvoice = await charge.GetInvoice(lightningMethod.InvoiceId);
|
||||
if (chargeInvoice == null)
|
||||
continue;
|
||||
if(chargeInvoice.Status == "paid")
|
||||
if (chargeInvoice.Status == "paid")
|
||||
await AddPayment(network, chargeInvoice, listenedInvoice);
|
||||
if (chargeInvoice.Status == "paid" || chargeInvoice.Status == "expired")
|
||||
continue;
|
||||
@ -124,18 +125,17 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
var charge = GetChargeClient(supportedPaymentMethod, network);
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var session = await charge.Listen(_Cts.Token);
|
||||
while (true)
|
||||
{
|
||||
var notification = await session.NextEvent(_Cts.Token);
|
||||
var notification = await session.WaitInvoice(_Cts.Token);
|
||||
ListenedInvoice listenedInvoice = GetListenedInvoice(notification.Id);
|
||||
if (listenedInvoice == null)
|
||||
continue;
|
||||
|
||||
if (notification.Id == listenedInvoice.PaymentMethodDetails.InvoiceId &&
|
||||
notification.PaymentRequest == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
notification.BOLT11 == listenedInvoice.PaymentMethodDetails.BOLT11)
|
||||
{
|
||||
if (notification.Status == "paid" && notification.PaidAt.HasValue)
|
||||
{
|
||||
@ -156,24 +156,21 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
DoneListening(supportedPaymentMethod.GetLightningUrl());
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningChargeUrl(false)}");
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
|
||||
private async Task AddPayment(BTCPayNetwork network, ChargeInvoice notification, ListenedInvoice listenedInvoice)
|
||||
private async Task AddPayment(BTCPayNetwork network, LightningInvoice notification, ListenedInvoice listenedInvoice)
|
||||
{
|
||||
await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||
{
|
||||
BOLT11 = notification.PaymentRequest,
|
||||
Amount = notification.MilliSatoshi
|
||||
BOLT11 = notification.BOLT11,
|
||||
Amount = notification.Amount
|
||||
}, network.CryptoCode, accounted: true);
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
|
||||
private static ChargeClient GetChargeClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
|
||||
if(payment != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
|
||||
List<Task> _ListeningLightning = new List<Task>();
|
||||
@ -202,9 +199,27 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop listening all invoices on this server
|
||||
/// </summary>
|
||||
/// <param name="uri"></param>
|
||||
private void DoneListening(LightningConnectionString connectionString)
|
||||
{
|
||||
var uri = connectionString.BaseUri;
|
||||
lock (_ListenedInvoiceByChargeInvoiceId)
|
||||
{
|
||||
foreach (var listenedInvoice in _ListenedInvoiceByLightningUrl[uri.AbsoluteUri])
|
||||
{
|
||||
_ListenedInvoiceByChargeInvoiceId.Remove(listenedInvoice.PaymentMethodDetails.InvoiceId);
|
||||
_InvoiceIds.Remove(listenedInvoice.InvoiceId);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Remove(uri.AbsoluteUri);
|
||||
}
|
||||
}
|
||||
|
||||
bool Listening(string invoiceId)
|
||||
{
|
||||
lock(_ListenedInvoiceByLightningUrl)
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
return _InvoiceIds.Contains(invoiceId);
|
||||
}
|
||||
@ -230,13 +245,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
var listen = Listen(listenedInvoice.SupportedPaymentMethod, listenedInvoice.Network);
|
||||
_ListeningLightning.Add(listen);
|
||||
listen.ContinueWith(_ =>
|
||||
{
|
||||
lock (_ListenedInvoiceByLightningUrl)
|
||||
{
|
||||
_ListeningLightning.Remove(listen);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
_ListenedInvoiceByLightningUrl.Add(listenedInvoice.Uri, listenedInvoice);
|
||||
_ListenedInvoiceByChargeInvoiceId.Add(listenedInvoice.PaymentMethodDetails.InvoiceId, listenedInvoice);
|
@ -8,41 +8,36 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string LightningChargeUrl { get; set; }
|
||||
|
||||
public Uri GetLightningChargeUrl(bool withCredentials)
|
||||
public LightningConnectionString GetLightningUrl()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
UriBuilder uri = new UriBuilder(LightningChargeUrl);
|
||||
if (withCredentials)
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
|
||||
{
|
||||
uri.UserName = Username;
|
||||
uri.Password = Password;
|
||||
throw new FormatException(error);
|
||||
}
|
||||
return connectionString;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
return uri.Uri;
|
||||
}
|
||||
|
||||
public void SetLightningChargeUrl(Uri uri)
|
||||
public void SetLightningUrl(LightningConnectionString connectionString)
|
||||
{
|
||||
if (uri == null)
|
||||
throw new ArgumentNullException(nameof(uri));
|
||||
if (string.IsNullOrEmpty(uri.UserInfo))
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
|
||||
var splitted = uri.UserInfo.Split(':');
|
||||
if (splitted.Length != 2)
|
||||
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
|
||||
if (connectionString == null)
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Username = splitted[0];
|
||||
Password = splitted[1];
|
||||
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
|
||||
Username = connectionString.Username;
|
||||
Password = connectionString.Password;
|
||||
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Username { get; set; }
|
||||
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Password { get; set; }
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||
}
|
||||
|
19
BTCPayServer/Payments/PaymentMethodUnavailableException.cs
Normal file
19
BTCPayServer/Payments/PaymentMethodUnavailableException.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
public class PaymentMethodUnavailableException : Exception
|
||||
{
|
||||
public PaymentMethodUnavailableException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
public PaymentMethodUnavailableException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,12 @@
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_INTERNALLIGHTNINGNODE": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
"BTCPAY_BUNDLEJSCSS": "false"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14142/"
|
||||
"applicationUrl": "http://127.0.0.1:14142/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -76,18 +77,19 @@ namespace BTCPayServer.Services
|
||||
return new LedgerTestResult() { Success = true };
|
||||
}
|
||||
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
|
||||
var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
|
||||
var path = new KeyPath("49'").Derive(network.CoinType).Derive(account, true);
|
||||
var pubkey = await GetExtPubKey(_Ledger, network, path, false);
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = true,
|
||||
Legacy = false
|
||||
});
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
|
||||
}
|
||||
|
||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
|
||||
@ -152,7 +154,8 @@ namespace BTCPayServer.Services
|
||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
||||
FeeRate feeRate,
|
||||
IDestination changeAddress,
|
||||
KeyPath changeKeyPath)
|
||||
KeyPath changeKeyPath,
|
||||
FeeRate minTxRelayFee)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
@ -185,6 +188,7 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
@ -229,5 +233,8 @@ namespace BTCPayServer.Services
|
||||
public class GetXPubResult
|
||||
{
|
||||
public string ExtPubKey { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
|
||||
public KeyPath KeyPath { get; set; }
|
||||
public int CoinType { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning disable CS0618
|
||||
public List<PaymentEntity> GetPayments()
|
||||
{
|
||||
return Payments.ToList();
|
||||
return Payments?.ToList() ?? new List<PaymentEntity>();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode)
|
||||
{
|
||||
@ -375,7 +375,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
|
||||
var paymentId = info.GetId();
|
||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
@ -383,7 +384,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
};
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
if (info.CryptoCode == "BTC")
|
||||
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
dto.Url = cryptoInfo.Url;
|
||||
dto.BTCPrice = cryptoInfo.Price;
|
||||
@ -582,7 +583,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 +593,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 +616,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 +647,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 +657,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 +683,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 +693,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 +754,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 +762,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 +790,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 +801,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));
|
||||
|
@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
List<string> textSearch = new List<string>();
|
||||
invoice = Clone(invoice, null);
|
||||
@ -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,
|
||||
@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
|
||||
}
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
|
||||
foreach(var log in creationLogs)
|
||||
{
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
Message = log,
|
||||
Timestamp = invoice.InvoiceTime,
|
||||
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -162,12 +173,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 +220,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 +472,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 +506,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;
|
||||
}
|
||||
|
36
BTCPayServer/Services/LanguageService.cs
Normal file
36
BTCPayServer/Services/LanguageService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Language
|
||||
{
|
||||
public Language(string code, string displayName)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
Code = code;
|
||||
}
|
||||
public string Code { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
public class LanguageService
|
||||
{
|
||||
public Language[] GetLanguages()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new Language("en-US", "English"),
|
||||
new Language("de-DE", "Deutsch"),
|
||||
new Language("ja-JP", "日本語"),
|
||||
new Language("fr-FR", "Français"),
|
||||
new Language("es-ES", "Spanish"),
|
||||
new Language("pt-BR", "Portuguese (Brazil)"),
|
||||
new Language("nl-NL", "Dutch"),
|
||||
new Language("cs-CZ", "Česky"),
|
||||
new Language("is-IS", "Íslenska"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -41,6 +41,8 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
|
||||
{
|
||||
if (currency == "BTC")
|
||||
return 1.0m;
|
||||
if (rates.TryGetValue(currency, out decimal result))
|
||||
return result;
|
||||
throw new RateUnavailableException(currency);
|
||||
|
@ -48,14 +48,72 @@ namespace BTCPayServer.Services.Stores
|
||||
}
|
||||
}
|
||||
|
||||
public class StoreUser
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Role { get; set; }
|
||||
}
|
||||
public async Task<StoreUser[]> GetStoreUsers(string storeId)
|
||||
{
|
||||
if (storeId == null)
|
||||
throw new ArgumentNullException(nameof(storeId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx
|
||||
.UserStore
|
||||
.Where(u => u.StoreDataId == storeId)
|
||||
.Select(u => new StoreUser()
|
||||
{
|
||||
Id = u.ApplicationUserId,
|
||||
Email = u.ApplicationUser.Email,
|
||||
Role = u.Role
|
||||
}).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreData[]> GetStoresByUserId(string userId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
return (await ctx.UserStore
|
||||
.Where(u => u.ApplicationUserId == userId)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
.Select(u => new { u.StoreData, u.Role })
|
||||
.ToArrayAsync())
|
||||
.Select(u =>
|
||||
{
|
||||
u.StoreData.Role = u.Role;
|
||||
return u.StoreData;
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddStoreUser(string storeId, string userId, string role)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role };
|
||||
ctx.UserStore.Add(userStore);
|
||||
try
|
||||
{
|
||||
await ctx.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveStoreUser(string storeId, string userId)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
|
||||
ctx.UserStore.Add(userStore);
|
||||
ctx.Entry<UserStore>(userStore).State = EntityState.Deleted;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
26
BTCPayServer/StorePolicies.cs
Normal file
26
BTCPayServer/StorePolicies.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class StorePolicies
|
||||
{
|
||||
public const string CanAccessStores = "CanAccessStore";
|
||||
public const string OwnStore = "OwnStore";
|
||||
}
|
||||
public class StoreRoles
|
||||
{
|
||||
public const string Owner = "Owner";
|
||||
public const string Guest = "Guest";
|
||||
public static IEnumerable<String> AllRoles
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return Owner;
|
||||
yield return Guest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
570
BTCPayServer/Views/Invoice/Checkout-Body.cshtml
Normal file
570
BTCPayServer/Views/Invoice/Checkout-Body.cshtml
Normal file
@ -0,0 +1,570 @@
|
||||
@model PaymentModel
|
||||
|
||||
<div class="top-header">
|
||||
<div class="header">
|
||||
<div class="header__icon">
|
||||
@if (Model.CustomLogoLink != null)
|
||||
{
|
||||
<img class="header__icon__img" src="@Model.CustomLogoLink" height="40">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="timer-row">
|
||||
<div class="timer-row__progress-bar" style="width: 0%;"></div>
|
||||
<div class="timer-row__spinner">
|
||||
@Html.Partial("Checkout-Spinner")
|
||||
</div>
|
||||
<div class="timer-row__message">
|
||||
<span v-if="srvModel.status === 'expired' || srvModel.status === 'invalid'">
|
||||
{{$t("Invoice expired")}}
|
||||
</span>
|
||||
<span v-else-if="expiringSoon">
|
||||
{{$t("Invoice expiring soon...")}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{$t("Awaiting Payment...")}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="timer-row__time-left">@Model.TimeLeft</div>
|
||||
</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;">
|
||||
{{$t("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">
|
||||
@Html.Partial("Checkout-Spinner")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="single-item-order buyerTotalLine">
|
||||
<div class="single-item-order__left">
|
||||
<div class="single-item-order__left__name">
|
||||
{{ srvModel.storeName }}
|
||||
</div>
|
||||
<div class="single-item-order__left__description">
|
||||
{{ srvModel.itemDesc }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="single-item-order__right">
|
||||
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
|
||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
<div class="single-item-order__right__ex-rate">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="fa fa-angle-double-down"></span>
|
||||
<span class="fa fa-angle-double-up"></span>
|
||||
</div>
|
||||
<line-items>
|
||||
<div class="line-items">
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">{{$t("Order Amount")}}</div>
|
||||
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span>{{$t("Network Cost")}}</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span>{{$t("Already Paid")}}</span>
|
||||
</div>
|
||||
<div class="line-items__item__value">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item line-items__item--total">
|
||||
<div class="line-items__item__label">{{$t("Due")}}</div>
|
||||
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</line-items>
|
||||
<div class="payment-tabs">
|
||||
<div class="payment-tabs__tab active" id="scan-tab">
|
||||
<span>{{$t("Scan")}}</span>
|
||||
</div>
|
||||
<div class="payment-tabs__tab" id="copy-tab">
|
||||
<span>{{$t("Copy")}}</span>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
{
|
||||
<div class="payment-tabs__tab" id="altcoins-tab">
|
||||
<span>{{$t("Conversion")}}</span>
|
||||
</div>
|
||||
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div id="tabsSlider" class="payment-tabs__slider"></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
|
||||
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
|
||||
<div class="manual__step-one__header">
|
||||
<span>{{$t("Contact and Refund Email")}}</span>
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label">
|
||||
<span>{{$t("Contact_Body")}}</span>
|
||||
</span>
|
||||
<span class="submission-error-label">{{$t("Please enter a valid email address")}}</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" v-bind:placeholder="$t('Your email')" type="email">
|
||||
<bp-loading-button>
|
||||
<button type="submit" class="action-button" style="margin-top: 15px;">
|
||||
<span class="button-text">{{$t("Continue")}}</span>
|
||||
<div class="loader-wrapper">
|
||||
@Html.Partial("Checkout-Spinner")
|
||||
</div>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" />
|
||||
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
<span>{{$t("Open in wallet")}}</span>
|
||||
<span class="glyphicon glyphicon-new-window"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bp-view payment manual-flow" id="copy">
|
||||
<div class="manual__step-two__instructions">
|
||||
<span i18n="">{{$t("CompletePay_Body", srvModel)}}</span>
|
||||
</div>
|
||||
<div class="copyLabelPopup">
|
||||
<span>{{$t("Copied")}}</span>
|
||||
</div>
|
||||
<nav v-if="srvModel.isLightning" class="copyBox">
|
||||
<div class="copySectionBox bottomBorder">
|
||||
<label>{{$t("BOLT 11 Invoice")}}</label>
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly" />
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="separatorGem"></div>
|
||||
<div class="copySectionBox">
|
||||
<label>{{$t("Peer Info")}}</label>
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly" />
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav v-else class="copyBox">
|
||||
<div class="copySectionBox bottomBorder">
|
||||
<label>{{$t("Amount")}}</label>
|
||||
<div class="copyAmountText copy-cursor _copySpan">
|
||||
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="separatorGem"></div>
|
||||
<div class="copySectionBox">
|
||||
<label>{{$t("Address")}}</label>
|
||||
<div class="inputWithIcon _copyInput">
|
||||
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly" />
|
||||
<img v-bind:src="srvModel.cryptoImage" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
{
|
||||
<div id="altcoins" class="bp-view payment manual-flow">
|
||||
<nav v-if="srvModel.isLightning">
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_Lightning")}}
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
<nav v-else>
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_BodyTop", srvModel)}}
|
||||
<br /><br />
|
||||
{{$t("ConversionTab_BodyDesc", srvModel)}}
|
||||
</span>
|
||||
</div>
|
||||
<center>
|
||||
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
|
||||
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
|
||||
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
|
||||
</a>
|
||||
|
||||
@*Changelly doesn't have TO_AMOUNT support so we can't include it
|
||||
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
|
||||
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
|
||||
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
|
||||
</a>*@
|
||||
</center>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="bp-view pad" id="paid">
|
||||
<div class="status-block">
|
||||
<div class="success-block">
|
||||
<div class="status-icon">
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-message">{{$t("This invoice has been paid")}}</div>
|
||||
<button class="action-button">
|
||||
<bp-done-text>
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bp-view expired" id="archived">
|
||||
<div class="expired-icon">
|
||||
<img src="~/imlegacy/archived.svg">
|
||||
</div>
|
||||
<div class="archived__message">
|
||||
<div class="archived__message__header">
|
||||
<span>{{$t("This invoice has been archived")}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{$t("Archived_Body")}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bp-view expired" id="expired">
|
||||
<div>
|
||||
<div class="expired__body">
|
||||
<div class="expired__header">{{$t("What happened?")}}</div>
|
||||
<div class="expired__text" i18n="">
|
||||
{{$t("InvoiceExpired_Body_1", {storeName: srvModel.storeName, maxTimeMinutes: @Model.MaxTimeMinutes})}}
|
||||
</div>
|
||||
<div class="expired__text">
|
||||
{{$t("InvoiceExpired_Body_2")}}
|
||||
</div>
|
||||
<div class="expired__text">
|
||||
{{$t("InvoiceExpired_Body_3")}}
|
||||
</div>
|
||||
<div class="expired__text expired__text__smaller">
|
||||
<span class="expired__text__bullet">{{$t("Invoice ID")}}</span>:
|
||||
{{srvModel.invoiceId}}
|
||||
<br />
|
||||
<span class="expired__text__bullet">{{$t("Order ID")}}</span>:
|
||||
{{srvModel.orderId}}
|
||||
</div>
|
||||
</div>
|
||||
<a href="/invoices" class="action-button" style="margin-top: 20px;">
|
||||
<bp-done-text>
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
</bp-done-text>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Obsolete? Start *@
|
||||
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
|
||||
<div class="manual__step-one refund-address-form" novalidate="">
|
||||
<div class="manual__step-one__header" i18n="">Link Expired</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<bp-loading-button i18n="">
|
||||
<button class="action-button" style="margin-top: 15px;" type="submit">
|
||||
<span class="button-text" lcl="">Request Refund</span>
|
||||
<div class="loader-wrapper">
|
||||
@Html.Partial("Checkout-Spinner")
|
||||
</div>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view confirm-contact-email-view">
|
||||
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
|
||||
<div class="manual__step-one__header" i18n="">Contact & Refund Email</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email"
|
||||
style="opacity: 1;" type="email">
|
||||
<button type="submit" class="action-button" style="margin-top: 15px;">
|
||||
<span i18n="">Confirm email address</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
|
||||
<span i18n="">Wrong email?</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view wrong-email-view" id="compromised-invoice">
|
||||
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
|
||||
<div class="manual__step-one__header">
|
||||
<span i18n="">There seems to be a problem</span>
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">
|
||||
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
|
||||
If you did not submit the email address, it's possible a thief falsely
|
||||
submitted this address to steal refunds. Please contact the merchant
|
||||
about this security incident, and try your payment again.
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
|
||||
<span i18n="">Contact {{srvModel.storeName}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="refund-address-form__link">
|
||||
<span i18n="">I understand, continue to payment →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
|
||||
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
|
||||
<div><img src="~/imlegacy/mail.svg"></div>
|
||||
<div class="manual__step-one__header">
|
||||
<span i18n="">Please confirm your address</span>
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
|
||||
</div>
|
||||
<bp-resend-link id="resend-link">
|
||||
<div class="bp-resend__link">
|
||||
<span class="link-text">
|
||||
|
||||
<span i18n="">Resend email</span>
|
||||
|
||||
</span>
|
||||
<div class="success-text">
|
||||
|
||||
<img src="~/imlegacy/circle-check.svg">
|
||||
|
||||
<div i18n="">Email resent</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</bp-resend-link>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view refund-address-view" id="enter-refund-address">
|
||||
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
|
||||
<div class="manual__step-one__header">
|
||||
|
||||
<span i18n="">Please provide a refund address.</span>
|
||||
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label">
|
||||
|
||||
<span i18n="">
|
||||
To send your refund of {BTC to refund} BTC,
|
||||
we’ll need a bitcoin address from your wallet. Please open your bitcoin
|
||||
wallet, copy a receiving address, and paste it below.
|
||||
</span>
|
||||
|
||||
</span>
|
||||
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
|
||||
<div class="bp-refund-address">
|
||||
<div class="bitcoin-logo">
|
||||
<div><img src="@Model.CryptoImage"></div>
|
||||
</div>
|
||||
<input class="bp-input {'not-empty': addressValue.length > 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length > 0}">
|
||||
</div>
|
||||
</bp-refund-address>
|
||||
<bp-loading-button i18n="" id="request-refund-button">
|
||||
<button class="action-button" style="margin-top: 15px;" type="submit">
|
||||
<span class="button-text" lcl="">Request Refund</span>
|
||||
<div class="loader-wrapper">
|
||||
@Html.Partial("Checkout-Spinner")
|
||||
</div>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
<div class="refund-address-form__cancel">
|
||||
<span i18n="">Cancel</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view" id="refund-pending">
|
||||
<div class="status-block">
|
||||
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
|
||||
<img src="~/imlegacy/refund-pending.svg">
|
||||
<div class="pending-block__header" i18n="">Processing Refund</div>
|
||||
<span>
|
||||
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="manual-box" style="margin-bottom: 30px;">
|
||||
<div class="manual-box__amount amount-only">
|
||||
<div class="manual-box__amount__label label">
|
||||
<span class="initial-label"> </span>
|
||||
<span class="final-label" i18n="">Amount To Be Refunded</span>
|
||||
</div>
|
||||
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
|
||||
</div>
|
||||
<div class="manual-box__address">
|
||||
<div class="flipper">
|
||||
<div class="back"></div>
|
||||
<div class="front">
|
||||
<div class="manual-box__address__arrow"></div>
|
||||
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view expired" id="low-fee">
|
||||
<div class="expired__body">
|
||||
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
|
||||
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
|
||||
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
|
||||
<div class="expired__text" i18n="">
|
||||
If the transaction
|
||||
doesn't confirm, the funds will be spendable again in your wallet.
|
||||
Depending on the wallet, this may take 48-72 hours.
|
||||
</div>
|
||||
<low-fee-timeline>
|
||||
<div class="timeline">
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--complete">
|
||||
<img src="~/imlegacy/checkmark-small.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name" i18n="">Transaction created</div>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--pending">
|
||||
<img src="~/imlegacy/pending.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name">
|
||||
<span i18n="">Transaction confirming — funds have not yet moved</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon"></div>
|
||||
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</low-fee-timeline>
|
||||
</div>
|
||||
<button class="action-button" style="margin-top: .75rem;">
|
||||
<bp-done-text>
|
||||
<span i18n="">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bp-view" id="refund-complete">
|
||||
<div class="status-block">
|
||||
<div class="success-block" style="opacity: 1;">
|
||||
<div class="status-icon">
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-message">
|
||||
<span>
|
||||
<span i18n="">Refund Complete</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="manual-box">
|
||||
<div class="manual-box__amount amount-only">
|
||||
<div class="manual-box__amount__label label">
|
||||
<span class="initial-label" i18n="">Overpaid By</span>
|
||||
<span class="final-label">
|
||||
<span i18n="">Amount Refunded</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="manual-box__amount__value">{BTC amount} BTC</div>
|
||||
</div>
|
||||
<div class="manual-box__address">
|
||||
<div class="flipper flipped-initially">
|
||||
<div class="back"></div>
|
||||
<div class="front">
|
||||
<div class="manual-box__address__arrow"></div>
|
||||
<div class="manual-box__address__label label" i18n="">Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-button finished" style="margin-top: 23px;">
|
||||
<bp-done-text>
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
<div class="footer-button enter-different-address-button">
|
||||
<bp-done-text>
|
||||
<span>{{$t("Return to StoreName", srvModel)}}</span>
|
||||
</bp-done-text>
|
||||
</div>
|
||||
@* Obsolete? End *@
|
||||
</div>
|
5
BTCPayServer/Views/Invoice/Checkout-Spinner.cshtml
Normal file
5
BTCPayServer/Views/Invoice/Checkout-Spinner.cshtml
Normal file
@ -0,0 +1,5 @@
|
||||
<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>
|
@ -1,7 +1,8 @@
|
||||
@model PaymentModel
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.Services.LanguageService langService
|
||||
@model PaymentModel
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = "Payment";
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@ -12,18 +13,19 @@
|
||||
<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" />
|
||||
|
||||
|
||||
@if(Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
}
|
||||
</head>
|
||||
<body style="background: #E4E4E4">
|
||||
<noscript>
|
||||
@ -50,573 +52,102 @@
|
||||
|
||||
<invoice>
|
||||
<div class="no-bounce" id="checkoutCtrl">
|
||||
@*<div class="modal-backdrop fade-in"></div>*@
|
||||
|
||||
<!---->
|
||||
<div class="modal page">
|
||||
<div class="modal-dialog open opened" role="document">
|
||||
<div class="modal-dialog open opened enter-purchaser-email" role="document">
|
||||
<div class="modal-content long">
|
||||
<div class="content">
|
||||
<div class="invoice">
|
||||
<div class="top-header">
|
||||
<div class="header">
|
||||
<div class="header__icon">
|
||||
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="timer-row">
|
||||
<div class="timer-row__progress-bar" style="width: 0%;"></div>
|
||||
<!---->
|
||||
<div class="timer-row__spinner">
|
||||
<bp-spinner>
|
||||
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
|
||||
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
|
||||
</svg>
|
||||
</bp-spinner>
|
||||
</div>
|
||||
<div class="timer-row__message">
|
||||
<!---->
|
||||
<span>
|
||||
<!---->
|
||||
<span i18n="">Awaiting Payment...</span>
|
||||
<!---->
|
||||
</span>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="timer-row__time-left">@Model.TimeLeft</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-details">
|
||||
<!---->
|
||||
<div class="single-item-order buyerTotalLine">
|
||||
<div class="single-item-order__left">
|
||||
<div class="single-item-order__left__name">
|
||||
{{ srvModel.storeName }}
|
||||
</div>
|
||||
<div class="single-item-order__left__description">
|
||||
{{ srvModel.itemDesc }}
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="single-item-order__right">
|
||||
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
|
||||
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="single-item-order__right__ex-rate">
|
||||
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
|
||||
<span class="fa fa-angle-double-down"></span>
|
||||
<span class="fa fa-angle-double-up"></span>
|
||||
</div>
|
||||
<!---->
|
||||
<line-items>
|
||||
<div class="line-items">
|
||||
<!---->
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label" i18n="">Order Amount</div>
|
||||
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span i18n="">Network Cost</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span i18n="">Already Paid</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item line-items__item--total">
|
||||
<div class="line-items__item__label" i18n="">Due </div>
|
||||
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</line-items>
|
||||
<div class="payment-tabs">
|
||||
<div class="payment-tabs__tab active" id="scan-tab">
|
||||
<span i18n="">Scan</span>
|
||||
</div>
|
||||
<div class="payment-tabs__tab" id="copy-tab">
|
||||
<span i18n="">Copy</span>
|
||||
</div>
|
||||
<div class="payment-tabs__slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<qrcode :val="srvModel.invoiceBitcoinUrlQR" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||
<span i18n="">Open in wallet</span>
|
||||
<span class="glyphicon glyphicon-new-window"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
|
||||
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
|
||||
<div class="manual__step-one__header">
|
||||
<!---->
|
||||
<span i18n="">Contact & Refund Email</span>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label">
|
||||
<!---->
|
||||
<span i18n="">Please provide an email address below. We’ll contact you at this address if there is an issue with your payment. </span>
|
||||
<!---->
|
||||
</span>
|
||||
<span class="submission-error-label" i18n="">Please enter a valid email address.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" placeholder="Your email" type="email">
|
||||
<bp-loading-button i18n="">
|
||||
<button class="action-button" style="margin-top: 15px;" type="button">
|
||||
<span class="button-text" lcl="">Continue</span>
|
||||
<div class="loader-wrapper">
|
||||
<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>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
|
||||
<div class="manual__step-one refund-address-form" novalidate="">
|
||||
<div class="manual__step-one__header" i18n="">Link Expired</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<bp-loading-button i18n="">
|
||||
<button class="action-button" style="margin-top: 15px;" type="submit">
|
||||
<span class="button-text" lcl="">Request Refund</span>
|
||||
<div class="loader-wrapper">
|
||||
<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>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view confirm-contact-email-view">
|
||||
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
|
||||
<div class="manual__step-one__header" i18n="">Contact & Refund Email</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email" style="opacity: 1;" type="email">
|
||||
<button class="action-button" style="margin-top: 15px;">
|
||||
<span i18n="">Confirm email address</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
|
||||
<span i18n="">Wrong email?</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view wrong-email-view" id="compromised-invoice">
|
||||
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
|
||||
<div class="manual__step-one__header">
|
||||
<span i18n="">There seems to be a problem</span>
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">
|
||||
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
|
||||
If you did not submit the email address, it's possible a thief falsely
|
||||
submitted this address to steal refunds. Please contact the merchant
|
||||
about this security incident, and try your payment again.
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
|
||||
<span i18n="">Contact {{srvModel.storeName}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="refund-address-form__link">
|
||||
<span i18n="">I understand, continue to payment →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
|
||||
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
|
||||
<div><img src="~/imlegacy/mail.svg"></div>
|
||||
<div class="manual__step-one__header">
|
||||
<span i18n="">Please confirm your address</span>
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
|
||||
</div>
|
||||
<bp-resend-link id="resend-link">
|
||||
<div class="bp-resend__link">
|
||||
<span class="link-text">
|
||||
<!---->
|
||||
<span i18n="">Resend email</span>
|
||||
<!---->
|
||||
</span>
|
||||
<div class="success-text">
|
||||
<!---->
|
||||
<img src="~/imlegacy/circle-check.svg">
|
||||
<!---->
|
||||
<div i18n="">Email resent</div>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</bp-resend-link>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view refund-address-view" id="enter-refund-address">
|
||||
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
|
||||
<div class="manual__step-one__header">
|
||||
<!---->
|
||||
<span i18n="">Please provide a refund address.</span>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label">
|
||||
<!---->
|
||||
<span i18n="">
|
||||
To send your refund of {BTC to refund} BTC,
|
||||
we’ll need a bitcoin address from your wallet. Please open your bitcoin
|
||||
wallet, copy a receiving address, and paste it below.
|
||||
</span>
|
||||
<!---->
|
||||
</span>
|
||||
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
|
||||
<div class="bp-refund-address">
|
||||
<div class="bitcoin-logo">
|
||||
<div><img src="@Model.CryptoImage"></div>
|
||||
</div>
|
||||
<input class="bp-input {'not-empty': addressValue.length > 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length > 0}">
|
||||
</div>
|
||||
</bp-refund-address>
|
||||
<bp-loading-button i18n="" id="request-refund-button">
|
||||
<button class="action-button" style="margin-top: 15px;" type="submit">
|
||||
<span class="button-text" lcl="">Request Refund</span>
|
||||
<div class="loader-wrapper">
|
||||
<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>
|
||||
</button>
|
||||
</bp-loading-button>
|
||||
</div>
|
||||
<div class="refund-address-form__cancel">
|
||||
<span i18n="">Cancel</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bp-view payment manual-flow" id="copy">
|
||||
<div class="manual__step-two__instructions">
|
||||
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
|
||||
</div>
|
||||
<div class="manual-box flipped" style="margin-bottom: 30px;">
|
||||
<div class="manual-box__amount">
|
||||
<div class="manual-box__amount__label label" i18n="">Amount</div>
|
||||
<!---->
|
||||
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
|
||||
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
|
||||
<div class="copied-label">
|
||||
<span i18n="">Copied</span>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="manual-box__address">
|
||||
<div class="flipper flipped-initially">
|
||||
<div class="back"></div>
|
||||
<div class="front">
|
||||
<div class="manual-box__address__arrow"></div>
|
||||
<div class="manual-box__address__label label" i18n="">Address</div>
|
||||
<!---->
|
||||
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img :src="srvModel.cryptoImage" height="16" />
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
|
||||
</div>
|
||||
<div class="copied-label" style="top: 5px;">
|
||||
<span i18n="">Copied</span>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bp-view pad" id="paid">
|
||||
<div class="status-block">
|
||||
<div class="success-block">
|
||||
<div class="status-icon">
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="success-message" i18n="">This invoice has been paid.</div>
|
||||
<!---->
|
||||
<button class="action-button" style="margin-top: 0px;">
|
||||
<bp-done-text>
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
|
||||
<!---->
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="bp-view" id="refund-pending">
|
||||
<div class="status-block">
|
||||
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
|
||||
<img src="~/imlegacy/refund-pending.svg">
|
||||
<div class="pending-block__header" i18n="">Processing Refund</div>
|
||||
<span>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="manual-box" style="margin-bottom: 30px;">
|
||||
<div class="manual-box__amount amount-only">
|
||||
<div class="manual-box__amount__label label">
|
||||
<span class="initial-label"> </span>
|
||||
<span class="final-label" i18n="">Amount To Be Refunded</span>
|
||||
</div>
|
||||
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
|
||||
</div>
|
||||
<div class="manual-box__address">
|
||||
<div class="flipper">
|
||||
<div class="back"></div>
|
||||
<div class="front">
|
||||
<div class="manual-box__address__arrow"></div>
|
||||
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="bp-view expired" id="low-fee">
|
||||
<div class="expired__body">
|
||||
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
|
||||
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
|
||||
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
|
||||
<div class="expired__text" i18n="">
|
||||
If the transaction
|
||||
doesn't confirm, the funds will be spendable again in your wallet.
|
||||
Depending on the wallet, this may take 48-72 hours.
|
||||
</div>
|
||||
<low-fee-timeline>
|
||||
<div class="timeline">
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--complete">
|
||||
<img src="~/imlegacy/checkmark-small.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name" i18n="">Transaction created</div>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon timeline__item__icon--pending">
|
||||
<img src="~/imlegacy/pending.svg">
|
||||
</div>
|
||||
<div class="timeline__item__name">
|
||||
<span i18n="">Transaction confirming — funds have not yet moved</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline__item">
|
||||
<div class="timeline__item__icon"></div>
|
||||
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</low-fee-timeline>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<button class="action-button" style="margin-top: .75rem;">
|
||||
<bp-done-text>
|
||||
<!---->
|
||||
<!---->
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bp-view expired" id="expired">
|
||||
<!---->
|
||||
<div>
|
||||
<div class="expired__body">
|
||||
<div class="expired__header" i18n="">What happened?</div>
|
||||
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
|
||||
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
|
||||
<div class="expired__text" i18n="">
|
||||
If the transaction
|
||||
is not accepted by the Bitcoin network, the funds will be spendable
|
||||
again in your wallet. Depending on your wallet, this may take 48-72
|
||||
hours.
|
||||
</div>
|
||||
<div class="expired__text">
|
||||
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
|
||||
<!---->
|
||||
<span>
|
||||
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/invoices" class="action-button" style="margin-top: 20px;">
|
||||
<bp-done-text>
|
||||
<!---->
|
||||
<!---->
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</a>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<div class="bp-view expired" id="archived">
|
||||
<div class="expired-icon">
|
||||
<img src="~/imlegacy/archived.svg">
|
||||
</div>
|
||||
<div class="archived__message">
|
||||
<div class="archived__message__header">
|
||||
<span i18n="">This invoice has been archived.</span>
|
||||
</div>
|
||||
<div>
|
||||
<span i18n="">Please contact the store for order information or assistance.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<div class="bp-view" id="refund-complete">
|
||||
<div class="status-block">
|
||||
<div class="success-block" style="opacity: 1;">
|
||||
<div class="status-icon">
|
||||
<div class="status-icon__wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<div class="status-icon__wrapper__icon">
|
||||
<img src="~/imlegacy/checkmark.svg">
|
||||
</div>
|
||||
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="success-message">
|
||||
<!---->
|
||||
<span>
|
||||
<span i18n="">Refund Complete</span>
|
||||
</span>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="manual-box">
|
||||
<div class="manual-box__amount amount-only">
|
||||
<div class="manual-box__amount__label label">
|
||||
<span class="initial-label" i18n="">Overpaid By</span>
|
||||
<!---->
|
||||
<span class="final-label">
|
||||
<span i18n="">Amount Refunded</span>
|
||||
</span>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="manual-box__amount__value">{BTC amount} BTC</div>
|
||||
</div>
|
||||
<div class="manual-box__address">
|
||||
<div class="flipper flipped-initially">
|
||||
<div class="back"></div>
|
||||
<div class="front">
|
||||
<div class="manual-box__address__arrow"></div>
|
||||
<div class="manual-box__address__label label" i18n="">Refunded To</div>
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="~/imlegacy/bitcoin-symbol.svg">
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="action-button finished" style="margin-top: 23px;">
|
||||
<bp-done-text>
|
||||
<!---->
|
||||
<!---->
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</button>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<div class="footer-button enter-different-address-button">
|
||||
<bp-done-text>
|
||||
<!---->
|
||||
<!---->
|
||||
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
|
||||
</bp-done-text>
|
||||
</div>
|
||||
</div>
|
||||
@Html.Partial("Checkout-Body")
|
||||
</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="margin-top: 10px; text-align: right;">
|
||||
@* Not working because of nsSeparator: false, keySeparator: false,
|
||||
{{$t("nested.lang")}} >>
|
||||
*@
|
||||
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
|
||||
@foreach(var lang in langService.GetLanguages())
|
||||
{
|
||||
<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>
|
||||
<option value="@lang.Code">@lang.DisplayName</option>
|
||||
}
|
||||
</div>
|
||||
</select>
|
||||
<script>
|
||||
$(function () {
|
||||
var storeDefaultLang = '@Model.DefaultLang';
|
||||
if (urlParams.lang) {
|
||||
$(".cmblang").val(urlParams.lang);
|
||||
} else if (storeDefaultLang) {
|
||||
$(".cmblang").val(storeDefaultLang);
|
||||
}
|
||||
|
||||
$('select').prettyDropdown({
|
||||
classic: false,
|
||||
height: 30,
|
||||
reverse: true,
|
||||
hoverIntent: 5000
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div style="margin-top: 10px; text-align: right;" class="form-text small text-muted">
|
||||
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</invoice>
|
||||
<script type="text/javascript">
|
||||
var storeDefaultLang = '@Model.DefaultLang';
|
||||
// initialization
|
||||
i18next.init({
|
||||
lng: storeDefaultLang,
|
||||
fallbackLng: 'en-US',
|
||||
nsSeparator: false,
|
||||
keySeparator: false,
|
||||
resources: {
|
||||
'en-US': { translation: locales_en },
|
||||
'de-DE': { translation: locales_de },
|
||||
'es-ES': { translation: locales_es },
|
||||
'ja-JP': { translation: locales_ja },
|
||||
'fr-FR': { translation: locales_fr },
|
||||
'pt-BR': { translation: locales_pt_br },
|
||||
'nl': { translation: locales_nl },
|
||||
'cs-CZ': { translation: locales_cs },
|
||||
'is-IS': { translation: locales_is }
|
||||
},
|
||||
});
|
||||
|
||||
function changeLanguage(lang) {
|
||||
i18next.changeLanguage(lang);
|
||||
}
|
||||
|
||||
if (urlParams.lang) {
|
||||
changeLanguage(urlParams.lang);
|
||||
}
|
||||
else if (storeDefaultLang) {
|
||||
changeLanguage(storeDefaultLang);
|
||||
}
|
||||
|
||||
const i18n = new VueI18next(i18next);
|
||||
|
||||
// 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({
|
||||
i18n: i18n,
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel,
|
||||
expiringSoon: false
|
||||
}
|
||||
});
|
||||
</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>
|
||||
|
@ -23,7 +23,8 @@
|
||||
<tr>
|
||||
<td>@user.Name</td>
|
||||
<td>@user.Email</td>
|
||||
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
|
||||
</tr>}
|
||||
<td><a asp-action="User" asp-route-userId="@user.Id">Modify</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
22
BTCPayServer/Views/Server/User.cshtml
Normal file
22
BTCPayServer/Views/Server/User.cshtml
Normal file
@ -0,0 +1,22 @@
|
||||
@model UserViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Email;
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="IsAdmin"></label>
|
||||
<input asp-for="IsAdmin" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user