Compare commits
245 Commits
Author | SHA1 | Date | |
---|---|---|---|
145e3bec83 | |||
563e931468 | |||
3113097c4f | |||
cdbbad1694 | |||
c9c2730409 | |||
310a9a6d59 | |||
1a1078782e | |||
73cb3dc4ee | |||
9eb36a8b40 | |||
6307aa8665 | |||
b9e8408db5 | |||
0879895678 | |||
a4ecf070b0 | |||
162d76206e | |||
5af14ef2ec | |||
7210eebeca | |||
25dbf6445f | |||
0828c60537 | |||
34deb17f3d | |||
06b02b8691 | |||
b7abc08c27 | |||
399ae2cd9e | |||
63fe0f6612 | |||
42d60ef84b | |||
1784c30787 | |||
ac8feceaf2 | |||
3d8c5195ae | |||
9a5259510b | |||
caecb26420 | |||
ecc8b3d9ed | |||
d313395751 | |||
9e698a8004 | |||
3c4c99ee42 | |||
d34ffc0d9a | |||
039303bfaa | |||
273cf1adc9 | |||
5feb520843 | |||
17e914778d | |||
db24ab792f | |||
42475ec7b7 | |||
4972f0ab7b | |||
07e13747cf | |||
2465eb7e36 | |||
4ddcd7a4c8 | |||
89d9658e82 | |||
66ecb32538 | |||
a22576da0a | |||
69bd888bab | |||
9b540273fc | |||
cfd083bed5 | |||
55c9314cdd | |||
448cc06a11 | |||
0780df4fd7 | |||
04174b7431 | |||
b7c58c2083 | |||
cd75fd6842 | |||
370951a3bd | |||
2c08b0137b | |||
1eee31e9f1 | |||
01cf579530 | |||
f72705935a | |||
a29ab6b6b0 | |||
4784518235 | |||
f8c88bd44f | |||
0d1d0d57f4 | |||
2bd1238668 | |||
d1fb51b412 | |||
279de1b869 | |||
ce9189caf8 | |||
431147784e | |||
0697b8bf86 | |||
5050b59014 | |||
665cf4c3b1 | |||
98e81ab0fd | |||
6ce70237fc | |||
4f23fc99a1 | |||
d7fccae452 | |||
d7a5021ed2 | |||
63ec832667 | |||
8d95b9fa04 | |||
b497d1871e | |||
c7cd029482 | |||
68f2cba60d | |||
5c4200b036 | |||
bc06114023 | |||
556082c4c9 | |||
6a46d02fc6 | |||
d75e5b8b12 | |||
d293bc3947 | |||
e634700913 | |||
ce81136c88 | |||
a97ef2eee8 | |||
be33ebc168 | |||
789193a0c8 | |||
01792cf299 | |||
ff9265f721 | |||
8d61314852 | |||
1ce6ae8727 | |||
dec5dbc0d2 | |||
4e32dad1ea | |||
127ca7582f | |||
b98993f84b | |||
e35f074b66 | |||
ba3d13d56c | |||
ead67887ab | |||
437f27f107 | |||
8d41a8e98d | |||
7e6ab015a6 | |||
f8bc3a5081 | |||
dd1a93ee0e | |||
093ae39e61 | |||
cac58808f0 | |||
a063f10778 | |||
3cf3aa63f6 | |||
011dd5574f | |||
365911286b | |||
fe5347aa86 | |||
f22c8a72cd | |||
eeb522fe7d | |||
f9e40b209a | |||
20635ea3d6 | |||
6cefd9c3e7 | |||
7062705d6f | |||
58b994e043 | |||
640ff36fa2 | |||
39ec5242d7 | |||
1c50210e61 | |||
a1ffda0151 | |||
fd15348551 | |||
989c99c550 | |||
bcf97b1474 | |||
67abbed66a | |||
eb01e91e13 | |||
12ceb9e0bc | |||
ecf03f90aa | |||
1747414a57 | |||
3a02f16c6e | |||
a6ee337ed0 | |||
559f535257 | |||
2952ccc7fd | |||
a0243fa569 | |||
789b9168ad | |||
7c29cb62ef | |||
f81ca1888d | |||
ed02e0f4d6 | |||
0a83f21af5 | |||
23a3c145ed | |||
4184c6c208 | |||
29c28b1841 | |||
de48fb4077 | |||
bcd79c5882 | |||
b8c513aa2b | |||
ad67f4ef18 | |||
2c0bcfc0ec | |||
0ba1072d54 | |||
f7fe855274 | |||
449738414b | |||
a34842585d | |||
eb882c2c46 | |||
ca65c6bd8f | |||
f97173e9e7 | |||
8fc1b0c856 | |||
cabd7c4e64 | |||
f8540dc78c | |||
b03d271f85 | |||
3770adb7d3 | |||
7fdf19ca22 | |||
4e776adb03 | |||
26db946392 | |||
d102c142b9 | |||
f7989541b9 | |||
b7f0ce18b3 | |||
e1dfbfe3b0 | |||
786d129452 | |||
a37a8e8fcd | |||
355989c278 | |||
af3dee95de | |||
70a6bd6a01 | |||
4afb0acc84 | |||
9afc143801 | |||
8e4943df65 | |||
9b3bd8343d | |||
ee4f83ddba | |||
c326998381 | |||
239a011e60 | |||
5ffe118159 | |||
6f07849e1d | |||
dbe5c62d11 | |||
199db01eaf | |||
a3c46c8f67 | |||
66a68d6180 | |||
be1128a886 | |||
d41a5a65a2 | |||
d5cab938ee | |||
9dddfb65f0 | |||
6bd5976d90 | |||
b3385bf901 | |||
bba268b5e2 | |||
70c98b6901 | |||
2d3b7fea2e | |||
3bdf1c9a00 | |||
a52665ea80 | |||
3d943d49e6 | |||
6ca8ba9231 | |||
75d685ae6c | |||
7b2ef9aec2 | |||
efe666b284 | |||
ca8af5047a | |||
cdc0b0d628 | |||
87e28b70fd | |||
b96f464e39 | |||
bca68986f3 | |||
272ac49872 | |||
5f05ca5ac6 | |||
7872b3ec55 | |||
27a0aebd12 | |||
366490516e | |||
9a92646d4d | |||
b002c49dac | |||
3f4ec9ba80 | |||
0290a5eacd | |||
744734a6a1 | |||
29f662f87c | |||
af21f9f10c | |||
efdc99b9d1 | |||
4458e63c1a | |||
01069ed583 | |||
0fc770bbb1 | |||
dfb79ef96e | |||
7c3ddf904c | |||
c3d73236e0 | |||
cfbcf0947a | |||
fcfba7f5e1 | |||
f4f9fabfd3 | |||
25208915eb | |||
75f4a39ef2 | |||
f9f4d93191 | |||
69050f7a56 | |||
1743919cd4 | |||
131328b42c | |||
ad3b605148 | |||
f32e225fa6 | |||
52e0845fc5 | |||
daf1a0a4bc | |||
bc8978182e |
@ -1,14 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
@ -129,19 +129,19 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
|
||||
Value = 5000m
|
||||
BidAsk = new BidAsk(5000m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
|
||||
Value = 4500m
|
||||
BidAsk = new BidAsk(4500m)
|
||||
});
|
||||
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
|
||||
{
|
||||
Exchange = "coinaverage",
|
||||
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
|
||||
Value = 500m
|
||||
BidAsk = new BidAsk(500m)
|
||||
});
|
||||
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
@ -15,7 +16,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
Client = new ChargeClient(new Uri(url), network);
|
||||
Client = (ChargeClient)LightningClientFactory.CreateClient(url, network);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
public ChargeClient Client { get; set; }
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.6-sdk-2.1.101-stretch
|
||||
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
27
BTCPayServer.Tests/Lnd/LndMockTester.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
public class LndMockTester
|
||||
{
|
||||
private ServerTester _Parent;
|
||||
|
||||
public LndMockTester(ServerTester serverTester, string environmentName, string defaultValue, string defaultHost, Network network)
|
||||
{
|
||||
this._Parent = serverTester;
|
||||
var url = serverTester.GetEnvironment(environmentName, defaultValue);
|
||||
|
||||
Swagger = new LndSwaggerClient(new LndRestSettings(new Uri(url)) { AllowInsecure = true });
|
||||
Client = new LndInvoiceClient(Swagger);
|
||||
P2PHost = _Parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
public LndSwaggerClient Swagger { get; set; }
|
||||
public LndInvoiceClient Client { get; set; }
|
||||
public string P2PHost { get; }
|
||||
}
|
||||
}
|
226
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
226
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NBitpayClient;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests.Lnd
|
||||
{
|
||||
// this depends for now on `docker-compose up devlnd`
|
||||
public class UnitTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public UnitTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
initializeEnvironment();
|
||||
|
||||
MerchantLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53280")) { AllowInsecure = true });
|
||||
InvoiceClient = new LndInvoiceClient(MerchantLnd);
|
||||
|
||||
CustomerLnd = new LndSwaggerClient(new LndRestSettings(new Uri("https://127.0.0.1:53281")) { AllowInsecure = true });
|
||||
}
|
||||
|
||||
private LndSwaggerClient MerchantLnd { get; set; }
|
||||
private LndInvoiceClient InvoiceClient { get; set; }
|
||||
|
||||
private LndSwaggerClient CustomerLnd { get; set; }
|
||||
|
||||
[Fact]
|
||||
public async Task GetInfo()
|
||||
{
|
||||
var res = await InvoiceClient.GetInfo();
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateInvoice()
|
||||
{
|
||||
var res = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
output.WriteLine("Result: " + res.ToJson());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetInvoice()
|
||||
{
|
||||
var createInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var getInvoice = await InvoiceClient.GetInvoice(createInvoice.Id);
|
||||
|
||||
Assert.Equal(createInvoice.BOLT11, getInvoice.BOLT11);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play()
|
||||
{
|
||||
var seq = new System.Buffers.ReadOnlySequence<byte>(new ReadOnlyMemory<byte>(new byte[1000]));
|
||||
var seq2 = seq.Slice(3);
|
||||
var pos = seq2.GetPosition(0);
|
||||
}
|
||||
|
||||
// integration tests
|
||||
[Fact]
|
||||
public async Task TestWaitListenInvoice()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
var merchantInvoice2 = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
var waitToken = default(CancellationToken);
|
||||
var listener = await InvoiceClient.Listen(waitToken);
|
||||
var waitTask = listener.WaitInvoice(waitToken);
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await waitTask;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask2 = listener.WaitInvoice(waitToken);
|
||||
|
||||
payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice2.BOLT11
|
||||
});
|
||||
|
||||
invoice = await waitTask2;
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
|
||||
var waitTask3 = listener.WaitInvoice(waitToken);
|
||||
await Task.Delay(100);
|
||||
listener.Dispose();
|
||||
Assert.Throws<TaskCanceledException>(()=> waitTask3.GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateLndInvoiceAndPay()
|
||||
{
|
||||
var merchantInvoice = await InvoiceClient.CreateInvoice(10000, "Hello world", TimeSpan.FromSeconds(3600));
|
||||
|
||||
await EnsureLightningChannelAsync();
|
||||
var payResponse = await CustomerLnd.SendPaymentSyncAsync(new LnrpcSendRequest
|
||||
{
|
||||
Payment_request = merchantInvoice.BOLT11
|
||||
});
|
||||
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
}
|
||||
|
||||
|
||||
public async Task<LnrpcChannel> EnsureLightningChannelAsync()
|
||||
{
|
||||
var merchantInfo = await WaitLNSynched();
|
||||
var merchantNodeAddress = new LnrpcLightningAddress
|
||||
{
|
||||
Pubkey = merchantInfo.NodeId,
|
||||
Host = "merchant_lnd:9735"
|
||||
};
|
||||
|
||||
while (true)
|
||||
{
|
||||
// if channel is pending generate blocks until confirmed
|
||||
var pendingResponse = await CustomerLnd.PendingChannelsAsync();
|
||||
if (pendingResponse.Pending_open_channels?
|
||||
.Any(a => a.Channel?.Remote_node_pub == merchantNodeAddress.Pubkey) == true)
|
||||
{
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if channel is established
|
||||
var chanResponse = await CustomerLnd.ListChannelsAsync(null, null, null, null);
|
||||
LnrpcChannel channelToMerchant = null;
|
||||
if (chanResponse != null && chanResponse.Channels != null)
|
||||
{
|
||||
channelToMerchant = chanResponse.Channels
|
||||
.Where(a => a.Remote_pubkey == merchantNodeAddress.Pubkey)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (channelToMerchant == null)
|
||||
{
|
||||
// create new channel
|
||||
var isConnected = await CustomerLnd.ListPeersAsync();
|
||||
if (isConnected.Peers == null ||
|
||||
!isConnected.Peers.Any(a => a.Pub_key == merchantInfo.NodeId))
|
||||
{
|
||||
var connectResp = await CustomerLnd.ConnectPeerAsync(new LnrpcConnectPeerRequest
|
||||
{
|
||||
Addr = merchantNodeAddress
|
||||
});
|
||||
}
|
||||
|
||||
var addressResponse = await CustomerLnd.NewWitnessAddressAsync();
|
||||
var address = BitcoinAddress.Create(addressResponse.Address, Network.RegTest);
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.2m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
|
||||
var channelReq = new LnrpcOpenChannelRequest
|
||||
{
|
||||
Local_funding_amount = 16777215.ToString(CultureInfo.InvariantCulture),
|
||||
Node_pubkey_string = merchantInfo.NodeId
|
||||
};
|
||||
var channelResp = await CustomerLnd.OpenChannelSyncAsync(channelReq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// channel exists, return it
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
return channelToMerchant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynched()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await InvoiceClient.GetInfo();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
private void initializeEnvironment()
|
||||
{
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
var var = Environment.GetEnvironmentVariable(variable);
|
||||
return String.IsNullOrEmpty(var) ? defaultValue : var;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using Xunit;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -35,9 +36,9 @@ namespace BTCPayServer.Tests
|
||||
rules.ToString());
|
||||
var tests = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
|
||||
};
|
||||
@ -81,9 +82,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var tests2 = new[]
|
||||
{
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(Pair: "BTC_USD", Expected: "gdax(BTC_USD)", ExpectedExchangeRates: "gdax(BTC_USD)"),
|
||||
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
|
||||
(Pair: "DOGE_USD", Expected: "bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),gdax(BTC_USD)"),
|
||||
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
|
||||
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
|
||||
};
|
||||
@ -94,12 +95,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
|
||||
}
|
||||
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
|
||||
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
|
||||
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
|
||||
rule2.Reevaluate();
|
||||
Assert.True(rule2.HasError);
|
||||
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
|
||||
rule2.Reevaluate();
|
||||
Assert.False(rule2.HasError);
|
||||
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
|
||||
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
|
||||
@ -124,11 +125,55 @@ namespace BTCPayServer.Tests
|
||||
// Test inverse
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
|
||||
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
|
||||
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
|
||||
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
|
||||
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
|
||||
////////
|
||||
|
||||
// Make sure kraken is not converted to CurrencyPair
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
|
||||
// Make sure can handle pairs
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("(6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(6000m, rule2.Value.Value);
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
|
||||
Assert.Equal(1m / 6100m, rule2.Value.Value);
|
||||
|
||||
// Make sure the inverse has more priority than X_X or CDNT_X
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("EUR_CDNT = 10");
|
||||
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
|
||||
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal("1 / 10", rule2.ToString(false));
|
||||
|
||||
// Make sure an inverse can be solved on an exchange
|
||||
builder = new StringBuilder();
|
||||
builder.AppendLine("X_X = coinaverage(X_X);");
|
||||
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
|
||||
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
|
||||
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
|
||||
Assert.True(rule2.Reevaluate());
|
||||
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ using System.Threading;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -47,10 +49,12 @@ 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;
|
||||
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);
|
||||
CustomerLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = (CLightningRPCClient)LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=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);
|
||||
MerchantCharge = new ChargeTester(this, "TEST_MERCHANTCHARGE", "type=charge;server=https://127.0.0.1:54938/;api-token=foiewnccewuify", "merchant_lightningd", btc);
|
||||
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:53280/", "merchant_lnd", btc);
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
@ -78,41 +82,52 @@ namespace BTCPayServer.Tests
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
public void PrepareLightning()
|
||||
public void PrepareLightning(LightningConnectionType lndBackend)
|
||||
{
|
||||
PrepareLightningAsync().GetAwaiter().GetResult();
|
||||
ILightningInvoiceClient client = MerchantCharge.Client;
|
||||
if (lndBackend == LightningConnectionType.LndREST)
|
||||
client = MerchantLnd.Client;
|
||||
|
||||
PrepareLightningAsync(client).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
||||
private static readonly string[] SKIPPED_STATES =
|
||||
{ "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
|
||||
/// <summary>
|
||||
/// Connect a customer LN node to the merchant LN node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task PrepareLightningAsync()
|
||||
private async Task PrepareLightningAsync(ILightningInvoiceClient client)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var skippedStates = new[] { "ONCHAIN", "CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "FUNDING_SPEND_SEEN" };
|
||||
var channel = (await CustomerLightningD.ListPeersAsync())
|
||||
var merchantInfo = await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
|
||||
var peers = await CustomerLightningD.ListPeersAsync();
|
||||
var filteringToTargetedPeers = peers.Where(a => a.Id == merchantInfo.NodeId);
|
||||
var channel = filteringToTargetedPeers
|
||||
.SelectMany(p => p.Channels)
|
||||
.Where(c => !skippedStates.Contains(c.State ?? ""))
|
||||
.Where(c => !SKIPPED_STATES.Contains(c.State ?? ""))
|
||||
.FirstOrDefault();
|
||||
|
||||
switch (channel?.State)
|
||||
{
|
||||
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));
|
||||
await ExplorerNode.SendToAddressAsync(address, Money.Coins(0.5m));
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
await Task.Delay(1000);
|
||||
await CustomerLightningD.FundChannelAsync(clightning, Money.Satoshis(16777215));
|
||||
|
||||
var merchantNodeInfo = new NodeInfo(merchantInfo.NodeId, merchantInfo.Address, merchantInfo.P2PPort);
|
||||
await CustomerLightningD.ConnectAsync(merchantNodeInfo);
|
||||
await CustomerLightningD.FundChannelAsync(merchantNodeInfo, Money.Satoshis(16777215));
|
||||
break;
|
||||
case "CHANNELD_AWAITING_LOCKIN":
|
||||
ExplorerNode.Generate(1);
|
||||
await WaitLNSynched();
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
@ -122,23 +137,29 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<GetInfoResponse> WaitLNSynched()
|
||||
private async Task<LightningNodeInformation> WaitLNSynched(params ILightningInvoiceClient[] clients)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var merchantInfo = await MerchantCharge.Client.GetInfoAsync();
|
||||
var blockCount = await ExplorerNode.GetBlockCountAsync();
|
||||
if (merchantInfo.BlockHeight != blockCount)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
var synching = clients.Select(c => WaitLNSynchedCore(blockCount, c)).ToArray();
|
||||
await Task.WhenAll(synching);
|
||||
if (synching.All(c => c.Result != null))
|
||||
return synching[0].Result;
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LightningNodeInformation> WaitLNSynchedCore(int blockCount, ILightningInvoiceClient client)
|
||||
{
|
||||
var merchantInfo = await client.GetInfo();
|
||||
if (merchantInfo.BlockHeight == blockCount)
|
||||
{
|
||||
return merchantInfo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SendLightningPayment(Invoice invoice)
|
||||
{
|
||||
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||
@ -152,8 +173,10 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
public CLightningRPCClient CustomerLightningD { get; set; }
|
||||
|
||||
public CLightningRPCClient MerchantLightningD { get; private set; }
|
||||
public ChargeTester MerchantCharge { get; private set; }
|
||||
public LndMockTester MerchantLnd { get; set; }
|
||||
|
||||
internal string GetEnvironment(string variable, string defaultValue)
|
||||
{
|
||||
@ -185,99 +208,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
class MockHttpRequest : HttpRequest
|
||||
{
|
||||
Uri serverUri;
|
||||
public MockHttpRequest(Uri serverUri)
|
||||
{
|
||||
this.serverUri = serverUri;
|
||||
}
|
||||
public override HttpContext HttpContext => throw new NotImplementedException();
|
||||
|
||||
public override string Method
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Scheme
|
||||
{
|
||||
get => serverUri.Scheme;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override bool IsHttps
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override HostString Host
|
||||
{
|
||||
get => new HostString(serverUri.Host, serverUri.Port);
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString PathBase
|
||||
{
|
||||
get => "";
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override PathString Path
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override QueryString QueryString
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override IQueryCollection Query
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string Protocol
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override IHeaderDictionary Headers => throw new NotImplementedException();
|
||||
|
||||
public override IRequestCookieCollection Cookies
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override long? ContentLength
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override string ContentType
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
public override Stream Body
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override bool HasFormContentType => throw new NotImplementedException();
|
||||
|
||||
public override IFormCollection Form
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public BTCPayServerTester PayTester
|
||||
{
|
||||
get; set;
|
||||
|
@ -117,7 +117,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
@ -126,15 +126,24 @@ namespace BTCPayServer.Tests
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" + parent.MerchantLightningD.Address.AbsoluteUri;
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = connectionType == LightningConnectionType.Charge ? parent.MerchantCharge.Client.Uri.AbsoluteUri :
|
||||
connectionType == LightningConnectionType.CLightning ? parent.MerchantLightningD.Address.AbsoluteUri
|
||||
: throw new NotSupportedException(connectionType.ToString()),
|
||||
ConnectionString = connectionString,
|
||||
SkipPortTest = true
|
||||
}, "save", "BTC");
|
||||
if (storeController.ModelState.ErrorCount != 0)
|
||||
Assert.False(true, storeController.ModelState.FirstOrDefault().Value.Errors[0].ErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,9 @@ using BTCPayServer.Services.Stores;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Validation;
|
||||
using ExchangeSharp;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -48,6 +50,27 @@ namespace BTCPayServer.Tests
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanHandleUriValidation()
|
||||
{
|
||||
var attribute = new UriAttribute();
|
||||
Assert.True(attribute.IsValid("http://localhost"));
|
||||
Assert.True(attribute.IsValid("http://localhost:1234"));
|
||||
Assert.True(attribute.IsValid("https://localhost"));
|
||||
Assert.True(attribute.IsValid("https://127.0.0.1"));
|
||||
Assert.True(attribute.IsValid("http://127.0.0.1"));
|
||||
Assert.True(attribute.IsValid("http://127.0.0.1:1234"));
|
||||
Assert.True(attribute.IsValid("http://gozo.com"));
|
||||
Assert.True(attribute.IsValid("https://gozo.com"));
|
||||
Assert.True(attribute.IsValid("https://gozo.com:1234"));
|
||||
Assert.True(attribute.IsValid("https://gozo.com:1234/test.css"));
|
||||
Assert.True(attribute.IsValid("https://gozo.com:1234/test.png"));
|
||||
Assert.False(attribute.IsValid("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud e"));
|
||||
Assert.False(attribute.IsValid(2));
|
||||
Assert.False(attribute.IsValid("http://"));
|
||||
Assert.False(attribute.IsValid("httpdsadsa.com"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateCryptoDue2()
|
||||
{
|
||||
@ -229,6 +252,89 @@ namespace BTCPayServer.Tests
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAcceptInvoiceWithTolerance()
|
||||
{
|
||||
var entity = new InvoiceEntity();
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 10;
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.99m), accounting.MinimumTotalDue);
|
||||
|
||||
entity.PaymentTolerance = 100;
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Satoshis(1), accounting.MinimumTotalDue);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanAcceptInvoiceWithTolerance2()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
// Set tolerance to 50%
|
||||
var stores = user.GetController<StoresController>();
|
||||
var vm = Assert.IsType<StoreViewModel>(Assert.IsType<ViewResult>(stores.UpdateStore()).Model);
|
||||
Assert.Equal(0.0, vm.PaymentTolerance);
|
||||
vm.PaymentTolerance = 50.0;
|
||||
Assert.IsType<RedirectToActionResult>(stores.UpdateStore(vm).Result);
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
// Pays 75%
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Satoshis((decimal)invoice.BtcDue.Satoshi * 0.75m));
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("paid", localInvoice.Status);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundupCurrenciesCorrectly()
|
||||
{
|
||||
foreach (var test in new[]
|
||||
{
|
||||
(0.0005m, "$0.0005 (USD)"),
|
||||
(0.001m, "$0.001 (USD)"),
|
||||
(0.01m, "$0.01 (USD)"),
|
||||
(0.1m, "$0.10 (USD)"),
|
||||
})
|
||||
{
|
||||
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayUsingBIP70()
|
||||
{
|
||||
@ -238,10 +344,11 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -303,14 +410,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "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()
|
||||
{
|
||||
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
ConnectionString = "type=charge;server=" + tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
// Make sure old connection string format does not work
|
||||
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||
{
|
||||
ConnectionString = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||
}, "save", "BTC").GetAwaiter().GetResult());
|
||||
|
||||
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||
@ -322,106 +435,148 @@ namespace BTCPayServer.Tests
|
||||
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())
|
||||
Assert.True(LightningConnectionString.TryParse("/test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
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());
|
||||
});
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal(i == 0, conn.IsLegacy);
|
||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=clightning;server=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", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=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.True(LightningConnectionString.TryParse("http://api-token:bbb@test/a", true, out conn));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
if (i == 1)
|
||||
Assert.True(LightningConnectionString.TryParse(conn.ToString(), false, out conn));
|
||||
Assert.Equal("type=charge;server=http://test/a;api-token=bbb", conn.ToString());
|
||||
}
|
||||
|
||||
Assert.False(LightningConnectionString.TryParse("lol://aaa:bbb@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("https://test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("unix://dwewoi:dwdwqd@test/a", true, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("tcp://test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=http://aaa:bbb@test/a;unk=lol", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=charge", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=tcp://aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=/aaa:bbb@test/a", false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse("type=clightning;server=unix://aaa:bbb@test/a", false, out conn));
|
||||
Assert.False(LightningConnectionString.TryParse("type=clightning;server=wtf://aaa:bbb@test/a", false, out conn));
|
||||
|
||||
var macaroon = "0201036c6e640247030a10b0dbbde28f009f83d330bde05075ca251201301a160a0761646472657373120472656164120577726974651a170a08696e766f6963657312047265616412057772697465000006200ae088692e67cf14e767c3d2a4a67ce489150bf810654ff980e1b7a7e263d5e8";
|
||||
|
||||
var certthumbprint = "c51bb1d402306d0da00e85581b32aa56166bcbab7eb888ff925d7167eb436d06";
|
||||
|
||||
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
|
||||
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
|
||||
|
||||
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
|
||||
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
|
||||
|
||||
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
|
||||
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
|
||||
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
|
||||
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
|
||||
Assert.Equal(conn2.ToString(), conn.ToString());
|
||||
Assert.Equal(lndUri, conn.ToString());
|
||||
Assert.Equal(LightningConnectionType.LndREST, conn.ConnectionType);
|
||||
Assert.Equal(macaroon, Encoders.Hex.EncodeData(conn.Macaroon));
|
||||
Assert.Equal(certthumbprint.Replace(":", "", StringComparison.OrdinalIgnoreCase).ToLowerInvariant(), Encoders.Hex.EncodeData(conn.CertificateThumbprint));
|
||||
Assert.True(certificateHash.SequenceEqual(conn.CertificateThumbprint));
|
||||
|
||||
// AllowInsecure can be set to allow http
|
||||
Assert.False(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=false", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
Assert.True(LightningConnectionString.TryParse($"type=lnd-rest;server=http://127.0.0.1:53280/;macaroon={macaroon};allowinsecure=true", false, out conn2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPayment()
|
||||
public void CanSendLightningPaymentCLightning()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.CLightning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentCharge()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.Charge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanSendLightningPaymentLnd()
|
||||
{
|
||||
ProcessLightningPayment(LightningConnectionType.LndREST);
|
||||
}
|
||||
|
||||
void ProcessLightningPayment(LightningConnectionType type)
|
||||
{
|
||||
// For easier debugging and testing
|
||||
// LightningLikePaymentHandler.LIGHTNING_TIMEOUT = int.MaxValue;
|
||||
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
tester.PrepareLightning();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.Charge);
|
||||
user.RegisterLightningNode("BTC", type);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
tester.PrepareLightning(type);
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
|
||||
|
||||
Task.WaitAll(Enumerable.Range(0, 5)
|
||||
.Select(_ => CanSendLightningPaymentCore(tester, user))
|
||||
@ -431,10 +586,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(RandomUtils.GetUInt32() % 5));
|
||||
// TODO: If this parameter is less than 1 second we start having concurrency problems
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(1000));
|
||||
//
|
||||
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Price = 0.01,
|
||||
Price = 0.01m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -487,7 +645,7 @@ namespace BTCPayServer.Tests
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0,
|
||||
Price = 5.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -531,6 +689,53 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanListInvoices()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||
});
|
||||
|
||||
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"storeid:{acc.StoreId}");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"storeid:blah");
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"{invoice.Id}");
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"exceptionstatus:paidPartial");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"exceptionstatus:paidOver");
|
||||
AssertSearchInvoice(acc, true, invoice.Id, $"unusual:true");
|
||||
AssertSearchInvoice(acc, false, invoice.Id, $"unusual:false");
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
|
||||
{
|
||||
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
|
||||
Assert.Equal(expected, result.Invoices.Any(i => i.InvoiceId == invoiceId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRBFPayment()
|
||||
{
|
||||
@ -542,7 +747,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD"
|
||||
}, Facade.Merchant);
|
||||
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
|
||||
@ -618,8 +823,30 @@ namespace BTCPayServer.Tests
|
||||
Assert.False(user.BitPay.TestAccess(Facade.Merchant));
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
|
||||
// Test request pairing code client side
|
||||
var storeController = user.GetController<StoresController>();
|
||||
storeController.CreateToken(new CreateTokenViewModel()
|
||||
{
|
||||
Facade = Facade.Merchant.ToString(),
|
||||
Label = "test2",
|
||||
StoreId = user.StoreId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.NotNull(storeController.GeneratedPairingCode);
|
||||
|
||||
|
||||
var k = new Key();
|
||||
var bitpay = new Bitpay(k, tester.PayTester.ServerUri);
|
||||
bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait();
|
||||
Assert.True(bitpay.TestAccess(Facade.Merchant));
|
||||
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
|
||||
// Same with new instance
|
||||
bitpay = new Bitpay(k, tester.PayTester.ServerUri);
|
||||
Assert.True(bitpay.TestAccess(Facade.Merchant));
|
||||
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
|
||||
|
||||
// Can generate API Key
|
||||
var repo = tester.PayTester.GetService<TokenRepository>();
|
||||
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
@ -642,7 +869,7 @@ namespace BTCPayServer.Tests
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
|
||||
var invoice = new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD"
|
||||
};
|
||||
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
|
||||
@ -684,7 +911,7 @@ namespace BTCPayServer.Tests
|
||||
storeController.Rates(vm).Wait();
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -708,7 +935,7 @@ namespace BTCPayServer.Tests
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -726,7 +953,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -761,6 +988,11 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||
var cashCow = tester.LTCExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.1m);
|
||||
@ -782,7 +1014,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
|
||||
Assert.Null(invoice.BitcoinAddress);
|
||||
Assert.NotEqual(1.0, invoice.Rate);
|
||||
Assert.NotEqual(1.0m, invoice.Rate);
|
||||
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
||||
|
||||
@ -869,7 +1101,7 @@ namespace BTCPayServer.Tests
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -895,13 +1127,23 @@ namespace BTCPayServer.Tests
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("BTC", checkout.CryptoCode);
|
||||
|
||||
Assert.Single(invoice.PaymentCodes);
|
||||
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||
Assert.Single(invoice.SupportedTransactionCurrencies);
|
||||
Assert.Single(invoice.PaymentSubtotals);
|
||||
Assert.Single(invoice.PaymentTotals);
|
||||
Assert.True(invoice.PaymentCodes.ContainsKey("BTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("BTC"));
|
||||
//////////////////////
|
||||
|
||||
// Retry now with LTC enabled
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -943,6 +1185,18 @@ namespace BTCPayServer.Tests
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
|
||||
|
||||
Assert.Equal(2, invoice.PaymentCodes.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count());
|
||||
Assert.Equal(2, invoice.PaymentSubtotals.Count());
|
||||
Assert.Equal(2, invoice.PaymentTotals.Count());
|
||||
Assert.True(invoice.PaymentCodes.ContainsKey("LTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC"));
|
||||
Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled);
|
||||
Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC"));
|
||||
Assert.True(invoice.PaymentTotals.ContainsKey("LTC"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1023,7 +1277,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 1.5,
|
||||
Price = 1.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1036,7 +1290,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.5,
|
||||
Price = 5.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1083,9 +1337,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
|
||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
var invoice = user.BitPay.GetInvoices().First();
|
||||
Assert.Equal(10.00, invoice.Price);
|
||||
Assert.Equal(10.00m, invoice.Price);
|
||||
Assert.Equal("CAD", invoice.Currency);
|
||||
Assert.Equal("orange", invoice.ItemDesc);
|
||||
}
|
||||
@ -1107,8 +1361,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.PointOfSale.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
Assert.Equal(nameof(apps.UpdatePointOfSale), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
@ -1136,7 +1391,7 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1146,6 +1401,8 @@ namespace BTCPayServer.Tests
|
||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
|
||||
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
|
||||
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
|
||||
Eventually(() =>
|
||||
{
|
||||
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
@ -1241,12 +1498,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("complete", localInvoice.Status);
|
||||
Assert.NotEqual(0.0, localInvoice.Rate);
|
||||
Assert.NotEqual(0.0m, localInvoice.Rate);
|
||||
});
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Price = 5000.0m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1285,10 +1542,10 @@ namespace BTCPayServer.Tests
|
||||
var quadri = new QuadrigacxRateProvider();
|
||||
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
|
||||
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
|
||||
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
|
||||
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
|
||||
}
|
||||
|
||||
@ -1297,7 +1554,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
|
||||
|
||||
foreach (var result in factory
|
||||
.DirectProviders
|
||||
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
|
||||
@ -1309,11 +1566,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
|
||||
|
||||
// This check if the currency pair is using right currency pair
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => ( e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
|
||||
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
|
||||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
|
||||
&& e.Value > 1.0m // 1BTC will always be more than 1USD
|
||||
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1356,7 +1613,6 @@ namespace BTCPayServer.Tests
|
||||
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
|
||||
|
||||
var factory = CreateBTCPayRateFactory(provider);
|
||||
factory.DirectProviders.Clear();
|
||||
factory.CacheSpan = TimeSpan.FromSeconds(10);
|
||||
|
||||
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
|
||||
|
48
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
48
BTCPayServer.Tests/UnitTestPeusa.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using NBitcoin;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -17,9 +17,10 @@ services:
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
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/
|
||||
TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc"
|
||||
TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc"
|
||||
TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true"
|
||||
TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true"
|
||||
TESTS_INCONTAINER: "true"
|
||||
expose:
|
||||
- "80"
|
||||
@ -44,9 +45,25 @@ services:
|
||||
- customer_lightningd
|
||||
- merchant_lightningd
|
||||
- lightning-charged
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
devlnd:
|
||||
image: nicolasdorier/docker-bitcoin:0.16.0
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- nbxplorer
|
||||
- postgres
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
|
||||
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.2.2
|
||||
image: nicolasdorier/nbxplorer:1.0.2.8
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -80,8 +97,13 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawtx=tcp://0.0.0.0:28332
|
||||
zmqpubrawblock=tcp://0.0.0.0:28332
|
||||
zmqpubrawtxlock=tcp://0.0.0.0:28332
|
||||
zmqpubhashblock=tcp://0.0.0.0:28332
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "28332:28332"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
@ -89,14 +111,15 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
network=regtest
|
||||
ipaddr=customer_lightningd
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
ports:
|
||||
@ -111,7 +134,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.3.9
|
||||
image: shesek/lightning-charge:0.3.15
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
API_TOKEN: foiewnccewuify
|
||||
@ -130,13 +153,14 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
||||
image: nicolasdorier/clightning:v0.6-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
ipaddr=merchant_lightningd
|
||||
bind-addr=0.0.0.0
|
||||
announce-addr=merchant_lightningd
|
||||
network=regtest
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
@ -175,8 +199,59 @@ services:
|
||||
expose:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=merchant_lnd:9735
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53280:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
- "merchant_lnd_datadir:/data"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:0.4.2.0
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
LND_ENVIRONMENT: "regtest"
|
||||
LND_EXTRA_ARGS: |
|
||||
restlisten=0.0.0.0:8080
|
||||
bitcoin.node=bitcoind
|
||||
bitcoind.rpchost=bitcoind:43782
|
||||
bitcoind.zmqpath=tcp://bitcoind:28332
|
||||
externalip=customer_lnd:10009
|
||||
no-macaroons=1
|
||||
debuglevel=debug
|
||||
noencryptwallet=1
|
||||
ports:
|
||||
- "53281:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
volumes:
|
||||
- "customer_lnd_datadir:/root/.lnd"
|
||||
- "bitcoin_datadir:/deps/.bitcoin"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
volumes:
|
||||
bitcoin_datadir:
|
||||
customer_lightningd_datadir:
|
||||
merchant_lightningd_datadir:
|
||||
lightning_charge_datadir:
|
||||
customer_lnd_datadir:
|
||||
merchant_lnd_datadir:
|
||||
|
@ -44,6 +44,7 @@ namespace BTCPayServer
|
||||
public string CryptoCode { get; internal set; }
|
||||
public string BlockExplorerLink { get; internal set; }
|
||||
public string UriScheme { get; internal set; }
|
||||
public Money MinFee { get; internal set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
|
29
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
29
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinGold()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.bitcoingold.org/insight/tx/{0}/" : "https://test-explorer.bitcoingold.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoingold",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = bitfinex(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btg-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -20,10 +20,15 @@ namespace BTCPayServer
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateRules = new[] { "DOGE_X = bittrex(DOGE_BTC) * BTC_X" },
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"DOGE_X = DOGE_BTC * BTC_X",
|
||||
"DOGE_BTC = bittrex(DOGE_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'"),
|
||||
MinFee = Money.Coins(1m)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
34
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "feathercoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FTC_X = FTC_BTC * BTC_X",
|
||||
"FTC_BTC = bittrex(FTC_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/feathercoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
33
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitGroestlcoin()
|
||||
{
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("GRS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/grs/tx.dws?{0}.htm" : "https://chainz.cryptoid.info/grs-test/tx.dws?{0}.htm",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "groestlcoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"GRS_X = GRS_BTC * BTC_X",
|
||||
"GRS_BTC = bittrex(GRS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/groestlcoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("17'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitMonacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("MONA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://mona.insight.monaco-ex.org/insight/tx/{0}" : "https://testnet-mona.insight.monaco-ex.org/insight/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "monacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"MONA_X = MONA_BTC * BTC_X",
|
||||
"MONA_BTC = zaif(MONA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/monacoin.png",
|
||||
LightningImagePath = "imlegacy/mona-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("22'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitPolis()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "polis",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"POLIS_X = POLIS_BTC * BTC_X",
|
||||
"POLIS_BTC = cryptopia(POLIS_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/polis.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/ufo/tx.dws?{0}" : "https://chainz.cryptoid.info/ufo/tx.dws?{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "ufo",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"UFO_X = UFO_BTC * BTC_X",
|
||||
"UFO_BTC = coinexchange(UFO_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/ufo.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("202'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,12 @@ namespace BTCPayServer
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
//InitUfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.2.4</Version>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.2.40</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -30,38 +30,35 @@
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.4" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.20" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
|
||||
<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" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.12" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.2" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="YamlDotNet" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -77,11 +77,16 @@ namespace BTCPayServer.Configuration
|
||||
var lightning = conf.GetOrDefault<string>($"{net.CryptoCode}.lightning", string.Empty);
|
||||
if(lightning.Length != 0)
|
||||
{
|
||||
if(!LightningConnectionString.TryParse(lightning, out var connectionString, out var error))
|
||||
if(!LightningConnectionString.TryParse(lightning, true, 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/)");
|
||||
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
|
||||
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
|
||||
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
|
||||
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaron=abf239...;certthumbprint=2abdf302...'");
|
||||
}
|
||||
if(connectionString.IsLegacy)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
|
||||
}
|
||||
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
|
||||
}
|
||||
|
@ -12,7 +12,8 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
|
||||
[BitpayAPIConstraint(true)]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -30,6 +31,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("tokens")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
|
||||
{
|
||||
PairingCodeEntity pairingEntity = null;
|
||||
@ -53,7 +55,7 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
|
||||
@ -77,6 +79,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
new PairingCodeResponse()
|
||||
{
|
||||
Policies = new Newtonsoft.Json.Linq.JArray(),
|
||||
PairingCode = pairingEntity.Id,
|
||||
PairingExpiration = pairingEntity.Expiration,
|
||||
DateCreated = pairingEntity.CreatedTime,
|
||||
|
@ -17,6 +17,8 @@ using YamlDotNet.RepresentationModel;
|
||||
using System.IO;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -57,15 +59,56 @@ namespace BTCPayServer.Controllers
|
||||
var app = await GetOwnedApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Currency = settings.Currency,
|
||||
Template = settings.Template
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
{
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
|
||||
var encoder = HtmlEncoder.Default;
|
||||
if (settings.ShowCustomAmount)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example1 = builder.ToString();
|
||||
}
|
||||
try
|
||||
{
|
||||
var items = Parse(settings.Template, settings.Currency);
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
|
||||
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
|
||||
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
|
||||
builder.AppendLine($"</form>");
|
||||
vm.Example2 = builder.ToString();
|
||||
}
|
||||
catch { }
|
||||
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
|
||||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/pos")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
if (_Currencies.GetCurrencyData(vm.Currency) == null)
|
||||
if (_Currencies.GetCurrencyData(vm.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.Currency), "Invalid currency");
|
||||
try
|
||||
{
|
||||
@ -91,7 +134,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdatePointOfSale));
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -102,8 +145,9 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency);
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
@ -162,7 +206,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
string notificationUrl,
|
||||
string redirectUrl,
|
||||
string choiceKey)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
@ -172,12 +223,12 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
double price = 0.0;
|
||||
var price = 0.0m;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
@ -185,20 +236,26 @@ namespace BTCPayServer.Controllers
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = (double)choice.Price.Value;
|
||||
price = choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
|
||||
var store = await GetStore(app);
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = price,
|
||||
BuyerEmail = email,
|
||||
OrderId = orderId,
|
||||
NotificationURL = notificationUrl,
|
||||
RedirectURL = redirectUrl,
|
||||
FullNotifications = true
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
@ -102,9 +102,9 @@ namespace BTCPayServer.Controllers
|
||||
StatusMessage = "Error: You are not owner of this store";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
|
||||
var appData = new AppData() { Id = id };
|
||||
appData.StoreDataId = selectedStore;
|
||||
appData.Name = vm.Name;
|
||||
@ -113,6 +113,9 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
@ -176,24 +179,19 @@ namespace BTCPayServer.Controllers
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Select(us => new
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
Apps = us.StoreData.Apps
|
||||
})
|
||||
.SelectMany(us => us.Apps.Select(app => new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.IsOwner,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id,
|
||||
StoreId = us.StoreId,
|
||||
StoreName = us.StoreName
|
||||
}))
|
||||
.ToArrayAsync();
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,11 +13,14 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[EnableCors("BitpayAPI")]
|
||||
[BitpayAPIConstraint]
|
||||
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
|
||||
public class InvoiceControllerAPI : Controller
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
@ -43,9 +46,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, id);
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
|
@ -78,7 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var payment = PaymentMessage.Load(Request.Body, network.NBitcoinNetwork);
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
|
@ -51,14 +51,17 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
|
||||
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
|
||||
"low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress;
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.Rate = ExchangeRate(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
@ -201,6 +204,12 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (network == null && isDefaultCrypto)
|
||||
{
|
||||
network = _NetworkProvider.GetAll().FirstOrDefault();
|
||||
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
@ -210,6 +219,7 @@ namespace BTCPayServer.Controllers
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
paymentMethodIdStr = paymentMethodId.ToString();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
@ -232,15 +242,16 @@ namespace BTCPayServer.Controllers
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(paymentMethod),
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||
@ -278,14 +289,45 @@ namespace BTCPayServer.Controllers
|
||||
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
||||
}
|
||||
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
return null;
|
||||
|
||||
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency);
|
||||
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
|
||||
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
|
||||
{
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
var provider = currencies.GetNumberFormatInfo(currency, true);
|
||||
var currencyData = currencies.GetCurrencyData(currency, true);
|
||||
var divisibility = currencyData.Divisibility;
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - price) / price) < 0.001m)
|
||||
{
|
||||
price = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
if (divisibility != provider.CurrencyDecimalDigits)
|
||||
{
|
||||
provider = (NumberFormatInfo)provider.Clone();
|
||||
provider.CurrencyDecimalDigits = divisibility;
|
||||
}
|
||||
|
||||
if (currencyData.Crypto)
|
||||
return price.ToString("C", provider);
|
||||
else
|
||||
return price.ToString("C", provider) + $" ({currency})";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -314,7 +356,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.Invoice.Id, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -370,15 +412,20 @@ namespace BTCPayServer.Controllers
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
|
||||
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
|
||||
: r,
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
||||
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
|
||||
ShowCheckout = invoice.Status == "new",
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
@ -415,7 +462,7 @@ namespace BTCPayServer.Controllers
|
||||
var stores = await _StoreRepository.GetStoresByUserId(GetUserId());
|
||||
model.Stores = new SelectList(stores, nameof(StoreData.Id), nameof(StoreData.StoreName), model.StoreId);
|
||||
var store = stores.FirstOrDefault(s => s.Id == model.StoreId);
|
||||
if(store == null)
|
||||
if (store == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.StoreId), "Store not found");
|
||||
}
|
||||
@ -488,8 +535,11 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -98,6 +98,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURL = notificationUri?.AbsoluteUri;
|
||||
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
@ -108,13 +109,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
|
||||
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
|
||||
entity.RedirectURL = null;
|
||||
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
|
||||
await UpdateCLightningConnectionStringIfNeeded(store);
|
||||
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
@ -140,32 +145,26 @@ namespace BTCPayServer.Controllers
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<string> invoiceLogs = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
foreach(var pair in fetchingByCurrencyPair)
|
||||
foreach (var pair in fetchingByCurrencyPair)
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
bool hasError = false;
|
||||
if(rateResult.Errors.Count != 0)
|
||||
invoiceLogs.Add($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
invoiceLogs.Add($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
paymentMethodErrors.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
hasError = true;
|
||||
invoiceLogs.Add($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
}
|
||||
if(rateResult.ExchangeExceptions.Count != 0)
|
||||
if (rateResult.ExchangeExceptions.Count != 0)
|
||||
{
|
||||
foreach(var ex in rateResult.ExchangeExceptions)
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
invoiceLogs.Add($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
}
|
||||
hasError = true;
|
||||
}
|
||||
if(hasError)
|
||||
{
|
||||
paymentMethodErrors.Add($"{pair.Key}: The rule is {rateResult.Rule}");
|
||||
paymentMethodErrors.Add($"{pair.Key}: Evaluated rule is {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,11 +180,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
invoiceLogs.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()})");
|
||||
invoiceLogs.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +192,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach (var error in paymentMethodErrors)
|
||||
foreach (var error in invoiceLogs)
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
}
|
||||
@ -203,13 +202,29 @@ namespace BTCPayServer.Controllers
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, invoiceLogs, _NetworkProvider);
|
||||
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task UpdateCLightningConnectionStringIfNeeded(StoreData store)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
{
|
||||
var lightning = method.GetLightningUrl();
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
needUpdate = true;
|
||||
}
|
||||
}
|
||||
if(needUpdate)
|
||||
await _StoreRepository.UpdateStore(store);
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair, IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreData store)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
@ -276,6 +291,7 @@ namespace BTCPayServer.Controllers
|
||||
return defaultPolicy;
|
||||
var mappings = new Dictionary<string, SpeedPolicy>();
|
||||
mappings.Add("low", SpeedPolicy.LowSpeed);
|
||||
mappings.Add("low-medium", SpeedPolicy.LowMediumSpeed);
|
||||
mappings.Add("medium", SpeedPolicy.MediumSpeed);
|
||||
mappings.Add("high", SpeedPolicy.HighSpeed);
|
||||
if (!mappings.TryGetValue(transactionSpeed, out SpeedPolicy policy))
|
||||
|
@ -36,11 +36,12 @@ namespace BTCPayServer.Controllers
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<IActionResult> GetRates(string currencyPairs, string storeId)
|
||||
{
|
||||
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
|
||||
var result = await GetRates2(currencyPairs, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
var rates = (result as JsonResult)?.Value as Rate[];
|
||||
if (rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
return Json(new DataWrapper<Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
@ -54,8 +55,9 @@ namespace BTCPayServer.Controllers
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
var store = this.HttpContext.GetStoreData();
|
||||
if(store == null || store.Id != storeId)
|
||||
store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
{
|
||||
var result = Json(new BitpayErrorsModel() { Error = "Store not found" });
|
||||
@ -86,7 +88,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = r.Pair.Left,
|
||||
Code = r.Pair.Right,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right)?.Name,
|
||||
CurrencyPair = r.Pair.ToString(),
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Pair.Right, true).Name,
|
||||
Value = r.Value.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
}
|
||||
@ -106,6 +109,14 @@ namespace BTCPayServer.Controllers
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "currencyPair")]
|
||||
public string CurrencyPair
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "code")]
|
||||
public string Code
|
||||
{
|
||||
|
@ -243,10 +243,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(model.Settings.From)
|
||||
|| string.IsNullOrWhiteSpace(model.TestEmail)
|
||||
|| string.IsNullOrWhiteSpace(model.Settings.Login)
|
||||
|| string.IsNullOrWhiteSpace(model.Settings.Server))
|
||||
if(!model.Settings.IsComplete())
|
||||
{
|
||||
model.StatusMessage = "Error: Required fields missing";
|
||||
return View(model);
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -264,7 +265,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
@ -286,11 +287,88 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
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, summary.Status.BitcoinStatus.MinRelayTxFee);
|
||||
var send = new[] { (
|
||||
destination: destinationAddress as IDestination,
|
||||
amount: amountBTC,
|
||||
substractFees: subsctractFeesValue) };
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await hw.GetKeyPath(network, strategy);
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress.Item1);
|
||||
|
||||
if (network.MinFee == null)
|
||||
{
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||
if (network.MinFee > estimatedFee)
|
||||
builder.SendFees(network.MinFee);
|
||||
else
|
||||
builder.SendEstimatedFees(feeRateValue);
|
||||
}
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach (var c in unspentCoins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
|
||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||
|
||||
if (!strategy.Segwit)
|
||||
{
|
||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||
var explorer = _ExplorerProvider.GetExplorerClient(network);
|
||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||
{
|
||||
var tx = (await getTransactionAsync.Op);
|
||||
if (tx == null)
|
||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||
}
|
||||
}
|
||||
|
||||
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null);
|
||||
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
@ -311,7 +389,7 @@ namespace BTCPayServer.Controllers
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
|
||||
finally { hw.Dispose(); }
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
@ -336,8 +414,6 @@ namespace BTCPayServer.Controllers
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
if (!directStrategy.Segwit)
|
||||
return null;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
|
@ -26,16 +26,15 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||
vm.CryptoCode = cryptoCode;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToUri(true)?.AbsoluteUri;
|
||||
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
|
||||
{
|
||||
vm.Url = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
@ -65,7 +64,7 @@ namespace BTCPayServer.Controllers
|
||||
var network = vm.CryptoCode == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCode);
|
||||
|
||||
var internalLightning = GetInternalLighningNode(network.CryptoCode);
|
||||
vm.InternalLightningNode = internalLightning?.ToUri(true)?.AbsoluteUri;
|
||||
vm.InternalLightningNode = internalLightning?.ToString();
|
||||
if (network == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network");
|
||||
@ -74,33 +73,51 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||
if (!string.IsNullOrEmpty(vm.Url))
|
||||
if (!string.IsNullOrEmpty(vm.ConnectionString))
|
||||
{
|
||||
if (!LightningConnectionString.TryParse(vm.Url, out var connectionString, out var error))
|
||||
if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), $"Invalid URL ({error})");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var internalDomain = internalLightning?.ToUri(false)?.DnsSafeHost;
|
||||
bool isLocal = (internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
var internalDomain = internalLightning.BaseUri?.DnsSafeHost;
|
||||
|
||||
bool isInternalNode = connectionString.ConnectionType == LightningConnectionType.CLightning ||
|
||||
connectionString.BaseUri.DnsSafeHost == internalDomain ||
|
||||
isLocal;
|
||||
(internalDomain == "127.0.0.1" || internalDomain == "localhost");
|
||||
|
||||
if (connectionString.BaseUri.Scheme == "http" && !isLocal)
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode || (isInternalNode && !CanUseInternalLightning()))
|
||||
if (!isInternalNode)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if(connectionString.MacaroonFilePath != null)
|
||||
{
|
||||
if(!CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use macaroonfilepath");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.File.Exists(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath file does exist");
|
||||
return View(vm);
|
||||
}
|
||||
if(!System.IO.Path.IsPathRooted(connectionString.MacaroonFilePath))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The macaroonfilepath should be fully rooted");
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
if (isInternalNode && !CanUseInternalLightning())
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Unauthorized url");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Unauthorized url");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -110,6 +127,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
paymentMethod.SetLightningUrl(connectionString);
|
||||
}
|
||||
|
||||
if (command == "save")
|
||||
{
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||
@ -121,7 +139,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (paymentMethod == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
|
||||
return View(vm);
|
||||
}
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
@ -135,7 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
await handler.TestConnection(info, cts.Token);
|
||||
}
|
||||
}
|
||||
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
|
||||
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -53,8 +53,7 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_RateFactory = rateFactory;
|
||||
_Dashboard = dashboard;
|
||||
@ -72,9 +71,7 @@ namespace BTCPayServer.Controllers
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
_CoinAverage = coinAverage;
|
||||
}
|
||||
CoinAverageSettings _CoinAverage;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
@ -316,7 +313,7 @@ namespace BTCPayServer.Controllers
|
||||
Action = "Continue",
|
||||
Title = "Rate rule scripting",
|
||||
Description = scripting ?
|
||||
"This action will mofify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
"This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)"
|
||||
: "This action will delete your rate script. Are you sure to turn off rate rules scripting?",
|
||||
ButtonClass = "btn-primary"
|
||||
});
|
||||
@ -431,6 +428,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -496,6 +494,7 @@ namespace BTCPayServer.Controllers
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
|
||||
if (StoreData.SetStoreBlob(blob))
|
||||
{
|
||||
@ -516,7 +515,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return _CoinAverage.AvailableExchanges
|
||||
return _RateFactory.GetSupportedExchanges()
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
@ -644,11 +643,11 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var stores = await _Repo.GetStoresByUserId(userId);
|
||||
model.Stores = new SelectList(stores.Where(s => s.HasClaim(Policies.CanModifyStoreSettings.Key)), nameof(StoreData.Id), nameof(StoreData.StoreName), storeId);
|
||||
}
|
||||
if (model.Stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of at least one store before pairing";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
if (model.Stores.Count() == 0)
|
||||
{
|
||||
StatusMessage = "Error: You need to be owner of at least one store before pairing";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
}
|
||||
return View(model);
|
||||
}
|
||||
|
@ -121,7 +121,10 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
|
||||
CreatedStoreId = store.Id;
|
||||
StatusMessage = "Store successfully created";
|
||||
return RedirectToAction(nameof(ListStores));
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
storeId = store.Id
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
|
@ -21,7 +21,7 @@ namespace BTCPayServer
|
||||
return false;
|
||||
|
||||
var currency = match.Groups.Last().Value.ToUpperInvariant();
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency);
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency, false);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
v = Math.Round(v, currencyData.Divisibility);
|
||||
|
@ -6,6 +6,11 @@ using System.Threading.Tasks;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.PostgreSql;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using JetBrains.Annotations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -31,20 +36,66 @@ namespace BTCPayServer.Data
|
||||
return new ApplicationDbContext(builder.Options);
|
||||
}
|
||||
|
||||
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
|
||||
{
|
||||
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
|
||||
{
|
||||
builder
|
||||
.Append("CREATE DATABASE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
|
||||
|
||||
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
|
||||
builder
|
||||
.Append(" TEMPLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
|
||||
|
||||
builder
|
||||
.Append(" LC_CTYPE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" LC_COLLATE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
|
||||
|
||||
builder
|
||||
.Append(" ENCODING ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
|
||||
|
||||
if (operation.Tablespace != null)
|
||||
{
|
||||
builder
|
||||
.Append(" TABLESPACE ")
|
||||
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
|
||||
}
|
||||
|
||||
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
|
||||
|
||||
EndStatement(builder, suppressTransaction: true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConfigureBuilder(DbContextOptionsBuilder builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseSqlite(_ConnectionString);
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UseNpgsql(_ConnectionString);
|
||||
builder
|
||||
.UseNpgsql(_ConnectionString)
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
}
|
||||
|
||||
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
|
||||
{
|
||||
if (_Type == DatabaseType.Sqlite)
|
||||
builder.UseMemoryStorage(); //Sql provider does not support multiple workers
|
||||
else if (_Type == DatabaseType.Postgres)
|
||||
builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
builder.UseMemoryStorage();
|
||||
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
|
||||
//if (_Type == DatabaseType.Sqlite)
|
||||
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
|
||||
//else if (_Type == DatabaseType.Postgres)
|
||||
// builder.UsePostgreSqlStorage(_ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +247,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
@ -326,6 +327,10 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(0)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (!RateScripting ||
|
||||
|
@ -72,7 +72,7 @@ namespace BTCPayServer
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Encoders.Base58Check.DecodeData(parts[i]);
|
||||
var data = Network.GetBase58CheckEncoder().DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
@ -80,7 +80,7 @@ namespace BTCPayServer
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
|
||||
var derivationScheme = new BitcoinExtPubKey(Network.GetBase58CheckEncoder().EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
|
@ -8,24 +8,20 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public Models.InvoiceResponse Invoice { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,21 +6,6 @@ using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NBXplorerErrorEvent
|
||||
{
|
||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
||||
{
|
||||
Message = errorMessage;
|
||||
Network = network;
|
||||
}
|
||||
public string Message { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
||||
}
|
||||
}
|
||||
public class NBXplorerStateChangedEvent
|
||||
{
|
||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||
|
@ -36,28 +36,6 @@ namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string Prettify(this TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
|
||||
}
|
||||
if (timeSpan.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
|
||||
}
|
||||
if (timeSpan.Days < 1)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
|
||||
}
|
||||
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
|
||||
}
|
||||
|
||||
private static string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
public static string PrettyPrint(this TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
@ -131,7 +109,7 @@ namespace BTCPayServer
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
@ -163,7 +141,7 @@ namespace BTCPayServer
|
||||
|
||||
public static void AddRange<T>(this HashSet<T> hashSet, IEnumerable<T> items)
|
||||
{
|
||||
foreach(var item in items)
|
||||
foreach (var item in items)
|
||||
{
|
||||
hashSet.Add(item);
|
||||
}
|
||||
@ -179,6 +157,15 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
var waiting = Task.Delay(-1, cancellationToken);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
|
@ -41,6 +41,13 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
}
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
@ -58,10 +65,19 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(ListenForThemeChanges)
|
||||
CreateLoopTask(ListenForThemeChanges),
|
||||
CreateLoopTask(ListenForPoliciesChanges),
|
||||
};
|
||||
}
|
||||
|
||||
async Task ListenForPoliciesChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var data = (await _SettingsRepository.GetSettingAsync<PoliciesSettings>()) ?? new PoliciesSettings();
|
||||
_CssThemeManager.Update(data);
|
||||
await _SettingsRepository.WaitSettingsChanged<PoliciesSettings>(Cancellation);
|
||||
}
|
||||
|
||||
async Task ListenForThemeChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
|
@ -198,7 +198,11 @@ namespace BTCPayServer.HostedServices
|
||||
PosData = dto.PosData,
|
||||
Price = dto.Price,
|
||||
Status = dto.Status,
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) }
|
||||
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
|
||||
PaymentSubtotals = dto.PaymentSubtotals,
|
||||
PaymentTotals = dto.PaymentTotals,
|
||||
AmountPaid = dto.AmountPaid,
|
||||
ExchangeRates = dto.ExchangeRates
|
||||
};
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
@ -207,7 +211,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
notification.Rate = dto.Rate;
|
||||
notification.Url = dto.Url;
|
||||
notification.BTCDue = dto.BTCDue;
|
||||
notification.BTCPaid = dto.BTCPaid;
|
||||
@ -304,8 +308,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.Invoice.Id);
|
||||
List<Task> tasks = new List<Task>();
|
||||
|
||||
// Awaiting this later help make sure invoices should arrive in order
|
||||
tasks.Add(SaveEvent(invoice.Id, e));
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
@ -315,20 +322,22 @@ namespace BTCPayServer.HostedServices
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
e.Name == "invoice_completed" ||
|
||||
e.Name == "invoice_expiredPaidPartial"
|
||||
)
|
||||
await Notify(invoice);
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
tasks.Add(Notify(invoice, e.EventCode, e.Name));
|
||||
}
|
||||
await Task.WhenAll(tasks.ToArray());
|
||||
}));
|
||||
|
||||
|
||||
|
@ -66,8 +66,10 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
if(invoice.ExceptionStatus == "paidPartial")
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@ -78,11 +80,11 @@ namespace BTCPayServer.HostedServices
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
if (accounting.Paid >= accounting.TotalDue)
|
||||
if (accounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@ -91,22 +93,22 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
if (accounting.Paid < accounting.MinimumTotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
if (accounting.Paid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
if (accounting.MinimumTotalDue <= accounting.Paid && accounting.Paid <= accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
context.MarkDirty();
|
||||
@ -118,7 +120,7 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (accounting.Paid < accounting.TotalDue)
|
||||
if (accounting.Paid < accounting.MinimumTotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = accounting.Paid == Money.Zero ? null : "paidPartial";
|
||||
@ -134,17 +136,17 @@ namespace BTCPayServer.HostedServices
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(confirmedAccounting.Paid < accounting.TotalDue))
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.TotalDue)
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -153,9 +155,9 @@ namespace BTCPayServer.HostedServices
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.TotalDue)
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -247,13 +249,13 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
@ -289,7 +291,7 @@ namespace BTCPayServer.HostedServices
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
||||
updateContext.Events.Insert(0, new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
|
@ -192,7 +192,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
status = null;
|
||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
||||
Logs.PayServer.LogError($"{_Network.CryptoCode}: NBXplorer error `{error}`");
|
||||
}
|
||||
|
||||
_Dashboard.Publish(_Network, State, status, error);
|
||||
|
@ -12,7 +12,6 @@ using NBitcoin;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using NBXplorer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -118,7 +117,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
services.AddSingleton<IHostedService, RatesHostedService>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
|
||||
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
@ -138,6 +136,7 @@ namespace BTCPayServer.Hosting
|
||||
// bundling
|
||||
|
||||
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
|
||||
BitpayAuthentication.AddAuthentication(services);
|
||||
|
||||
services.AddBundles();
|
||||
services.AddTransient<BundleOptions>(provider =>
|
||||
|
@ -79,12 +79,13 @@ namespace BTCPayServer.Hosting
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
@ -94,9 +95,9 @@ namespace BTCPayServer.Hosting
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
|
@ -35,7 +35,6 @@ using Hangfire.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
@ -104,10 +103,6 @@ namespace BTCPayServer.Hosting
|
||||
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
|
||||
});
|
||||
});
|
||||
services.Configure<IOptions<ApplicationInsightsServiceOptions>>(o =>
|
||||
{
|
||||
o.Value.DeveloperMode = _Env.IsDevelopment();
|
||||
});
|
||||
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
@ -146,12 +141,8 @@ namespace BTCPayServer.Hosting
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseBrowserLink();
|
||||
}
|
||||
|
||||
//App insight do not that by itself...
|
||||
loggerFactory.AddApplicationInsights(prov, LogLevel.Information);
|
||||
|
||||
app.UsePayServer();
|
||||
app.UseStaticFiles();
|
||||
app.UseAuthentication();
|
||||
|
@ -1,13 +1,14 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Logging.Console.Internal;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions.Internal;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Microsoft.Extensions.Logging.Console.Internal;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
@ -20,19 +21,18 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
|
||||
return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
||||
/// </summary>
|
||||
public class CustomConsoleLogger : ILogger
|
||||
public class CustomerConsoleLogger : ILogger
|
||||
{
|
||||
private static readonly string _loglevelPadding = ": ";
|
||||
private static readonly string _messagePadding;
|
||||
@ -47,19 +47,33 @@ namespace BTCPayServer.Logging
|
||||
[ThreadStatic]
|
||||
private static StringBuilder _logBuilder;
|
||||
|
||||
static CustomConsoleLogger()
|
||||
static CustomerConsoleLogger()
|
||||
{
|
||||
var logLevelString = GetLogLevelString(LogLevel.Information);
|
||||
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
|
||||
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
|
||||
}
|
||||
|
||||
public CustomConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
|
||||
public CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes)
|
||||
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Filter = filter ?? ((category, logLevel) => true);
|
||||
IncludeScopes = includeScopes;
|
||||
}
|
||||
|
||||
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
|
||||
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
|
||||
{
|
||||
}
|
||||
|
||||
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
Name = name;
|
||||
Filter = filter ?? ((category, logLevel) => true);
|
||||
ScopeProvider = scopeProvider;
|
||||
_queueProcessor = loggerProcessor;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
@ -80,7 +94,12 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
set
|
||||
{
|
||||
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
_queueProcessor.Console = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,13 +111,13 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
set
|
||||
{
|
||||
_filter = value ?? throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
}
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
public bool IncludeScopes
|
||||
{
|
||||
get; set;
|
||||
_filter = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
@ -106,6 +125,16 @@ namespace BTCPayServer.Logging
|
||||
get;
|
||||
}
|
||||
|
||||
internal IExternalScopeProvider ScopeProvider
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool DisableColors
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
@ -154,10 +183,7 @@ namespace BTCPayServer.Logging
|
||||
while (lenAfter++ < 18)
|
||||
logBuilder.Append(" ");
|
||||
// scope information
|
||||
if (IncludeScopes)
|
||||
{
|
||||
GetScopeInformation(logBuilder);
|
||||
}
|
||||
GetScopeInformation(logBuilder);
|
||||
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
@ -202,18 +228,15 @@ namespace BTCPayServer.Logging
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
if (logLevel == LogLevel.None)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Filter(Name, logLevel);
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
return ConsoleLogScope.Push(Name, state);
|
||||
}
|
||||
public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
|
||||
|
||||
private static string GetLogLevelString(LogLevel logLevel)
|
||||
{
|
||||
@ -238,6 +261,11 @@ namespace BTCPayServer.Logging
|
||||
|
||||
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
|
||||
{
|
||||
if (DisableColors)
|
||||
{
|
||||
return new ConsoleColors(null, null);
|
||||
}
|
||||
|
||||
// We must explicitly set the background color if we are setting the foreground color,
|
||||
// since just setting one can look bad on the users console.
|
||||
switch (logLevel)
|
||||
@ -259,30 +287,25 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
}
|
||||
|
||||
private void GetScopeInformation(StringBuilder builder)
|
||||
private void GetScopeInformation(StringBuilder stringBuilder)
|
||||
{
|
||||
var current = ConsoleLogScope.Current;
|
||||
string scopeLog = string.Empty;
|
||||
var length = builder.Length;
|
||||
|
||||
while (current != null)
|
||||
var scopeProvider = ScopeProvider;
|
||||
if (scopeProvider != null)
|
||||
{
|
||||
if (length == builder.Length)
|
||||
{
|
||||
scopeLog = $"=> {current}";
|
||||
}
|
||||
else
|
||||
{
|
||||
scopeLog = $"=> {current} ";
|
||||
}
|
||||
var initialLength = stringBuilder.Length;
|
||||
|
||||
builder.Insert(length, scopeLog);
|
||||
current = current.Parent;
|
||||
}
|
||||
if (builder.Length > length)
|
||||
{
|
||||
builder.Insert(length, _messagePadding);
|
||||
builder.AppendLine();
|
||||
scopeProvider.ForEachScope((scope, state) =>
|
||||
{
|
||||
var (builder, length) = state;
|
||||
var first = length == builder.Length;
|
||||
builder.Append(first ? "=> " : " => ").Append(scope);
|
||||
}, (stringBuilder, initialLength));
|
||||
|
||||
if (stringBuilder.Length > initialLength)
|
||||
{
|
||||
stringBuilder.Insert(initialLength, _messagePadding);
|
||||
stringBuilder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,9 +356,9 @@ namespace BTCPayServer.Logging
|
||||
// Start Console message queue processor
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueue,
|
||||
this,
|
||||
default(CancellationToken),
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
state: this,
|
||||
cancellationToken: default(CancellationToken),
|
||||
creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||
|
@ -15,9 +15,14 @@ namespace BTCPayServer.Logging
|
||||
}
|
||||
public static void Configure(ILoggerFactory factory)
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
if (factory == null)
|
||||
Configure(new FuncLoggerFactory(n => NullLogger.Instance));
|
||||
else
|
||||
{
|
||||
Configuration = factory.CreateLogger("Configuration");
|
||||
PayServer = factory.CreateLogger("PayServer");
|
||||
Events = factory.CreateLogger("Events");
|
||||
}
|
||||
}
|
||||
public static ILogger Configuration
|
||||
{
|
||||
|
@ -12,10 +12,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
Id = table.Column<string>(nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -26,21 +26,21 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Email = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
AccessFailedCount = table.Column<int>(nullable: false),
|
||||
ConcurrencyStamp = table.Column<string>(nullable: true),
|
||||
Email = table.Column<string>(maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(nullable: false),
|
||||
LockoutEnabled = table.Column<bool>(nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
|
||||
NormalizedEmail = table.Column<string>(maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(maxLength: 256, nullable: true),
|
||||
PasswordHash = table.Column<string>(nullable: true),
|
||||
PhoneNumber = table.Column<string>(nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
|
||||
SecurityStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SecurityStamp = table.Column<string>(nullable: true),
|
||||
TwoFactorEnabled = table.Column<bool>(nullable: false),
|
||||
UserName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: true)
|
||||
UserName = table.Column<string>(maxLength: 256, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -51,12 +51,12 @@ namespace BTCPayServer.Migrations
|
||||
name: "Stores",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
DerivationStrategy = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SpeedPolicy = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
DerivationStrategy = table.Column<string>(nullable: true),
|
||||
SpeedPolicy = table.Column<int>(nullable: false),
|
||||
StoreCertificate = table.Column<byte[]>(nullable: true),
|
||||
StoreName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreWebsite = table.Column<string>(type: "TEXT", nullable: true)
|
||||
StoreName = table.Column<string>(nullable: true),
|
||||
StoreWebsite = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -67,11 +67,11 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
ClaimType = table.Column<string>(nullable: true),
|
||||
ClaimValue = table.Column<string>(nullable: true),
|
||||
RoleId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -88,11 +88,11 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClaimType = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
ClaimType = table.Column<string>(nullable: true),
|
||||
ClaimValue = table.Column<string>(nullable: true),
|
||||
UserId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -109,10 +109,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "TEXT", nullable: true),
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
ProviderKey = table.Column<string>(nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(nullable: true),
|
||||
UserId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -129,8 +129,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
RoleId = table.Column<string>(type: "TEXT", nullable: false)
|
||||
UserId = table.Column<string>(nullable: false),
|
||||
RoleId = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -153,10 +153,10 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||
UserId = table.Column<string>(nullable: false),
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
Name = table.Column<string>(nullable: false),
|
||||
Value = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -173,15 +173,15 @@ namespace BTCPayServer.Migrations
|
||||
name: "Invoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
Created = table.Column<DateTimeOffset>(nullable: false),
|
||||
CustomerEmail = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExceptionStatus = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ItemCode = table.Column<string>(type: "TEXT", nullable: true),
|
||||
OrderId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Status = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
CustomerEmail = table.Column<string>(nullable: true),
|
||||
ExceptionStatus = table.Column<string>(nullable: true),
|
||||
ItemCode = table.Column<string>(nullable: true),
|
||||
OrderId = table.Column<string>(nullable: true),
|
||||
Status = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -198,9 +198,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "UserStore",
|
||||
columns: table => new
|
||||
{
|
||||
ApplicationUserId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Role = table.Column<string>(type: "TEXT", nullable: true)
|
||||
ApplicationUserId = table.Column<string>(nullable: false),
|
||||
StoreDataId = table.Column<string>(nullable: false),
|
||||
Role = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -223,9 +223,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "Payments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -242,9 +242,9 @@ namespace BTCPayServer.Migrations
|
||||
name: "RefundAddresses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Blob = table.Column<byte[]>(nullable: true),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "Settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Value = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Value = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,8 +12,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "AddressInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Address = table.Column<string>(nullable: false),
|
||||
InvoiceDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -12,13 +12,13 @@ namespace BTCPayServer.Migrations
|
||||
name: "PairedSINData",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
Facade = table.Column<string>(nullable: true),
|
||||
Label = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
PairingTime = table.Column<DateTimeOffset>(nullable: false),
|
||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true)
|
||||
SIN = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@ -29,15 +29,15 @@ namespace BTCPayServer.Migrations
|
||||
name: "PairingCodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Id = table.Column<string>(nullable: false),
|
||||
DateCreated = table.Column<DateTime>(nullable: false),
|
||||
Expiration = table.Column<DateTimeOffset>(nullable: false),
|
||||
Facade = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Label = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SIN = table.Column<string>(type: "TEXT", nullable: true),
|
||||
StoreDataId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
TokenValue = table.Column<string>(type: "TEXT", nullable: true)
|
||||
Facade = table.Column<string>(nullable: true),
|
||||
Label = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
SIN = table.Column<string>(nullable: true),
|
||||
StoreDataId = table.Column<string>(nullable: true),
|
||||
TokenValue = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
|
||||
name: "PendingInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "TEXT", nullable: false)
|
||||
Id = table.Column<string>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -17,8 +17,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "HistoricalAddressInvoices",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceDataId = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
InvoiceDataId = table.Column<string>(nullable: false),
|
||||
Address = table.Column<string>(nullable: false),
|
||||
Assigned = table.Column<DateTimeOffset>(nullable: false),
|
||||
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
|
||||
},
|
||||
|
@ -20,5 +20,9 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
|
||||
[Display(Name = "User can input custom amount")]
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public string Example1 { get; internal set; }
|
||||
public string Example2 { get; internal set; }
|
||||
public string ExampleCallback { get; internal set; }
|
||||
public string InvoiceUrl { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ namespace BTCPayServer.Models
|
||||
|
||||
//"price":5
|
||||
[JsonProperty("price")]
|
||||
public double Price
|
||||
public decimal Price
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -94,7 +94,7 @@ namespace BTCPayServer.Models
|
||||
//"exRates":{"USD":4320.02}
|
||||
[JsonProperty("exRates")]
|
||||
[Obsolete("Use CryptoInfo.ExRates instead")]
|
||||
public Dictionary<string, double> ExRates
|
||||
public Dictionary<string, decimal> ExRates
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -224,6 +224,29 @@ namespace BTCPayServer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty("paymentSubtotals")]
|
||||
public Dictionary<string, long> PaymentSubtotals { get; set; }
|
||||
|
||||
[JsonProperty("paymentTotals")]
|
||||
public Dictionary<string, long> PaymentTotals { get; set; }
|
||||
|
||||
[JsonProperty("amountPaid", DefaultValueHandling = DefaultValueHandling.Include)]
|
||||
public long AmountPaid { get; set; }
|
||||
|
||||
[JsonProperty("minerFees")]
|
||||
public Dictionary<string, NBitpayClient.MinerFeeInfo> MinerFees { get; set; }
|
||||
|
||||
[JsonProperty("exchangeRates")]
|
||||
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }
|
||||
|
||||
[JsonProperty("supportedTransactionCurrencies")]
|
||||
public Dictionary<string, NBitpayClient.InvoiceSupportedTransactionCurrency> SupportedTransactionCurrencies { get; set; }
|
||||
|
||||
[JsonProperty("addresses")]
|
||||
public Dictionary<string, string> Addresses { get; set; }
|
||||
[JsonProperty("paymentCodes")]
|
||||
public Dictionary<string, NBitpayClient.InvoicePaymentUrls> PaymentCodes { get; set; }
|
||||
}
|
||||
public class Flags
|
||||
{
|
||||
@ -233,4 +256,5 @@ namespace BTCPayServer.Models
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Validation;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
@ -14,7 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
Currency = "USD";
|
||||
}
|
||||
[Required]
|
||||
public double? Amount
|
||||
public decimal? Amount
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -52,8 +53,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
[Url]
|
||||
[Uri]
|
||||
public string NotificationUrl
|
||||
{
|
||||
get; set;
|
||||
|
@ -33,10 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
|
||||
public class InvoiceModel
|
||||
{
|
||||
public string Date
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public DateTimeOffset Date { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
@ -49,6 +46,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public bool ShowCheckout { get; set; }
|
||||
public string ExceptionStatus { get; set; }
|
||||
public string AmountCurrency
|
||||
{
|
||||
get; set;
|
||||
|
@ -37,6 +37,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string TimeLeft { get; set; }
|
||||
public string Rate { get; set; }
|
||||
public string OrderAmount { get; set; }
|
||||
public string OrderAmountFiat { get; set; }
|
||||
public string InvoiceBitcoinUrl { get; set; }
|
||||
public string InvoiceBitcoinUrlQR { get; set; }
|
||||
public int TxCount { get; set; }
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
@ -42,10 +43,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||
[Url]
|
||||
[Uri]
|
||||
public string CustomCSS { get; set; }
|
||||
[Display(Name = "Link to a custom logo")]
|
||||
[Url]
|
||||
[Uri]
|
||||
public string CustomLogo { get; set; }
|
||||
|
||||
[Display(Name = "Custom HTML title to display on Checkout page")]
|
||||
|
@ -9,8 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class LightningNodeViewModel
|
||||
{
|
||||
[Display(Name = "Lightning charge url")]
|
||||
public string Url
|
||||
[Display(Name = "Connection string")]
|
||||
public string ConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -44,7 +44,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string ScriptTest { get; set; }
|
||||
public CoinAverageExchange[] AvailableExchanges { get; set; }
|
||||
|
||||
[Display(Name = "Multiply the rate by ...")]
|
||||
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Validation;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using System;
|
||||
@ -34,7 +35,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Url]
|
||||
[Uri]
|
||||
[Display(Name = "Store Website")]
|
||||
[MaxLength(500)]
|
||||
public string StoreWebsite
|
||||
@ -85,5 +86,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
get; set;
|
||||
} = new List<LightningNode>();
|
||||
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
|
||||
[Range(0, 100)]
|
||||
public double PaymentTolerance
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
@ -44,6 +45,9 @@ namespace BTCPayServer.Models
|
||||
|
||||
public class PairingCodeResponse
|
||||
{
|
||||
[JsonProperty(PropertyName = "policies")]
|
||||
public JArray Policies { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "pairingCode")]
|
||||
public string PairingCode
|
||||
{
|
||||
|
@ -68,6 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
return ConfirmationCount >= 1;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.LowMediumSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 2;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 6;
|
||||
|
@ -28,7 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
IApplicationLifetime _Lifetime;
|
||||
Microsoft.Extensions.Hosting.IApplicationLifetime _Lifetime;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
private TaskCompletionSource<bool> _RunningTask;
|
||||
private CancellationTokenSource _Cts;
|
||||
@ -39,7 +39,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
BTCPayWalletProvider wallets,
|
||||
InvoiceRepository invoiceRepository,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
EventAggregator aggregator, IApplicationLifetime lifetime)
|
||||
EventAggregator aggregator, Microsoft.Extensions.Hosting.IApplicationLifetime lifetime)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Wallets = wallets;
|
||||
@ -161,7 +161,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
if(payment != null)
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
if (payment != null)
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
@ -346,10 +346,10 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, InvoiceEntity invoice, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
{
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
|
||||
invoice = (await UpdatePaymentStates(wallet, invoice.Id));
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
@ -358,13 +358,13 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address.ToString();
|
||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
return invoice;
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
@ -156,7 +156,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, string description, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" });
|
||||
var invoice = await CreateInvoiceAsync(new CreateInvoiceRequest() { Amount = amount, Expiry = expiry, Description = description ?? "" }, cancellation);
|
||||
return new LightningInvoice() { Id = invoice.Id, Amount = amount, BOLT11 = invoice.PayReq, Status = "unpaid" };
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using BTCPayServer.Payments.Lightning.CLightning;
|
||||
using NBitcoin;
|
||||
using BTCPayServer.Payments.Lightning.Lnd;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -12,16 +14,38 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public ILightningInvoiceClient CreateClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var uri = supportedPaymentMethod.GetLightningUrl();
|
||||
if (uri.ConnectionType == LightningConnectionType.Charge)
|
||||
return CreateClient(uri, network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(LightningConnectionString connString, Network network)
|
||||
{
|
||||
if (connString.ConnectionType == LightningConnectionType.Charge)
|
||||
{
|
||||
return new ChargeClient(uri.ToUri(true), network.NBitcoinNetwork);
|
||||
return new ChargeClient(connString.ToUri(true), network);
|
||||
}
|
||||
else if (uri.ConnectionType == LightningConnectionType.CLightning)
|
||||
else if (connString.ConnectionType == LightningConnectionType.CLightning)
|
||||
{
|
||||
return new CLightningRPCClient(uri.ToUri(false), network.NBitcoinNetwork);
|
||||
return new CLightningRPCClient(connString.ToUri(false), network);
|
||||
}
|
||||
else if (connString.ConnectionType == LightningConnectionType.LndREST)
|
||||
{
|
||||
return new LndInvoiceClient(new LndSwaggerClient(new LndRestSettings(connString.BaseUri)
|
||||
{
|
||||
Macaroon = connString.Macaroon,
|
||||
MacaroonFilePath = connString.MacaroonFilePath,
|
||||
CertificateThumbprint = connString.CertificateThumbprint,
|
||||
AllowInsecure = connString.AllowInsecure,
|
||||
}));
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({uri.ConnectionType})");
|
||||
throw new NotSupportedException($"Unsupported connection string for lightning server ({connString.ConnectionType})");
|
||||
}
|
||||
|
||||
public static ILightningInvoiceClient CreateClient(string connectionString, Network network)
|
||||
{
|
||||
if (!Payments.Lightning.LightningConnectionString.TryParse(connectionString, false, out var conn, out string error))
|
||||
throw new FormatException($"Invalid format ({error})");
|
||||
return Payments.Lightning.LightningClientFactory.CreateClient(conn, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public enum LightningConnectionType
|
||||
{
|
||||
Charge,
|
||||
CLightning
|
||||
CLightning,
|
||||
LndREST
|
||||
}
|
||||
public class LightningConnectionString
|
||||
{
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString)
|
||||
static Dictionary<string, LightningConnectionType> typeMapping;
|
||||
static Dictionary<LightningConnectionType, string> typeMappingReverse;
|
||||
static LightningConnectionString()
|
||||
{
|
||||
return TryParse(str, out connectionString, out var error);
|
||||
typeMapping = new Dictionary<string, LightningConnectionType>();
|
||||
typeMapping.Add("clightning", LightningConnectionType.CLightning);
|
||||
typeMapping.Add("charge", LightningConnectionType.Charge);
|
||||
typeMapping.Add("lnd-rest", LightningConnectionType.LndREST);
|
||||
typeMappingReverse = new Dictionary<LightningConnectionType, string>();
|
||||
foreach (var kv in typeMapping)
|
||||
{
|
||||
typeMappingReverse.Add(kv.Value, kv.Key);
|
||||
}
|
||||
}
|
||||
public static bool TryParse(string str, out LightningConnectionString connectionString, out string error)
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString)
|
||||
{
|
||||
return TryParse(str, supportLegacy, out connectionString, out var error);
|
||||
}
|
||||
public static bool TryParse(string str, bool supportLegacy, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
|
||||
if (supportLegacy)
|
||||
{
|
||||
var parsed = TryParseLegacy(str, out connectionString, out error);
|
||||
if (!parsed)
|
||||
{
|
||||
parsed = TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return TryParseNewFormat(str, out connectionString, out error);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNewFormat(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
connectionString = null;
|
||||
error = null;
|
||||
var parts = str.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Dictionary<string, string> keyValues = new Dictionary<string, string>();
|
||||
foreach (var part in parts.Select(p => p.Trim()))
|
||||
{
|
||||
var idx = part.IndexOf('=', StringComparison.OrdinalIgnoreCase);
|
||||
if (idx == -1)
|
||||
{
|
||||
error = "The format of the connectionString should a list of key=value delimited by semicolon";
|
||||
return false;
|
||||
}
|
||||
var key = part.Substring(0, idx).Trim().ToLowerInvariant();
|
||||
var value = part.Substring(idx + 1).Trim();
|
||||
if (keyValues.ContainsKey(key))
|
||||
{
|
||||
error = $"Duplicate key {key}";
|
||||
return false;
|
||||
}
|
||||
keyValues.Add(key, value);
|
||||
}
|
||||
|
||||
var possibleTypes = String.Join(", ", typeMapping.Select(k => k.Key).ToArray());
|
||||
|
||||
LightningConnectionString result = new LightningConnectionString();
|
||||
var type = Take(keyValues, "type");
|
||||
if (type == null)
|
||||
{
|
||||
error = $"The key 'type' is mandatory, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!typeMapping.TryGetValue(type.ToLowerInvariant(), out var connectionType))
|
||||
{
|
||||
error = $"The key 'type' is invalid, possible values are {possibleTypes}";
|
||||
return false;
|
||||
}
|
||||
|
||||
result.ConnectionType = connectionType;
|
||||
|
||||
switch (connectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
var apiToken = Take(keyValues, "api-token");
|
||||
if (apiToken == null)
|
||||
{
|
||||
error = "The key 'api-token' is not found";
|
||||
return false;
|
||||
}
|
||||
result.Username = "api-token";
|
||||
result.Password = apiToken;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for charge connection strings";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server.StartsWith("//", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:" + str;
|
||||
else if (server.StartsWith("/", StringComparison.OrdinalIgnoreCase))
|
||||
server = "unix:/" + str;
|
||||
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "tcp" && uri.Scheme != "unix"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by tcp:// or unix:// or a path to the 'lightning-rpc' unix socket";
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = uri;
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
{
|
||||
var server = Take(keyValues, "server");
|
||||
if (server == null)
|
||||
{
|
||||
error = $"The key 'server' is mandatory for lnd connection strings";
|
||||
return false;
|
||||
}
|
||||
if (!Uri.TryCreate(server, UriKind.Absolute, out var uri)
|
||||
|| (uri.Scheme != "http" && uri.Scheme != "https"))
|
||||
{
|
||||
error = $"The key 'server' should be an URI starting by http:// or https://";
|
||||
return false;
|
||||
}
|
||||
parts = uri.UserInfo.Split(':');
|
||||
if (!string.IsNullOrEmpty(uri.UserInfo) && parts.Length == 2)
|
||||
{
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
|
||||
var macaroon = Take(keyValues, "macaroon");
|
||||
if (macaroon != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Macaroon = Encoder.DecodeData(macaroon);
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'macaroon' format should be in hex";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var macaroonFilePath = Take(keyValues, "macaroonfilepath");
|
||||
if (macaroonFilePath != null)
|
||||
{
|
||||
if(macaroon != null)
|
||||
{
|
||||
error = $"The key 'macaroon' is already specified";
|
||||
return false;
|
||||
}
|
||||
if(!macaroonFilePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = $"The key 'macaroonfilepath' should point to a .macaroon file";
|
||||
return false;
|
||||
}
|
||||
result.MacaroonFilePath = macaroonFilePath;
|
||||
}
|
||||
|
||||
string securitySet = null;
|
||||
var certthumbprint = Take(keyValues, "certthumbprint");
|
||||
if (certthumbprint != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = Encoders.Hex.DecodeData(certthumbprint.Replace(":", string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||
if (bytes.Length != 32)
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid length: it should be the SHA256 of the PEM format of the certificate (32 bytes)";
|
||||
return false;
|
||||
}
|
||||
result.CertificateThumbprint = bytes;
|
||||
}
|
||||
catch
|
||||
{
|
||||
error = $"The key 'certthumbprint' has invalid format: it should be the SHA256 of the PEM format of the certificate";
|
||||
return false;
|
||||
}
|
||||
securitySet = "certthumbprint";
|
||||
}
|
||||
|
||||
var allowinsecureStr = Take(keyValues, "allowinsecure");
|
||||
|
||||
if (allowinsecureStr != null)
|
||||
{
|
||||
var allowedValues = new[] { "true", "false" };
|
||||
if (!allowedValues.Any(v => v.Equals(allowinsecureStr, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
error = $"The key 'allowinsecure' should be true or false";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allowInsecure = allowinsecureStr.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
if (securitySet != null && allowInsecure)
|
||||
{
|
||||
error = $"The key 'allowinsecure' conflict with '{securitySet}'";
|
||||
return false;
|
||||
}
|
||||
result.AllowInsecure = allowInsecure;
|
||||
}
|
||||
|
||||
if (!result.AllowInsecure && result.BaseUri.Scheme == "http")
|
||||
{
|
||||
error = $"The key 'allowinsecure' is false, but server's Uri is not using https";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
}
|
||||
|
||||
if (keyValues.Count != 0)
|
||||
{
|
||||
error = $"Unknown keys ({String.Join(", ", keyValues.Select(k => k.Key).ToArray())})";
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
private static string Take(Dictionary<string, string> keyValues, string key)
|
||||
{
|
||||
if (keyValues.TryGetValue(key, out var v))
|
||||
keyValues.Remove(key);
|
||||
return v;
|
||||
}
|
||||
|
||||
private static bool TryParseLegacy(string str, out LightningConnectionString connectionString, out string error)
|
||||
{
|
||||
if (str.StartsWith('/'))
|
||||
str = "unix:" + str;
|
||||
var result = new LightningConnectionString();
|
||||
@ -27,7 +290,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
error = null;
|
||||
|
||||
Uri uri;
|
||||
if (!System.Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
if (!Uri.TryCreate(str, UriKind.Absolute, out uri))
|
||||
{
|
||||
error = "Invalid URL";
|
||||
return false;
|
||||
@ -40,7 +303,6 @@ namespace BTCPayServer.Payments.Lightning
|
||||
error = $"The url support the following protocols {protocols}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "unix")
|
||||
{
|
||||
str = uri.AbsoluteUri.Substring("unix:".Length);
|
||||
@ -49,8 +311,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
str = str.Substring(1);
|
||||
}
|
||||
uri = new Uri("unix://" + str, UriKind.Absolute);
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
}
|
||||
|
||||
if (uri.Scheme == "tcp")
|
||||
result.ConnectionType = LightningConnectionType.CLightning;
|
||||
|
||||
if (uri.Scheme == "http" || uri.Scheme == "https")
|
||||
{
|
||||
var parts = uri.UserInfo.Split(':');
|
||||
@ -61,6 +327,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
result.Username = parts[0];
|
||||
result.Password = parts[1];
|
||||
result.ConnectionType = LightningConnectionType.Charge;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uri.UserInfo))
|
||||
{
|
||||
@ -68,6 +335,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return false;
|
||||
}
|
||||
result.BaseUri = new UriBuilder(uri) { UserName = "", Password = "" }.Uri;
|
||||
result.IsLegacy = true;
|
||||
connectionString = result;
|
||||
return true;
|
||||
}
|
||||
@ -80,15 +348,17 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public Uri BaseUri { get; set; }
|
||||
public bool IsLegacy { get; private set; }
|
||||
|
||||
public LightningConnectionType ConnectionType
|
||||
{
|
||||
get
|
||||
{
|
||||
return BaseUri.Scheme == "http" || BaseUri.Scheme == "https" ? LightningConnectionType.Charge
|
||||
: LightningConnectionType.CLightning;
|
||||
}
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
|
||||
public Uri ToUri(bool withCredentials)
|
||||
{
|
||||
@ -101,10 +371,57 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return BaseUri;
|
||||
}
|
||||
}
|
||||
|
||||
static NBitcoin.DataEncoders.DataEncoder Encoder = NBitcoin.DataEncoders.Encoders.Hex;
|
||||
public override string ToString()
|
||||
{
|
||||
return ToUri(true).AbsoluteUri;
|
||||
var type = typeMappingReverse[ConnectionType];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append($"type={type}");
|
||||
switch (ConnectionType)
|
||||
{
|
||||
case LightningConnectionType.Charge:
|
||||
if (Username == null || Username == "api-token")
|
||||
{
|
||||
builder.Append($";server={BaseUri};api-token={Password}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
break;
|
||||
case LightningConnectionType.CLightning:
|
||||
builder.Append($";server={BaseUri}");
|
||||
break;
|
||||
case LightningConnectionType.LndREST:
|
||||
if (Username == null)
|
||||
{
|
||||
builder.Append($";server={BaseUri}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($";server={ToUri(true)}");
|
||||
}
|
||||
if (Macaroon != null)
|
||||
{
|
||||
builder.Append($";macaroon={Encoder.EncodeData(Macaroon)}");
|
||||
}
|
||||
if (MacaroonFilePath != null)
|
||||
{
|
||||
builder.Append($";macaroonfilepath={MacaroonFilePath}");
|
||||
}
|
||||
if (CertificateThumbprint != null)
|
||||
{
|
||||
builder.Append($";certthumbprint={Encoders.Hex.EncodeData(CertificateThumbprint)}");
|
||||
}
|
||||
if (AllowInsecure)
|
||||
{
|
||||
builder.Append($";allowinsecure=true");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(type);
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
|
||||
{
|
||||
public static int LIGHTNING_TIMEOUT = 5000;
|
||||
|
||||
NBXplorerDashboard _Dashboard;
|
||||
LightningClientFactory _LightningClientFactory;
|
||||
public LightningLikePaymentHandler(
|
||||
@ -36,17 +38,25 @@ namespace BTCPayServer.Payments.Lightning
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
|
||||
LightningInvoice lightningInvoice = null;
|
||||
try
|
||||
|
||||
string description = storeBlob.LightningDescriptionTemplate;
|
||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
string description = storeBlob.LightningDescriptionTemplate;
|
||||
description = description.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoice.ProductInformation.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoice.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
||||
try
|
||||
{
|
||||
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), description, expiry, 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 PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
||||
}
|
||||
}
|
||||
var nodeInfo = await test;
|
||||
return new LightningLikePaymentMethodDetails()
|
||||
@ -62,34 +72,36 @@ namespace BTCPayServer.Payments.Lightning
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
LightningNodeInformation info = null;
|
||||
try
|
||||
using (var cts = new CancellationTokenSource(LIGHTNING_TIMEOUT))
|
||||
{
|
||||
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 PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
LightningNodeInformation info = null;
|
||||
try
|
||||
{
|
||||
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 PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if (info.Address == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
if (info.Address == null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
|
||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||
if (blocksGap > 10)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||
if (blocksGap > 10)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TestConnection(NodeInfo nodeInfo, CancellationToken cancellation)
|
||||
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
await EnsureListening(inv.InvoiceId, false);
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
}
|
||||
}));
|
||||
|
||||
@ -189,8 +189,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
BOLT11 = notification.BOLT11,
|
||||
Amount = notification.Amount
|
||||
}, network.CryptoCode, accounted: true);
|
||||
if(payment != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(listenedInvoice.InvoiceId, 1002, "invoice_receivedPayment"));
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, listenedInvoice.InvoiceId);
|
||||
if(invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> _ListeningLightning = new List<Task>();
|
||||
|
@ -8,18 +8,41 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Username { get; set; }
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Password { get; set; }
|
||||
|
||||
// This property MUST be after CryptoCode or else JSON serialization fails
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string LightningChargeUrl { get; set; }
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string LightningConnectionString { get; set; }
|
||||
|
||||
public LightningConnectionString GetLightningUrl()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if(!LightningConnectionString.TryParse(fullUri, out var connectionString, out var error))
|
||||
if (!string.IsNullOrEmpty(LightningConnectionString))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(LightningConnectionString, false, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
var fullUri = new UriBuilder(LightningChargeUrl) { UserName = Username, Password = Password }.Uri.AbsoluteUri;
|
||||
if (!BTCPayServer.Payments.Lightning.LightningConnectionString.TryParse(fullUri, true, out var connectionString, out var error))
|
||||
{
|
||||
throw new FormatException(error);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
return connectionString;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
@ -29,16 +52,11 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new ArgumentNullException(nameof(connectionString));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Username = connectionString.Username;
|
||||
Password = connectionString.Password;
|
||||
LightningChargeUrl = connectionString.BaseUri.AbsoluteUri;
|
||||
LightningConnectionString = connectionString.ToString();
|
||||
Username = null;
|
||||
Password = null;
|
||||
LightningChargeUrl = null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Username { get; set; }
|
||||
[Obsolete("Use Get/SetLightningUrl")]
|
||||
public string Password { get; set; }
|
||||
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||
}
|
||||
}
|
||||
|
67
BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs
Normal file
67
BTCPayServer/Payments/Lightning/Lnd/LndAuthentication.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public abstract class LndAuthentication
|
||||
{
|
||||
public class FixedMacaroonAuthentication : LndAuthentication
|
||||
{
|
||||
public FixedMacaroonAuthentication(byte[] macaroon)
|
||||
{
|
||||
if (macaroon == null)
|
||||
throw new ArgumentNullException(nameof(macaroon));
|
||||
Macaroon = macaroon;
|
||||
}
|
||||
public byte[] Macaroon { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(Macaroon));
|
||||
}
|
||||
}
|
||||
public class NullAuthentication : LndAuthentication
|
||||
{
|
||||
public static NullAuthentication Instance { get; } = new NullAuthentication();
|
||||
|
||||
private NullAuthentication()
|
||||
{
|
||||
|
||||
}
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class MacaroonFileAuthentication : LndAuthentication
|
||||
{
|
||||
public MacaroonFileAuthentication(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
// Because this dump the whole file, let's make sure it is indeed the macaroon
|
||||
if (!filePath.EndsWith(".macaroon", StringComparison.OrdinalIgnoreCase))
|
||||
throw new ArgumentException(message: "filePath is not a macaroon file", paramName: nameof(filePath));
|
||||
FilePath = filePath;
|
||||
}
|
||||
public string FilePath { get; set; }
|
||||
public override void AddAuthentication(HttpRequestMessage httpRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(FilePath);
|
||||
httpRequest.Headers.Add("Grpc-Metadata-macaroon", Encoders.Hex.EncodeData(bytes));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void AddAuthentication(HttpRequestMessage httpRequest);
|
||||
}
|
||||
}
|
227
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
227
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndInvoiceClient : ILightningInvoiceClient
|
||||
{
|
||||
class LndInvoiceClientSession : ILightningListenInvoiceSession
|
||||
{
|
||||
private LndSwaggerClient _Parent;
|
||||
Channel<LightningInvoice> _Invoices = Channel.CreateBounded<LightningInvoice>(50);
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
ManualResetEventSlim _Stopped = new ManualResetEventSlim(false);
|
||||
|
||||
public LndInvoiceClientSession(LndSwaggerClient parent)
|
||||
{
|
||||
_Parent = parent;
|
||||
}
|
||||
|
||||
public async void StartListening()
|
||||
{
|
||||
var urlBuilder = new StringBuilder();
|
||||
urlBuilder.Append(_Parent.BaseUrl).Append("/v1/invoices/subscribe");
|
||||
try
|
||||
{
|
||||
using (var client = _Parent.CreateHttpClient())
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString());
|
||||
|
||||
using (var response = await client.SendAsync(
|
||||
request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token))
|
||||
{
|
||||
using (var body = await response.Content.ReadAsStreamAsync())
|
||||
using (var reader = new StreamReader(body))
|
||||
{
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
string line = await reader.ReadLineAsync().WithCancellation(_Cts.Token);
|
||||
if (line != null && line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var invoiceString = JObject.Parse(line)["result"].ToString();
|
||||
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
|
||||
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Stopped.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _Invoices.Reader.ReadAsync(cancellation);
|
||||
}
|
||||
catch (ChannelClosedException)
|
||||
{
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Cts.Cancel();
|
||||
_Stopped.Wait();
|
||||
_Invoices.Writer.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LndSwaggerClient _rpcClient;
|
||||
|
||||
public LndInvoiceClient(LndSwaggerClient swaggerClient)
|
||||
{
|
||||
if (swaggerClient == null)
|
||||
throw new ArgumentNullException(nameof(swaggerClient));
|
||||
_rpcClient = swaggerClient;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> CreateInvoice(LightMoney amount, string description, TimeSpan expiry,
|
||||
CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var strAmount = ConvertInv.ToString(amount.ToUnit(LightMoneyUnit.Satoshi));
|
||||
var strExpiry = ConvertInv.ToString(Math.Round(expiry.TotalSeconds, 0));
|
||||
// lnd requires numbers sent as strings. don't ask
|
||||
var resp = await _rpcClient.AddInvoiceAsync(new LnrpcInvoice
|
||||
{
|
||||
Value = strAmount,
|
||||
Memo = description,
|
||||
Expiry = strExpiry
|
||||
});
|
||||
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = amount,
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task<LightningNodeInformation> GetInfo(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.GetInfoAsync(cancellation);
|
||||
|
||||
var nodeInfo = new LightningNodeInformation
|
||||
{
|
||||
BlockHeight = (int?)resp.Block_height ?? 0,
|
||||
NodeId = resp.Identity_pubkey
|
||||
};
|
||||
|
||||
|
||||
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
|
||||
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
|
||||
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
|
||||
|
||||
var firstNodeInfo = node.Node.Addresses.First();
|
||||
var externalHostPort = firstNodeInfo.Addr.Split(':');
|
||||
|
||||
nodeInfo.Address = externalHostPort[0];
|
||||
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var resp = await _rpcClient.LookupInvoiceAsync(invoiceId, null, cancellation);
|
||||
return ConvertLndInvoice(resp);
|
||||
}
|
||||
|
||||
public Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var session = new LndInvoiceClientSession(this._rpcClient);
|
||||
session.StartListening();
|
||||
return Task.FromResult<ILightningListenInvoiceSession>(session);
|
||||
}
|
||||
|
||||
internal static LightningInvoice ConvertLndInvoice(LnrpcInvoice resp)
|
||||
{
|
||||
var invoice = new LightningInvoice
|
||||
{
|
||||
// TODO: Verify id corresponds to R_hash
|
||||
Id = BitString(resp.R_hash),
|
||||
Amount = new LightMoney(ConvertInv.ToInt64(resp.Value), LightMoneyUnit.Satoshi),
|
||||
BOLT11 = resp.Payment_request,
|
||||
Status = "unpaid"
|
||||
};
|
||||
|
||||
if (resp.Settle_date != null)
|
||||
{
|
||||
invoice.PaidAt = DateTimeOffset.FromUnixTimeSeconds(ConvertInv.ToInt64(resp.Settle_date));
|
||||
invoice.Status = "paid";
|
||||
}
|
||||
else
|
||||
{
|
||||
var invoiceExpiry = ConvertInv.ToInt64(resp.Creation_date) + ConvertInv.ToInt64(resp.Expiry);
|
||||
if (DateTimeOffset.FromUnixTimeSeconds(invoiceExpiry) > DateTimeOffset.UtcNow)
|
||||
{
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
|
||||
|
||||
// utility static methods... maybe move to separate class
|
||||
private static string BitString(byte[] bytes)
|
||||
{
|
||||
return BitConverter.ToString(bytes)
|
||||
.Replace("-", "", StringComparison.InvariantCulture)
|
||||
.ToLower(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Invariant culture conversion
|
||||
public static class ConvertInv
|
||||
{
|
||||
public static int ToInt32(string str)
|
||||
{
|
||||
return Convert.ToInt32(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static long ToInt64(string str)
|
||||
{
|
||||
return Convert.ToInt64(str, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
public static string ToString(decimal d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string ToString(double d)
|
||||
{
|
||||
return d.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs
Normal file
36
BTCPayServer/Payments/Lightning/Lnd/LndRestSettings.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public class LndRestSettings
|
||||
{
|
||||
public LndRestSettings()
|
||||
{
|
||||
|
||||
}
|
||||
public LndRestSettings(Uri uri)
|
||||
{
|
||||
Uri = uri;
|
||||
}
|
||||
public Uri Uri { get; set; }
|
||||
/// <summary>
|
||||
/// The SHA256 of the PEM certificate
|
||||
/// </summary>
|
||||
public byte[] CertificateThumbprint { get; set; }
|
||||
public byte[] Macaroon { get; set; }
|
||||
public bool AllowInsecure { get; set; }
|
||||
public string MacaroonFilePath { get; set; }
|
||||
|
||||
public LndAuthentication CreateLndAuthentication()
|
||||
{
|
||||
if (Macaroon != null)
|
||||
return new LndAuthentication.FixedMacaroonAuthentication(Macaroon);
|
||||
if (!string.IsNullOrEmpty(MacaroonFilePath))
|
||||
return new LndAuthentication.MacaroonFileAuthentication(MacaroonFilePath);
|
||||
return LndAuthentication.NullAuthentication.Instance;
|
||||
}
|
||||
}
|
||||
}
|
8713
BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.cs
Normal file
8713
BTCPayServer/Payments/Lightning/Lnd/LndSwaggerClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.Lnd
|
||||
{
|
||||
public partial class LndSwaggerClient
|
||||
{
|
||||
public LndSwaggerClient(LndRestSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
_LndSettings = settings;
|
||||
_Authentication = settings.CreateLndAuthentication();
|
||||
BaseUrl = settings.Uri.AbsoluteUri.TrimEnd('/');
|
||||
_httpClient = CreateHttpClient(settings);
|
||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(() =>
|
||||
{
|
||||
var json = new Newtonsoft.Json.JsonSerializerSettings();
|
||||
UpdateJsonSerializerSettings(json);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
LndRestSettings _LndSettings;
|
||||
LndAuthentication _Authentication;
|
||||
|
||||
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
|
||||
{
|
||||
_Authentication.AddAuthentication(request);
|
||||
}
|
||||
|
||||
internal static HttpClient CreateHttpClient(LndRestSettings settings)
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
SslProtocols = SslProtocols.Tls12
|
||||
};
|
||||
|
||||
var expectedThumbprint = settings.CertificateThumbprint?.ToArray();
|
||||
if (expectedThumbprint != null)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = (request, cert, chain, errors) =>
|
||||
{
|
||||
var actualCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
|
||||
var hash = actualCert.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
|
||||
return hash.SequenceEqual(expectedThumbprint);
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.AllowInsecure)
|
||||
{
|
||||
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (settings.Uri.Scheme == "http")
|
||||
throw new InvalidOperationException("AllowInsecure is set to false, but the URI is not using https");
|
||||
}
|
||||
return new HttpClient(handler);
|
||||
}
|
||||
|
||||
internal HttpClient CreateHttpClient()
|
||||
{
|
||||
return LndSwaggerClient.CreateHttpClient(_LndSettings);
|
||||
}
|
||||
|
||||
internal T Deserialize<T>(string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(str, _settings.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,16 +31,20 @@ namespace BTCPayServer
|
||||
var logger = loggerFactory.CreateLogger("Configuration");
|
||||
try
|
||||
{
|
||||
// This is the only way toat LoadArgs can print to console. Because LoadArgs is called by the HostBuilder before Logs.Configure is called
|
||||
var conf = new DefaultConfiguration() { Logger = logger }.CreateConfiguration(args);
|
||||
if (conf == null)
|
||||
return;
|
||||
Logs.Configure(loggerFactory);
|
||||
new BTCPayServerOptions().LoadArgs(conf);
|
||||
Logs.Configure(null);
|
||||
/////
|
||||
|
||||
host = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseIISIntegration()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseConfiguration(conf)
|
||||
.UseApplicationInsights()
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
@ -65,6 +69,8 @@ namespace BTCPayServer
|
||||
finally
|
||||
{
|
||||
processor.Dispose();
|
||||
if(host == null)
|
||||
Logs.Configuration.LogError("Configuration error");
|
||||
if (host != null)
|
||||
host.Dispose();
|
||||
loggerProvider.Dispose();
|
||||
|
@ -45,6 +45,11 @@ namespace BTCPayServer.Rating
|
||||
var currencyPair = splitted[0];
|
||||
if (currencyPair.Length < 6 || currencyPair.Length > 10)
|
||||
return false;
|
||||
if (currencyPair.Length == 6)
|
||||
{
|
||||
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
|
||||
return true;
|
||||
}
|
||||
for (int i = 3; i < 5; i++)
|
||||
{
|
||||
var potentialCryptoName = currencyPair.Substring(0, i);
|
||||
|
@ -41,9 +41,9 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rate.Value.HasValue)
|
||||
if (rate.BidAsk != null)
|
||||
{
|
||||
_AllRates[key].Value = rate.Value;
|
||||
_AllRates[key].BidAsk = rate.BidAsk;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,39 +58,175 @@ namespace BTCPayServer.Rating
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
|
||||
public void SetRate(string exchangeName, CurrencyPair currencyPair, BidAsk bidAsk)
|
||||
{
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
rate.Value = value;
|
||||
if(rate != null)
|
||||
{
|
||||
rate.BidAsk = bidAsk;
|
||||
}
|
||||
var invPair = currencyPair.Inverse();
|
||||
var invRate = rates.FirstOrDefault(r => r.CurrencyPair == invPair);
|
||||
if (invRate != null)
|
||||
{
|
||||
invRate.BidAsk = bidAsk?.Inverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||
public BidAsk GetRate(string exchangeName, CurrencyPair currencyPair)
|
||||
{
|
||||
if (currencyPair.Left == currencyPair.Right)
|
||||
return 1.0m;
|
||||
return BidAsk.One;
|
||||
if (ByExchange.TryGetValue(exchangeName, out var rates))
|
||||
{
|
||||
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
|
||||
if (rate != null)
|
||||
return rate.Value;
|
||||
return rate.BidAsk;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public class BidAsk
|
||||
{
|
||||
|
||||
private readonly static BidAsk _One = new BidAsk(1.0m);
|
||||
public static BidAsk One
|
||||
{
|
||||
get
|
||||
{
|
||||
return _One;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly static BidAsk _Zero = new BidAsk(0.0m);
|
||||
public static BidAsk Zero
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Zero;
|
||||
}
|
||||
}
|
||||
public BidAsk(decimal bid, decimal ask)
|
||||
{
|
||||
if (bid > ask)
|
||||
throw new ArgumentException("the bid should be lower than ask", nameof(bid));
|
||||
_Ask = ask;
|
||||
_Bid = bid;
|
||||
}
|
||||
public BidAsk(decimal v) : this(v, v)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private readonly decimal _Bid;
|
||||
public decimal Bid
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Bid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly decimal _Ask;
|
||||
public decimal Ask
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Ask;
|
||||
}
|
||||
}
|
||||
public BidAsk Inverse()
|
||||
{
|
||||
return new BidAsk(1.0m / Ask, 1.0m / Bid);
|
||||
}
|
||||
|
||||
public static BidAsk operator +(BidAsk a, BidAsk b)
|
||||
{
|
||||
return new BidAsk(a.Bid + b.Bid, a.Ask + b.Ask);
|
||||
}
|
||||
|
||||
public static BidAsk operator +(BidAsk a)
|
||||
{
|
||||
return new BidAsk(a.Bid, a.Ask);
|
||||
}
|
||||
|
||||
public static BidAsk operator -(BidAsk a)
|
||||
{
|
||||
return new BidAsk(-a.Bid, -a.Ask);
|
||||
}
|
||||
|
||||
public static BidAsk operator *(BidAsk a, BidAsk b)
|
||||
{
|
||||
return new BidAsk(a.Bid * b.Bid, a.Ask * b.Ask);
|
||||
}
|
||||
|
||||
public static BidAsk operator /(BidAsk a, BidAsk b)
|
||||
{
|
||||
// This one is tricky.
|
||||
// BTC_EUR = (6000, 6100)
|
||||
// Implicit rule give
|
||||
// EUR_BTC = 1 / BTC_EUR
|
||||
// Or
|
||||
// EUR_BTC = (1, 1) / BTC_EUR
|
||||
// Naive calculation would give us ( 1/6000, 1/6100) = (0.000166, 0.000163)
|
||||
// However, this is an invalid BidAsk!!! because 0.000166 > 0.000163
|
||||
// So instead, we need to calculate (1/6100, 1/6000)
|
||||
return new BidAsk(a.Bid / b.Ask, a.Ask / b.Bid);
|
||||
}
|
||||
|
||||
public static BidAsk operator -(BidAsk a, BidAsk b)
|
||||
{
|
||||
return new BidAsk(a.Bid - b.Bid, a.Ask - b.Ask);
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
BidAsk item = obj as BidAsk;
|
||||
if (item == null)
|
||||
return false;
|
||||
return Bid == item.Bid && Ask == item.Ask;
|
||||
}
|
||||
public static bool operator ==(BidAsk a, BidAsk b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.Bid == b.Bid && a.Ask == b.Ask;
|
||||
}
|
||||
|
||||
public static bool operator !=(BidAsk a, BidAsk b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ToString().GetHashCode(StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Bid == Ask)
|
||||
return Bid.ToString(CultureInfo.InvariantCulture);
|
||||
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
}
|
||||
public class ExchangeRate
|
||||
{
|
||||
public string Exchange { get; set; }
|
||||
public CurrencyPair CurrencyPair { get; set; }
|
||||
public decimal? Value { get; set; }
|
||||
public BidAsk BidAsk { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Value == null)
|
||||
if (BidAsk == null)
|
||||
return $"{Exchange}({CurrencyPair})";
|
||||
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
|
||||
return $"{Exchange}({CurrencyPair}) == {BidAsk.ToString()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ namespace BTCPayServer.Rating
|
||||
UnsupportedOperator,
|
||||
MissingArgument,
|
||||
DivideByZero,
|
||||
InvalidNegative,
|
||||
PreprocessError,
|
||||
RateUnavailable,
|
||||
InvalidExchangeName,
|
||||
@ -139,7 +140,7 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
return new RateRule(this, currencyPair, candidate);
|
||||
}
|
||||
|
||||
|
||||
public ExpressionSyntax FindBestCandidate(CurrencyPair p)
|
||||
{
|
||||
var invP = p.Inverse();
|
||||
@ -147,9 +148,9 @@ namespace BTCPayServer.Rating
|
||||
foreach (var pair in new[]
|
||||
{
|
||||
(Pair: p, Priority: 0, Inverse: false),
|
||||
(Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false),
|
||||
(Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false),
|
||||
(Pair: invP, Priority: 2, Inverse: true),
|
||||
(Pair: invP, Priority: 1, Inverse: true),
|
||||
(Pair: new CurrencyPair(p.Left, "X"), Priority: 2, Inverse: false),
|
||||
(Pair: new CurrencyPair("X", p.Right), Priority: 2, Inverse: false),
|
||||
(Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true),
|
||||
(Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true),
|
||||
(Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false)
|
||||
@ -216,8 +217,7 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
else
|
||||
{
|
||||
var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token);
|
||||
return RateRules.CreateExpression(rate.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,7 +225,7 @@ namespace BTCPayServer.Rating
|
||||
|
||||
class CalculateWalker : CSharpSyntaxWalker
|
||||
{
|
||||
public Stack<decimal> Values = new Stack<decimal>();
|
||||
public Stack<BidAsk> Values = new Stack<BidAsk>();
|
||||
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||
|
||||
public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node)
|
||||
@ -254,7 +254,15 @@ namespace BTCPayServer.Rating
|
||||
switch (node.Kind())
|
||||
{
|
||||
case SyntaxKind.UnaryMinusExpression:
|
||||
Values.Push(-Values.Pop());
|
||||
var v = Values.Pop();
|
||||
if(v.Bid == v.Ask)
|
||||
{
|
||||
Values.Push(-v);
|
||||
}
|
||||
else
|
||||
{
|
||||
Errors.Add(RateRulesErrors.InvalidNegative);
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.UnaryPlusExpression:
|
||||
Values.Push(+Values.Pop());
|
||||
@ -299,7 +307,7 @@ namespace BTCPayServer.Rating
|
||||
Values.Push(a * b);
|
||||
break;
|
||||
case SyntaxKind.DivideExpression:
|
||||
if (b == decimal.Zero)
|
||||
if (a.Ask == decimal.Zero || b.Ask == decimal.Zero)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.DivideByZero);
|
||||
}
|
||||
@ -309,19 +317,48 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
break;
|
||||
case SyntaxKind.SubtractExpression:
|
||||
Values.Push(a - b);
|
||||
if (b.Bid == b.Ask)
|
||||
{
|
||||
Values.Push(a - b);
|
||||
}
|
||||
else
|
||||
{
|
||||
Errors.Add(RateRulesErrors.InvalidNegative);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("Should never happen");
|
||||
}
|
||||
}
|
||||
|
||||
Stack<decimal> _TupleValues = null;
|
||||
public override void VisitTupleExpression(TupleExpressionSyntax node)
|
||||
{
|
||||
_TupleValues = new Stack<decimal>();
|
||||
base.VisitTupleExpression(node);
|
||||
if(_TupleValues.Count != 2)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.MissingArgument);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ask = _TupleValues.Pop();
|
||||
var bid = _TupleValues.Pop();
|
||||
Values.Push(new BidAsk(bid, ask));
|
||||
}
|
||||
_TupleValues = null;
|
||||
}
|
||||
|
||||
public override void VisitLiteralExpression(LiteralExpressionSyntax node)
|
||||
{
|
||||
switch (node.Kind())
|
||||
{
|
||||
case SyntaxKind.NumericLiteralExpression:
|
||||
Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture));
|
||||
var v = decimal.Parse(node.ToString(), CultureInfo.InvariantCulture);
|
||||
if (_TupleValues == null)
|
||||
Values.Push(new BidAsk(v));
|
||||
else
|
||||
_TupleValues.Push(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -347,17 +384,23 @@ namespace BTCPayServer.Rating
|
||||
class FlattenExpressionRewriter : CSharpSyntaxRewriter
|
||||
{
|
||||
RateRules parent;
|
||||
CurrencyPair pair;
|
||||
int nested = 0;
|
||||
public FlattenExpressionRewriter(RateRules parent, CurrencyPair pair)
|
||||
{
|
||||
Context.Push(pair);
|
||||
this.pair = pair;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public ExchangeRates ExchangeRates = new ExchangeRates();
|
||||
public Stack<CurrencyPair> Context { get; set; } = new Stack<CurrencyPair>();
|
||||
bool IsInvocation;
|
||||
public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node)
|
||||
{
|
||||
if (IsInvocation)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.InvalidCurrencyIdentifier);
|
||||
return RateRules.CreateExpression($"ERR_INVALID_CURRENCY_PAIR({node.ToString()})");
|
||||
}
|
||||
IsInvocation = true;
|
||||
_ExchangeName = node.Expression.ToString();
|
||||
var result = base.VisitInvocationExpression(node);
|
||||
@ -365,18 +408,27 @@ namespace BTCPayServer.Rating
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsArgumentList;
|
||||
public override SyntaxNode VisitArgumentList(ArgumentListSyntax node)
|
||||
{
|
||||
IsArgumentList = true;
|
||||
var result = base.VisitArgumentList(node);
|
||||
IsArgumentList = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
string _ExchangeName = null;
|
||||
|
||||
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
|
||||
const int MaxNestedCount = 8;
|
||||
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
if (CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
|
||||
if (
|
||||
(!IsInvocation || IsArgumentList) &&
|
||||
CurrencyPair.TryParse(node.Identifier.ValueText, out var currentPair))
|
||||
{
|
||||
var ctx = Context.Peek();
|
||||
|
||||
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? ctx.Left : currentPair.Left,
|
||||
right: currentPair.Right == "X" ? ctx.Right : currentPair.Right);
|
||||
var replacedPair = new CurrencyPair(left: currentPair.Left == "X" ? pair.Left : currentPair.Left,
|
||||
right: currentPair.Right == "X" ? pair.Right : currentPair.Right);
|
||||
if (IsInvocation) // eg. replace bittrex(BTC_X) to bittrex(BTC_USD)
|
||||
{
|
||||
ExchangeRates.Add(new ExchangeRate() { CurrencyPair = replacedPair, Exchange = _ExchangeName });
|
||||
@ -385,13 +437,13 @@ namespace BTCPayServer.Rating
|
||||
else // eg. replace BTC_X to BTC_USD, then replace by the expression for BTC_USD
|
||||
{
|
||||
var bestCandidate = parent.FindBestCandidate(replacedPair);
|
||||
if (Context.Count > MaxNestedCount)
|
||||
if (nested > MaxNestedCount)
|
||||
{
|
||||
Errors.Add(RateRulesErrors.TooMuchNestedCalls);
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
}
|
||||
Context.Push(replacedPair);
|
||||
var replaced = Visit(bestCandidate);
|
||||
var innerFlatten = CreateNewContext(replacedPair);
|
||||
var replaced = innerFlatten.Visit(bestCandidate);
|
||||
if (replaced is ExpressionSyntax expression)
|
||||
{
|
||||
var hasBinaryOps = new HasBinaryOperations();
|
||||
@ -401,7 +453,6 @@ namespace BTCPayServer.Rating
|
||||
replaced = SyntaxFactory.ParenthesizedExpression(expression);
|
||||
}
|
||||
}
|
||||
Context.Pop();
|
||||
if (Errors.Contains(RateRulesErrors.TooMuchNestedCalls))
|
||||
{
|
||||
return RateRules.CreateExpression($"ERR_TOO_MUCH_NESTED_CALLS({replacedPair})");
|
||||
@ -411,16 +462,37 @@ namespace BTCPayServer.Rating
|
||||
}
|
||||
return base.VisitIdentifierName(node);
|
||||
}
|
||||
|
||||
private FlattenExpressionRewriter CreateNewContext(CurrencyPair pair)
|
||||
{
|
||||
return new FlattenExpressionRewriter(parent, pair)
|
||||
{
|
||||
Errors = Errors,
|
||||
nested = nested + 1,
|
||||
ExchangeRates = ExchangeRates,
|
||||
};
|
||||
}
|
||||
}
|
||||
private SyntaxNode expression;
|
||||
FlattenExpressionRewriter flatten;
|
||||
|
||||
public RateRule(RateRules parent, CurrencyPair currencyPair, SyntaxNode candidate)
|
||||
{
|
||||
_CurrencyPair = currencyPair;
|
||||
flatten = new FlattenExpressionRewriter(parent, currencyPair);
|
||||
this.expression = flatten.Visit(candidate);
|
||||
}
|
||||
|
||||
|
||||
private readonly CurrencyPair _CurrencyPair;
|
||||
public CurrencyPair CurrencyPair
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrencyPair;
|
||||
}
|
||||
}
|
||||
|
||||
public ExchangeRates ExchangeRates
|
||||
{
|
||||
get
|
||||
@ -452,7 +524,7 @@ namespace BTCPayServer.Rating
|
||||
Errors.AddRange(calculate.Errors);
|
||||
return false;
|
||||
}
|
||||
_Value = calculate.Values.Pop();
|
||||
_Value = calculate.Values.Pop().Bid;
|
||||
_EvaluatedNode = result;
|
||||
return true;
|
||||
}
|
||||
|
247
BTCPayServer/Security/BitpayAuthentication.cs
Normal file
247
BTCPayServer/Security/BitpayAuthentication.cs
Normal file
@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayAuthentication
|
||||
{
|
||||
public class BitpayAuthOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
|
||||
}
|
||||
class BitpayAuthHandler : AuthenticationHandler<BitpayAuthOptions>
|
||||
{
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
public BitpayAuthHandler(
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
IOptionsMonitor<BitpayAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
{
|
||||
_TokenRepository = tokenRepository;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (Context.Request.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
List<Claim> claims = new List<Claim>();
|
||||
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
// Careful, those are not the opposite. failedAuth says if a the tentative failed.
|
||||
// successAuth, ensure that at least one succeed.
|
||||
var failedAuth = false;
|
||||
var successAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
var result = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id, claims);
|
||||
storeId = result.StoreId;
|
||||
failedAuth = !result.SuccessAuth;
|
||||
successAuth = result.SuccessAuth;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
successAuth = storeId != null;
|
||||
}
|
||||
|
||||
if (failedAuth)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
}
|
||||
|
||||
if (successAuth)
|
||||
{
|
||||
if (storeId != null)
|
||||
{
|
||||
claims.Add(new Claim(Policies.CanUseStore.Key, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
Context.Request.HttpContext.SetStoreData(store);
|
||||
}
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(new ClaimsIdentity(claims, Policies.BitpayAuthentication)), Policies.BitpayAuthentication));
|
||||
}
|
||||
}
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
private async Task<(string StoreId, bool SuccessAuth)> CheckBitId(HttpContext httpContext, string sig, string id, List<Claim> claims)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
claims.Add(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return (null, false);
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return (storeId, true);
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
|
||||
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||
{
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
isJson)
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET" &&
|
||||
(isJson || httpContext.Request.Query.ContainsKey("token")))
|
||||
return true;
|
||||
|
||||
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
path.Equals("/tokens", StringComparison.Ordinal) &&
|
||||
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
|
||||
{
|
||||
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) => { });
|
||||
services.AddAuthentication().AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Authentication;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBitpayClient.Extensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
|
||||
{
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
StoreRepository _StoreRepository;
|
||||
TokenRepository _TokenRepository;
|
||||
|
||||
public BitpayClaimsFilter(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_TokenRepository = tokenRepository;
|
||||
}
|
||||
|
||||
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
|
||||
{
|
||||
options.Filters.Add(typeof(BitpayClaimsFilter));
|
||||
}
|
||||
|
||||
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||
{
|
||||
var principal = context.HttpContext.User;
|
||||
if (context.HttpContext.GetIsBitpayAPI())
|
||||
{
|
||||
var bitpayAuth = context.HttpContext.GetBitpayAuth();
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
|
||||
{
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
|
||||
{
|
||||
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
context.HttpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Can't access to store");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
string storeId = null;
|
||||
string body = string.Empty;
|
||||
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
|
||||
{
|
||||
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
|
||||
{
|
||||
body = reader.ReadToEnd();
|
||||
}
|
||||
httpContext.Request.Body.Position = 0;
|
||||
}
|
||||
|
||||
var url = httpContext.Request.GetEncodedUrl();
|
||||
try
|
||||
{
|
||||
var key = new PubKey(id);
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var sin = key.GetBitIDSIN();
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.SIN, sin));
|
||||
|
||||
string token = null;
|
||||
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
|
||||
{
|
||||
token = tokenValues[0];
|
||||
}
|
||||
|
||||
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
|
||||
{
|
||||
try
|
||||
{
|
||||
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (token != null)
|
||||
{
|
||||
var bitToken = await GetTokenPermissionAsync(sin, token);
|
||||
if (bitToken == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
storeId = bitToken.StoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
return storeId;
|
||||
}
|
||||
|
||||
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
|
||||
{
|
||||
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string apiKey = null;
|
||||
try
|
||||
{
|
||||
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
|
||||
{
|
||||
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
|
||||
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
|
||||
|
||||
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
|
||||
if (expectedToken == null || actualToken == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
|
||||
return null;
|
||||
}
|
||||
return actualToken;
|
||||
}
|
||||
|
||||
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
|
||||
{
|
||||
if (token.Facade == Facade.Merchant.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
yield return token.Clone(Facade.PointOfSale);
|
||||
}
|
||||
if (token.Facade == Facade.PointOfSale.ToString())
|
||||
{
|
||||
yield return token.Clone(Facade.User);
|
||||
}
|
||||
yield return token;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Security
|
||||
{
|
||||
public static class Policies
|
||||
{
|
||||
public const string BitpayAuthentication = "Bitpay.Auth";
|
||||
public const string CookieAuthentication = "Identity.Application";
|
||||
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
|
||||
{
|
||||
|
@ -39,6 +39,8 @@ namespace BTCPayServer.Services.Fees
|
||||
ExplorerClient _ExplorerClient;
|
||||
public async Task<FeeRate> GetFeeRateAsync()
|
||||
{
|
||||
if (!_ExplorerClient.Network.SupportEstimatesSmartFee)
|
||||
return _Factory.Fallback;
|
||||
try
|
||||
{
|
||||
return (await _ExplorerClient.GetFeeRateAsync(_Factory.BlockTarget).ConfigureAwait(false)).FeeRate;
|
||||
|
@ -20,9 +20,9 @@ namespace BTCPayServer.Services
|
||||
public HardwareWalletException(string message) : base(message) { }
|
||||
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
public class HardwareWalletService
|
||||
public class HardwareWalletService : IDisposable
|
||||
{
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport, IDisposable
|
||||
{
|
||||
private readonly WebSocket webSocket;
|
||||
|
||||
@ -33,26 +33,40 @@ namespace BTCPayServer.Services
|
||||
this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public async Task<byte[][]> Exchange(byte[][] apdus)
|
||||
{
|
||||
await _Semaphore.WaitAsync();
|
||||
List<byte[]> responses = new List<byte[]>();
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||
try
|
||||
{
|
||||
foreach (var apdu in apdus)
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Semaphore.Release();
|
||||
}
|
||||
return responses.ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Semaphore.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LedgerClient _Ledger;
|
||||
@ -118,21 +132,10 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
if (!strategy.Segwit)
|
||||
return false;
|
||||
return await GetKeyPath(_Ledger, network, strategy) != null;
|
||||
}
|
||||
|
||||
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||
{
|
||||
List<KeyPath> derivations = new List<KeyPath>();
|
||||
if(network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
derivations.Add(new KeyPath("49'"));
|
||||
derivations.Add(new KeyPath("44'"));
|
||||
KeyPath foundKeyPath = null;
|
||||
@ -143,7 +146,7 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
var extpubkey = await GetExtPubKey(ledger, network, account, true);
|
||||
var extpubkey = await GetExtPubKey(_Ledger, network, account, true);
|
||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
||||
{
|
||||
foundKeyPath = account;
|
||||
@ -159,79 +162,18 @@ namespace BTCPayServer.Services
|
||||
return foundKeyPath;
|
||||
}
|
||||
|
||||
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
|
||||
ReceivedCoin[] coins, BTCPayNetwork network,
|
||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
||||
FeeRate feeRate,
|
||||
IDestination changeAddress,
|
||||
KeyPath changeKeyPath,
|
||||
FeeRate minTxRelayFee)
|
||||
public async Task<Transaction> SignTransactionAsync(SignatureRequest[] signatureRequests,
|
||||
Transaction unsigned,
|
||||
KeyPath changeKeyPath)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (feeRate == null)
|
||||
throw new ArgumentNullException(nameof(feeRate));
|
||||
if (changeAddress == null)
|
||||
throw new ArgumentNullException(nameof(changeAddress));
|
||||
if (feeRate.FeePerK <= Money.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
|
||||
}
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
|
||||
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress);
|
||||
builder.SendEstimatedFees(feeRate);
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach(var c in coins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
||||
var fullySigned = await Ledger.SignTransactionAsync(
|
||||
usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(),
|
||||
unsigned,
|
||||
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
|
||||
return fullySigned;
|
||||
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if(_Transport != null)
|
||||
_Transport.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ using NBXplorer.Models;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
@ -100,7 +102,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
HighSpeed = 0,
|
||||
MediumSpeed = 1,
|
||||
LowSpeed = 2
|
||||
LowSpeed = 2,
|
||||
LowMediumSpeed = 3
|
||||
}
|
||||
public class InvoiceEntity
|
||||
{
|
||||
@ -314,6 +317,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
public bool ExtendedNotifications { get; set; }
|
||||
public List<InvoiceEventData> Events { get; internal set; }
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
@ -334,19 +338,34 @@ namespace BTCPayServer.Services.Invoices
|
||||
ExpirationTime = ExpirationTime,
|
||||
Status = Status,
|
||||
Currency = ProductInformation.Currency,
|
||||
Flags = new Flags() { Refundable = Refundable }
|
||||
Flags = new Flags() { Refundable = Refundable },
|
||||
PaymentSubtotals = new Dictionary<string, long>(),
|
||||
PaymentTotals = new Dictionary<string, long>(),
|
||||
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>(),
|
||||
Addresses = new Dictionary<string, string>(),
|
||||
PaymentCodes = new Dictionary<string, InvoicePaymentUrls>(),
|
||||
ExchangeRates = new Dictionary<string, Dictionary<string, decimal>>()
|
||||
};
|
||||
|
||||
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
|
||||
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
|
||||
dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
|
||||
foreach (var info in this.GetPaymentMethods(networkProvider))
|
||||
{
|
||||
var accounting = info.Calculate();
|
||||
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
|
||||
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
|
||||
var subtotalPrice = accounting.TotalDue - accounting.NetworkFee;
|
||||
var cryptoCode = info.GetId().CryptoCode;
|
||||
var address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
|
||||
var exrates = new Dictionary<string, decimal>
|
||||
{
|
||||
{ ProductInformation.Currency, cryptoInfo.Rate }
|
||||
};
|
||||
|
||||
cryptoInfo.CryptoCode = cryptoCode;
|
||||
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
||||
cryptoInfo.Rate = info.Rate;
|
||||
cryptoInfo.Price = (accounting.TotalDue - accounting.NetworkFee).ToString();
|
||||
cryptoInfo.Price = subtotalPrice.ToString();
|
||||
|
||||
cryptoInfo.Due = accounting.Due.ToString();
|
||||
cryptoInfo.Paid = accounting.Paid.ToString();
|
||||
@ -355,17 +374,19 @@ namespace BTCPayServer.Services.Invoices
|
||||
cryptoInfo.TxCount = accounting.TxCount;
|
||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
|
||||
|
||||
cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
|
||||
cryptoInfo.ExRates = new Dictionary<string, double>
|
||||
{
|
||||
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
|
||||
};
|
||||
cryptoInfo.Address = address;
|
||||
|
||||
cryptoInfo.ExRates = exrates;
|
||||
var paymentId = info.GetId();
|
||||
var scheme = info.Network.UriScheme;
|
||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"i/{paymentId}/{Id}";
|
||||
|
||||
if (paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var minerInfo = new MinerFeeInfo();
|
||||
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
|
||||
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi;
|
||||
dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo);
|
||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
@ -375,7 +396,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
@ -394,10 +415,23 @@ namespace BTCPayServer.Services.Invoices
|
||||
dto.BTCDue = cryptoInfo.Due;
|
||||
dto.PaymentUrls = cryptoInfo.PaymentUrls;
|
||||
}
|
||||
|
||||
#pragma warning restore CS0618
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
|
||||
dto.PaymentCodes.Add(paymentId.ToString(), cryptoInfo.PaymentUrls);
|
||||
dto.PaymentSubtotals.Add(paymentId.ToString(), subtotalPrice.Satoshi);
|
||||
dto.PaymentTotals.Add(paymentId.ToString(), accounting.TotalDue.Satoshi);
|
||||
dto.SupportedTransactionCurrencies.TryAdd(cryptoCode, new InvoiceSupportedTransactionCurrency()
|
||||
{
|
||||
Enabled = true
|
||||
});
|
||||
dto.Addresses.Add(paymentId.ToString(), address);
|
||||
dto.ExchangeRates.TryAdd(cryptoCode, exrates);
|
||||
}
|
||||
|
||||
//dto.AmountPaid dto.MinerFees & dto.TransactionCurrency are not supported by btcpayserver as we have multi currency payment support per invoice
|
||||
|
||||
Populate(ProductInformation, dto);
|
||||
Populate(BuyerInformation, dto);
|
||||
|
||||
@ -523,6 +557,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
public Money NetworkFee { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum required to be paid in order to accept invocie as paid
|
||||
/// </summary>
|
||||
public Money MinimumTotalDue { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentMethod
|
||||
@ -671,6 +709,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||
accounting.MinimumTotalDue = Money.Max(Money.Satoshis(1), Money.Satoshis(accounting.TotalDue.Satoshi * (1.0m - ((decimal)ParentEntity.PaymentTolerance / 100.0m))));
|
||||
return accounting;
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
context.Invoices.Add(new InvoiceData()
|
||||
context.Invoices.Add(new Data.InvoiceData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Id = invoice.Id,
|
||||
@ -267,7 +267,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
|
||||
@ -307,7 +307,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
invoiceData.Status = status;
|
||||
@ -320,7 +320,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
var invoiceData = await context.FindAsync<Data.InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData?.Status != "paid")
|
||||
return;
|
||||
invoiceData.Status = "invalid";
|
||||
@ -331,7 +331,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
IQueryable<InvoiceData> query =
|
||||
IQueryable<Data.InvoiceData> query =
|
||||
context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
@ -351,7 +351,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
private InvoiceEntity ToEntity(InvoiceData invoice)
|
||||
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
|
||||
{
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
|
||||
#pragma warning disable CS0618
|
||||
@ -386,7 +386,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
IQueryable<InvoiceData> query = context
|
||||
IQueryable<Data.InvoiceData> query = context
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
.Include(o => o.RefundAddresses);
|
||||
@ -436,6 +436,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => statusSet.Contains(i.Status));
|
||||
}
|
||||
|
||||
if(queryObject.Unusual != null)
|
||||
{
|
||||
var unused = queryObject.Unusual.Value;
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
|
||||
}
|
||||
|
||||
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
|
||||
{
|
||||
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
|
||||
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
|
||||
}
|
||||
|
||||
query = query.OrderByDescending(q => q.Created);
|
||||
|
||||
if (queryObject.Skip != null)
|
||||
@ -451,6 +463,29 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
}
|
||||
|
||||
private string NormalizeExceptionStatus(string status)
|
||||
{
|
||||
status = status.ToLowerInvariant();
|
||||
switch (status)
|
||||
{
|
||||
case "paidover":
|
||||
case "over":
|
||||
case "overpaid":
|
||||
status = "paidOver";
|
||||
break;
|
||||
case "paidlate":
|
||||
case "late":
|
||||
status = "paidLate";
|
||||
break;
|
||||
case "paidpartial":
|
||||
case "underpaid":
|
||||
case "partial":
|
||||
status = "paidPartial";
|
||||
break;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
|
||||
{
|
||||
if (outputs.Length == 0)
|
||||
@ -614,10 +649,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool? Unusual { get; set; }
|
||||
|
||||
public string[] Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string[] ExceptionStatus
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string InvoiceId
|
||||
{
|
||||
get;
|
||||
|
@ -24,8 +24,8 @@ namespace BTCPayServer.Services.Mails
|
||||
}
|
||||
public async Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||
if (settings == null)
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (!settings.IsComplete())
|
||||
{
|
||||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||
return;
|
||||
@ -36,8 +36,8 @@ namespace BTCPayServer.Services.Mails
|
||||
|
||||
public async Task SendMailCore(string email, string subject, string message)
|
||||
{
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||
if (settings == null)
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
|
||||
if (!settings.IsComplete())
|
||||
throw new InvalidOperationException("Email settings not configured");
|
||||
var smtp = settings.CreateSmtpClient();
|
||||
MailMessage mail = new MailMessage(settings.From, email, subject, message);
|
||||
|
@ -40,10 +40,22 @@ namespace BTCPayServer.Services.Mails
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool IsComplete()
|
||||
{
|
||||
SmtpClient smtp = null;
|
||||
try
|
||||
{
|
||||
smtp = CreateSmtpClient();
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public SmtpClient CreateSmtpClient()
|
||||
{
|
||||
SmtpClient client = new SmtpClient(Server, Port.Value);
|
||||
client.EnableSsl = true;
|
||||
client.EnableSsl = EnableSSL;
|
||||
client.UseDefaultCredentials = false;
|
||||
client.Credentials = new NetworkCredential(Login, Password);
|
||||
client.DeliveryMethod = SmtpDeliveryMethod.Network;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
@ -8,12 +9,14 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
public class PoliciesSettings
|
||||
{
|
||||
[Display(Name = "Requires a confirmation mail for registering")]
|
||||
public bool RequiresConfirmedEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Disable registration")]
|
||||
public bool LockSubscription { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using ExchangeSharp;
|
||||
@ -35,7 +36,6 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
IMemoryCache _Cache;
|
||||
private IOptions<MemoryCacheOptions> _CacheOptions;
|
||||
|
||||
public IMemoryCache Cache
|
||||
{
|
||||
get
|
||||
@ -68,10 +68,12 @@ namespace BTCPayServer.Services.Rates
|
||||
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
|
||||
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
|
||||
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
|
||||
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
|
||||
|
||||
// Handmade providers
|
||||
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
|
||||
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
|
||||
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings });
|
||||
|
||||
// Those exchanges make multiple requests when calling GetTickers so we remove them
|
||||
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
|
||||
@ -82,6 +84,20 @@ namespace BTCPayServer.Services.Rates
|
||||
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
|
||||
}
|
||||
|
||||
public CoinAverageExchanges GetSupportedExchanges()
|
||||
{
|
||||
CoinAverageExchanges exchanges = new CoinAverageExchanges();
|
||||
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
|
||||
{
|
||||
exchanges.Add(exchange.Value);
|
||||
}
|
||||
|
||||
// Add other exchanges supported here
|
||||
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
|
||||
|
||||
return exchanges;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
|
||||
public Dictionary<string, IRateProvider> DirectProviders
|
||||
|
@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
|
||||
.AllRates
|
||||
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
|
||||
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), BidAsk = new BidAsk(r.Value) })
|
||||
.ToList());
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Rates
|
||||
public const string CoinAverageName = "coinaverage";
|
||||
public CoinAverageRateProvider()
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
static HttpClient _Client = new HttpClient();
|
||||
|
||||
@ -69,10 +69,31 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public ICoinAverageAuthenticator Authenticator { get; set; }
|
||||
|
||||
private bool TryToDecimal(JProperty p, out decimal v)
|
||||
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
|
||||
{
|
||||
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
bidAsk = null;
|
||||
if (Exchange == CoinAverageName)
|
||||
{
|
||||
JToken last = p.Value["last"];
|
||||
if (!decimal.TryParse(last.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) ||
|
||||
v <= 0)
|
||||
return false;
|
||||
bidAsk = new BidAsk(v);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
JToken bid = p.Value["bid"];
|
||||
JToken ask = p.Value["ask"];
|
||||
if (bid == null || ask == null ||
|
||||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
|
||||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
|
||||
v1 > v2 ||
|
||||
v1 <= 0 || v2 <= 0)
|
||||
return false;
|
||||
bidAsk = new BidAsk(v1, v2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ExchangeRates> GetRatesAsync()
|
||||
@ -108,10 +129,10 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
ExchangeRate exchangeRate = new ExchangeRate();
|
||||
exchangeRate.Exchange = Exchange;
|
||||
if (!TryToDecimal(prop, out decimal value))
|
||||
if (!TryToBidAsk(prop, out var value))
|
||||
continue;
|
||||
exchangeRate.Value = value;
|
||||
if(CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
exchangeRate.BidAsk = value;
|
||||
if (CurrencyPair.TryParse(prop.Name, out var pair))
|
||||
{
|
||||
exchangeRate.CurrencyPair = pair;
|
||||
exchangeRates.Add(exchangeRate);
|
||||
|
@ -43,12 +43,11 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public CoinAverageExchanges()
|
||||
{
|
||||
Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
|
||||
}
|
||||
|
||||
public void Add(CoinAverageExchange exchange)
|
||||
{
|
||||
Add(exchange.Name, exchange);
|
||||
TryAdd(exchange.Name, exchange);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
|
@ -31,6 +31,7 @@ namespace BTCPayServer.Services.Rates
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public bool Crypto { get; set; }
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
@ -40,6 +41,27 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
|
||||
static Dictionary<string, IFormatProvider> _CurrencyProviders = new Dictionary<string, IFormatProvider>();
|
||||
|
||||
public NumberFormatInfo GetNumberFormatInfo(string currency, bool useFallback)
|
||||
{
|
||||
var data = GetCurrencyProvider(currency);
|
||||
if (data is NumberFormatInfo nfi)
|
||||
return nfi;
|
||||
if (data is CultureInfo ci)
|
||||
return ci.NumberFormat;
|
||||
if (!useFallback)
|
||||
return null;
|
||||
return CreateFallbackCurrencyFormatInfo(currency);
|
||||
}
|
||||
|
||||
private NumberFormatInfo CreateFallbackCurrencyFormatInfo(string currency)
|
||||
{
|
||||
var usd = GetNumberFormatInfo("USD", false);
|
||||
var currencyInfo = (NumberFormatInfo)usd.Clone();
|
||||
currencyInfo.CurrencySymbol = currency;
|
||||
return currencyInfo;
|
||||
}
|
||||
|
||||
public IFormatProvider GetCurrencyProvider(string currency)
|
||||
{
|
||||
lock (_CurrencyProviders)
|
||||
@ -54,7 +76,11 @@ namespace BTCPayServer.Services.Rates
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
AddCurrency(_CurrencyProviders, "BTC", 8, "BTC");
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
{
|
||||
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
|
||||
}
|
||||
}
|
||||
return _CurrencyProviders.TryGet(currency);
|
||||
}
|
||||
@ -106,13 +132,38 @@ namespace BTCPayServer.Services.Rates
|
||||
info.Symbol = splitted[3];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
{
|
||||
dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||
{
|
||||
Code = network.CryptoCode,
|
||||
Divisibility = 8,
|
||||
Name = network.CryptoCode,
|
||||
Crypto = true
|
||||
});
|
||||
}
|
||||
|
||||
return dico.Values.ToArray();
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency)
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
||||
{
|
||||
CurrencyData result;
|
||||
_Currencies.TryGetValue(currency.ToUpperInvariant(), out result);
|
||||
if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
|
||||
{
|
||||
if(useFallback)
|
||||
{
|
||||
var usd = GetCurrencyData("USD", false);
|
||||
result = new CurrencyData()
|
||||
{
|
||||
Code = currency,
|
||||
Crypto = true,
|
||||
Name = currency,
|
||||
Divisibility = usd.Divisibility
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user