Compare commits
305 Commits
Author | SHA1 | Date | |
---|---|---|---|
480afebcd9 | |||
96721e95a2 | |||
883cd41232 | |||
3f48a478af | |||
8d3b45bdec | |||
bbd19a96ec | |||
ce17e3212a | |||
c3ea63c6ce | |||
1ee4bd9c92 | |||
e6bb6619e5 | |||
cc29d863d7 | |||
8cb2c93abd | |||
2187e05a10 | |||
4c07483383 | |||
65916755b6 | |||
3cefbc89e4 | |||
c40b47b1dd | |||
a2d17bfa7e | |||
cdf0c6d27d | |||
8154986102 | |||
203494e809 | |||
d49bbc95af | |||
c44132fd35 | |||
c75512303d | |||
97d50df13e | |||
0e1ef78af1 | |||
464ab30fea | |||
3ee1c05646 | |||
c54c39926b | |||
33d18a3278 | |||
97e564901e | |||
832069dd44 | |||
1ac17e96c3 | |||
d907031ec7 | |||
4b5af9cb5c | |||
0057146fee | |||
0c8925d2a2 | |||
b9e5b0d56e | |||
eb6dbd1247 | |||
fe8428b8b0 | |||
f2aa15310a | |||
1814cb2d6e | |||
94a6f20a05 | |||
22e700a53e | |||
cd78e559cf | |||
f0257fb8f7 | |||
34cdbf73f0 | |||
b291a6d25a | |||
fa7e974e73 | |||
976d9d0cda | |||
6ea2d9175d | |||
10ceddc709 | |||
5dd57c8064 | |||
a256dd3277 | |||
5ee9a92f1e | |||
65c7c85c14 | |||
27b686095c | |||
cd2fef0dab | |||
743288fa47 | |||
270ebead49 | |||
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>
|
||||
|
@ -120,7 +120,7 @@ namespace BTCPayServer.Tests
|
||||
.Build();
|
||||
_Host.Start();
|
||||
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
|
||||
|
||||
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
|
||||
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
|
||||
rateProvider.DirectProviders.Clear();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -152,6 +152,7 @@ namespace BTCPayServer.Tests
|
||||
internal set;
|
||||
}
|
||||
public InvoiceRepository InvoiceRepository { get; private set; }
|
||||
public StoreRepository StoreRepository { get; private set; }
|
||||
public Uri IntegratedLightning { get; internal set; }
|
||||
public bool InContainer { get; internal set; }
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
245
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
245
BTCPayServer.Tests/Lnd/UnitTests.cs
Normal file
@ -0,0 +1,245 @@
|
||||
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;
|
||||
using Xunit.Sdk;
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
await EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await InvoiceClient.GetInvoice(merchantInvoice.Id);
|
||||
Assert.True(invoice.PaidAt.HasValue);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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=http://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,54 @@ 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)
|
||||
{
|
||||
bool awaitingLocking = false;
|
||||
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();
|
||||
ExplorerNode.Generate(awaitingLocking ? 1 : 10);
|
||||
await WaitLNSynched(client, CustomerLightningD, MerchantLightningD);
|
||||
awaitingLocking = true;
|
||||
break;
|
||||
case "CHANNELD_NORMAL":
|
||||
return;
|
||||
@ -122,23 +139,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 +175,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,106 +210,18 @@ 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;
|
||||
}
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach(var store in Stores)
|
||||
{
|
||||
Xunit.Assert.True(PayTester.StoreRepository.DeleteStore(store).GetAwaiter().GetResult());
|
||||
}
|
||||
if (PayTester != null)
|
||||
PayTester.Dispose();
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ namespace BTCPayServer.Tests
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
parent.Stores.Add(StoreId);
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
@ -117,7 +118,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
@ -126,15 +127,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,21 @@ 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,
|
||||
SkipPortTest = true // We can't test this as the IP can't be resolved by the test host :(
|
||||
}, "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 +436,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 +587,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 +646,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 +690,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 +748,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 +824,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 +870,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 +912,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 +936,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 +954,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 +989,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 +1015,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 +1102,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 +1128,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 +1186,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 +1278,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 +1291,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.5,
|
||||
Price = 5.5m,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1083,9 +1338,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 +1362,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 +1392,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 +1402,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 +1499,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 +1543,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 +1555,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 +1567,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 +1614,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,8 @@ 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; }
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[Obsolete("Should not be needed")]
|
||||
public bool IsBTC
|
||||
|
@ -17,12 +17,13 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Bitcoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
CryptoImagePath = "imlegacy/bitcoin.svg",
|
||||
LightningImagePath = "imlegacy/bitcoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
|
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
30
BTCPayServer/BTCPayNetworkProvider.BitcoinGold.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoinGold()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTG");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "BGold",
|
||||
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.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("156'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,14 +16,20 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Dogecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Feathercoin.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 InitFeathercoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Feathercoin",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Groestlcoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
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,
|
||||
DisplayName = "Groestlcoin",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -16,12 +16,13 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Litecoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
CryptoImagePath = "imlegacy/litecoin.svg",
|
||||
LightningImagePath = "imlegacy/litecoin-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
});
|
||||
|
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
36
BTCPayServer/BTCPayNetworkProvider.Monacoin.cs
Normal file
@ -0,0 +1,36 @@
|
||||
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,
|
||||
DisplayName = "Monacoin",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Polis.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Polis.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 InitPolis()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Polis",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Ufo.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Ufo.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 InitUfo()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("UFO");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Ufo",
|
||||
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'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.cs
Normal file
35
BTCPayServer/BTCPayNetworkProvider.Viacoin.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 InitViacoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("VIA");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
DisplayName = "Viacoin",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.viacoin.org/tx/{0}" : "https://explorer.viacoin.org/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "viacoin",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"VIA_X = VIA_BTC * BTC_X",
|
||||
"VIA_BTC = bittrex(VIA_BTC)"
|
||||
},
|
||||
CryptoImagePath = "imlegacy/viacoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("14'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,13 @@ namespace BTCPayServer
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
InitBitcoinGold();
|
||||
InitMonacoin();
|
||||
InitPolis();
|
||||
InitFeathercoin();
|
||||
InitGroestlcoin();
|
||||
InitViacoin();
|
||||
//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.53</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.27" />
|
||||
<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.13" />
|
||||
<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>
|
||||
|
||||
@ -114,6 +111,7 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -77,11 +77,18 @@ 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;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
|
||||
error);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -27,19 +27,19 @@ namespace BTCPayServer.Configuration
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle javascript and css files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to a PostgreSQL database (default: SQLite)", CommandOptionType.SingleValue);
|
||||
app.Option("--externalurl", $"The expected external URL of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);
|
||||
app.Option("--rootpath", "The root path in the URL to access BTCPay (default: /)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorerurl", $"URL of the NBXplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server adnistrator: Must be a unix socket of CLightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue);
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
@ -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,
|
||||
@ -118,7 +162,8 @@ namespace BTCPayServer.Controllers
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.Apps
|
||||
.Where(us => us.Id == appId && us.AppType == appType.ToString())
|
||||
.Where(us => us.Id == appId &&
|
||||
us.AppType == appType.ToString())
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
@ -162,7 +207,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 +224,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 +237,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);
|
||||
}
|
||||
@ -172,6 +175,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
[ReferrerPolicyAttribute("origin")]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
@ -183,6 +187,20 @@ namespace BTCPayServer.Controllers
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
|
||||
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
|
||||
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
|
||||
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
|
||||
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
|
||||
{
|
||||
_CSP.Clear();
|
||||
}
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
@ -201,6 +219,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 +234,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);
|
||||
@ -223,6 +248,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
PaymentMethodName = GetDisplayName(paymentMethodId, network),
|
||||
CryptoImage = GetImage(paymentMethodId, network),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
@ -232,15 +259,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 :
|
||||
@ -253,7 +281,6 @@ namespace BTCPayServer.Controllers
|
||||
TxCount = accounting.TxRequired,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
AllowCoinConversion = storeBlob.AllowCoinConversion,
|
||||
@ -262,10 +289,14 @@ namespace BTCPayServer.Controllers
|
||||
.Select(kv => new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||
CryptoCode = kv.GetId().CryptoCode,
|
||||
PaymentMethodName = GetDisplayName(kv.GetId(), kv.Network),
|
||||
IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike,
|
||||
CryptoImage = GetImage(kv.GetId(), kv.Network),
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
.OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
@ -273,19 +304,58 @@ namespace BTCPayServer.Controllers
|
||||
return model;
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
||||
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
network.DisplayName : network.DisplayName + " - Lightning";
|
||||
}
|
||||
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
|
||||
Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
|
||||
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 +384,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 +440,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 +490,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 +563,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));
|
||||
}
|
||||
|
||||
|
@ -41,12 +41,14 @@ using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ContentSecurityPolicies _CSP;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ContentSecurityPolicies csp,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
_CSP = csp;
|
||||
}
|
||||
|
||||
|
||||
@ -98,6 +102,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,10 +113,12 @@ 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);
|
||||
|
||||
@ -140,32 +147,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 +182,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 +194,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,9 +204,9 @@ 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" };
|
||||
}
|
||||
@ -276,6 +277,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
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Validations;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -25,14 +26,17 @@ namespace BTCPayServer.Controllers
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
private BTCPayRateProviderFactory _RateProviderFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
|
||||
public ServerController(UserManager<ApplicationUser> userManager,
|
||||
BTCPayRateProviderFactory rateProviderFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
SettingsRepository settingsRepository,
|
||||
Services.Stores.StoreRepository storeRepository)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
@ -188,6 +192,7 @@ namespace BTCPayServer.Controllers
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
await _UserManager.DeleteAsync(user);
|
||||
await _StoreRepository.CleanUnreachableStores();
|
||||
StatusMessage = "User deleted";
|
||||
return RedirectToAction(nameof(ListUsers));
|
||||
}
|
||||
@ -243,10 +248,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;
|
||||
@ -286,7 +283,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CurrencyPair = fetch.Key.ToString(),
|
||||
Error = testResult.Errors.Count != 0,
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
|
||||
: testResult.EvaluatedRule
|
||||
});
|
||||
}
|
||||
@ -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"
|
||||
});
|
||||
@ -427,10 +424,12 @@ namespace BTCPayServer.Controllers
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -470,10 +469,8 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model)
|
||||
public async Task<IActionResult> UpdateStore(StoreViewModel model, string command = null)
|
||||
{
|
||||
AddPaymentMethods(StoreData, model);
|
||||
|
||||
bool needUpdate = false;
|
||||
if (StoreData.SpeedPolicy != model.SpeedPolicy)
|
||||
{
|
||||
@ -496,6 +493,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))
|
||||
{
|
||||
@ -512,11 +510,34 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
storeId = StoreData.Id
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Action = "Delete this store",
|
||||
Title = "Delete this store",
|
||||
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
|
||||
ButtonClass = "btn-danger"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
await _Repo.DeleteStore(StoreData.Id);
|
||||
StatusMessage = "Success: Store successfully deleted";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
}
|
||||
|
||||
private CoinAverageExchange[] GetSupportedExchanges()
|
||||
{
|
||||
return _CoinAverage.AvailableExchanges
|
||||
return _RateFactory.GetSupportedExchanges()
|
||||
.Select(c => c.Value)
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
@ -644,11 +665,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);
|
||||
}
|
||||
|
@ -35,21 +35,7 @@ namespace BTCPayServer.Controllers
|
||||
_NetworkProvider = networkProvider;
|
||||
_UserManager = userManager;
|
||||
_WalletProvider = walletProvider;
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("create")]
|
||||
@ -63,8 +49,23 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete store " + store.StoreName,
|
||||
Description = "This store will still be accessible to users sharing it",
|
||||
Action = "Delete"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/delete")]
|
||||
[Route("{storeId}/me/delete")]
|
||||
public async Task<IActionResult> DeleteStorePost(string storeId)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
@ -121,7 +122,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);
|
||||
|
@ -19,5 +19,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -102,14 +102,27 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.Entity<InvoiceData>()
|
||||
.HasIndex(o => o.StoreDataId);
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceData>().HasIndex(o => o.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PaymentData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<RefundAddressesData>()
|
||||
.HasIndex(o => o.InvoiceDataId);
|
||||
|
||||
builder.Entity<UserStore>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<UserStore>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
@ -117,9 +130,16 @@ namespace BTCPayServer.Data
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.APIKeys)
|
||||
.HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(a => a.StoreData);
|
||||
|
||||
@ -133,6 +153,10 @@ namespace BTCPayServer.Data
|
||||
.WithMany(t => t.UserStores)
|
||||
.HasForeignKey(pt => pt.StoreDataId);
|
||||
|
||||
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<AddressInvoiceData>()
|
||||
#pragma warning disable CS0618
|
||||
.HasKey(o => o.Address);
|
||||
@ -141,12 +165,24 @@ namespace BTCPayServer.Data
|
||||
builder.Entity<PairingCodeData>()
|
||||
.HasKey(o => o.Id);
|
||||
|
||||
builder.Entity<PendingInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(o => o.PendingInvoices)
|
||||
.HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
|
||||
builder.Entity<PairedSINData>()
|
||||
.HasOne(o => o.StoreData)
|
||||
.WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<PairedSINData>(b =>
|
||||
{
|
||||
b.HasIndex(o => o.SIN);
|
||||
b.HasIndex(o => o.StoreDataId);
|
||||
});
|
||||
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<HistoricalAddressInvoiceData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
@ -156,6 +192,10 @@ namespace BTCPayServer.Data
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasOne(o => o.InvoiceData)
|
||||
.WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
@ -24,6 +29,15 @@ namespace BTCPayServer.Data
|
||||
_Type = type;
|
||||
}
|
||||
|
||||
|
||||
public DatabaseType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Type;
|
||||
}
|
||||
}
|
||||
|
||||
public ApplicationDbContext CreateContext()
|
||||
{
|
||||
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
|
||||
@ -31,20 +45,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some crypto currencies share same address prefix
|
||||
/// For not having exceptions thrown by two address on different network, we suffix by "#CRYPTOCODE"
|
||||
|
@ -80,5 +80,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
|
@ -21,6 +21,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public string Label
|
||||
{
|
||||
get;
|
||||
|
@ -11,5 +11,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -34,12 +34,13 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<AppData> Apps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
{
|
||||
@ -192,6 +193,8 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
[Obsolete("Use GetDefaultCrypto instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public string GetDefaultCrypto()
|
||||
@ -247,6 +250,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
@ -326,6 +330,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)
|
||||
|
@ -31,33 +31,12 @@ using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
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();
|
||||
@ -104,6 +83,15 @@ namespace BTCPayServer
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static bool SupportDropForeignKey(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
public static bool SupportDropForeignKey(this DatabaseFacade facade)
|
||||
{
|
||||
return facade.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
@ -120,6 +108,26 @@ namespace BTCPayServer
|
||||
return str + "/";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
if (resp.HasStarted)
|
||||
return;
|
||||
resp.OnStarting(() =>
|
||||
{
|
||||
SetHeader(resp, name, value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetHeader(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
var existing = resp.Headers[name].FirstOrDefault();
|
||||
if (existing != null && value == null)
|
||||
resp.Headers.Remove(name);
|
||||
else
|
||||
resp.Headers[name] = value;
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
@ -131,7 +139,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 +171,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 +187,30 @@ namespace BTCPayServer
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "BitpayAuth", value);
|
||||
}
|
||||
|
||||
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return await doing;
|
||||
}
|
||||
}
|
||||
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var delayCTS = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
var waiting = Task.Delay(-1, delayCTS.Token);
|
||||
var doing = task;
|
||||
await Task.WhenAny(waiting, doing);
|
||||
delayCTS.Cancel();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
public static (string Signature, String Id, String Authorization) GetBitpayAuth(this HttpContext ctx)
|
||||
{
|
||||
ctx.Items.TryGetValue("BitpayAuth", out object obj);
|
||||
|
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IContentSecurityPolicy : IFilterMetadata { }
|
||||
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool AutoSelf { get; set; } = true;
|
||||
public bool UnsafeInline { get; set; } = true;
|
||||
public bool FixWebsocket { get; set; } = true;
|
||||
public string FontSrc { get; set; } = null;
|
||||
public string ImgSrc { get; set; } = null;
|
||||
public string DefaultSrc { get; set; }
|
||||
public string StyleSrc { get; set; }
|
||||
public string ScriptSrc { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
|
||||
{
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (policies == null)
|
||||
return;
|
||||
if (DefaultSrc != null)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
|
||||
}
|
||||
if (UnsafeInline)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(FontSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ImgSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(StyleSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
|
||||
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
var url = string.Concat(
|
||||
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent());
|
||||
policies.Add(new ConsentSecurityPolicy("connect-src", url));
|
||||
}
|
||||
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
{
|
||||
bool hasSelf = false;
|
||||
foreach (var group in policies.Rules.GroupBy(p => p.Name))
|
||||
{
|
||||
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
|
||||
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
|
||||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
hasSelf = true;
|
||||
}
|
||||
if (hasSelf)
|
||||
{
|
||||
foreach (var authorized in policies.Authorized)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
30
BTCPayServer/Filters/ReferrerPolicyAttribute.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IReferrerPolicy : IFilterMetadata { }
|
||||
public class ReferrerPolicyAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public ReferrerPolicyAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public string Value { get; set; }
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Filters/XContentTypeOptionsAttribute.cs
Normal file
25
BTCPayServer/Filters/XContentTypeOptionsAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XContentTypeOptionsAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public XContentTypeOptionsAttribute(string value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public string Value { get; set; }
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["x-frame-options"].FirstOrDefault();
|
||||
if (existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("x-frame-options");
|
||||
else
|
||||
context.HttpContext.Response.Headers["x-frame-options"] = Value;
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
BTCPayServer/Filters/XXSSProtectionAttribute.cs
Normal file
23
BTCPayServer/Filters/XXSSProtectionAttribute.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public class XXSSProtectionAttribute : Attribute, IActionFilter
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -41,6 +43,40 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
|
||||
public bool ShowRegister { get; set; }
|
||||
|
||||
internal void Update(PoliciesSettings data)
|
||||
{
|
||||
ShowRegister = !data.LockSubscription;
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
|
||||
{
|
||||
public int Order => 1001;
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var manager = context.HttpContext.RequestServices.GetService(typeof(CssThemeManager)) as CssThemeManager;
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (manager != null && policies != null)
|
||||
{
|
||||
if(manager.CreativeStartUri != null && Uri.TryCreate(manager.CreativeStartUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (manager.BootstrapUri != null && Uri.TryCreate(manager.BootstrapUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
@ -58,10 +94,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)
|
||||
|
89
BTCPayServer/HostedServices/MigratorHostedService.cs
Normal file
89
BTCPayServer/HostedServices/MigratorHostedService.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class MigratorHostedService : BaseAsyncService
|
||||
{
|
||||
private ApplicationDbContextFactory _DBContextFactory;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private SettingsRepository _Settings;
|
||||
public MigratorHostedService(
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepository,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
_DBContextFactory = dbContextFactory;
|
||||
_StoreRepository = storeRepository;
|
||||
_NetworkProvider = networkProvider;
|
||||
_Settings = settingsRepository;
|
||||
}
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
Update()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task Update()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = (await _Settings.GetSettingAsync<MigrationSettings>()) ?? new MigrationSettings();
|
||||
if (!settings.DeprecatedLightningConnectionStringCheck)
|
||||
{
|
||||
await DeprecatedLightningConnectionStringCheck();
|
||||
settings.DeprecatedLightningConnectionStringCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.UnreachableStoreCheck)
|
||||
{
|
||||
await UnreachableStoreCheck();
|
||||
settings.UnreachableStoreCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Error on the MigratorHostedService");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnreachableStoreCheck()
|
||||
{
|
||||
return _StoreRepository.CleanUnreachableStores();
|
||||
}
|
||||
|
||||
private async Task DeprecatedLightningConnectionStringCheck()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
foreach (var method in store.GetSupportedPaymentMethods(_NetworkProvider).OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||
{
|
||||
var lightning = method.GetLightningUrl();
|
||||
if (lightning.IsLegacy)
|
||||
{
|
||||
method.SetLightningUrl(lightning);
|
||||
store.SetSupportedPaymentMethod(method.PaymentId, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -105,7 +104,9 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
@ -118,7 +119,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 +138,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,11 +35,11 @@ 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;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -80,8 +80,19 @@ namespace BTCPayServer.Hosting
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
o.Filters.Add(new XFrameOptionsAttribute("DENY"));
|
||||
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
|
||||
o.Filters.Add(new XXSSProtectionAttribute());
|
||||
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
|
||||
//o.Filters.Add(new ContentSecurityPolicyAttribute()
|
||||
//{
|
||||
// FontSrc = "'self' https://fonts.gstatic.com/",
|
||||
// ImgSrc = "'self' data:",
|
||||
// DefaultSrc = "'none'",
|
||||
// StyleSrc = "'self' 'unsafe-inline'",
|
||||
// ScriptSrc = "'self' 'unsafe-inline'"
|
||||
//});
|
||||
});
|
||||
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
@ -104,10 +115,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 +153,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)
|
||||
},
|
||||
|
578
BTCPayServer/Migrations/20180719095626_CanDeleteStores.Designer.cs
generated
Normal file
578
BTCPayServer/Migrations/20180719095626_CanDeleteStores.Designer.cs
generated
Normal file
@ -0,0 +1,578 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180719095626_CanDeleteStores")]
|
||||
partial class CanDeleteStores
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
172
BTCPayServer/Migrations/20180719095626_CanDeleteStores.cs
Normal file
172
BTCPayServer/Migrations/20180719095626_CanDeleteStores.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class CanDeleteStores : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (this.SupportDropForeignKey(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ApiKeys_Stores_StoreId",
|
||||
table: "ApiKeys",
|
||||
column: "StoreId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PairedSINData_Stores_StoreDataId",
|
||||
table: "PairedSINData",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
table: "PendingInvoices",
|
||||
column: "Id",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ApiKeys_Stores_StoreId",
|
||||
table: "ApiKeys");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PairedSINData_Stores_StoreDataId",
|
||||
table: "PairedSINData");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_PendingInvoices_Invoices_Id",
|
||||
table: "PendingInvoices");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_AddressInvoices_Invoices_InvoiceDataId",
|
||||
table: "AddressInvoices",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Apps_Stores_StoreDataId",
|
||||
table: "Apps",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Invoices_Stores_StoreDataId",
|
||||
table: "Invoices",
|
||||
column: "StoreDataId",
|
||||
principalTable: "Stores",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Payments_Invoices_InvoiceDataId",
|
||||
table: "Payments",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_RefundAddresses_Invoices_InvoiceDataId",
|
||||
table: "RefundAddresses",
|
||||
column: "InvoiceDataId",
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
@ -18,7 +14,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
.HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -202,8 +198,7 @@ namespace BTCPayServer.Migrations
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
b.Property<string>("Id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@ -442,19 +437,29 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("APIKeys")
|
||||
.HasForeignKey("StoreId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId");
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
@ -463,30 +468,49 @@ namespace BTCPayServer.Migrations
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
.WithMany("Invoices")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("PairedSINs")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("PendingInvoices")
|
||||
.HasForeignKey("Id")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
|
@ -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;
|
||||
|
@ -12,6 +12,9 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string PaymentMethodName { get; set; }
|
||||
public bool IsLightning { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
public string HtmlTitle { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
@ -30,13 +33,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string Status { get; set; }
|
||||
public string MerchantRefLink { get; set; }
|
||||
public int MaxTimeSeconds { get; set; }
|
||||
|
||||
// These properties are not used in client side code
|
||||
|
||||
public string StoreName { get; set; }
|
||||
public string ItemDesc { get; set; }
|
||||
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; }
|
||||
@ -44,12 +47,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string StoreEmail { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public decimal NetworkFee { get; set; }
|
||||
public bool IsMultiCurrency { get; set; }
|
||||
public int MaxTimeMinutes { get; internal set; }
|
||||
public string PaymentType { get; internal set; }
|
||||
public string PaymentMethodId { get; internal set; }
|
||||
public int MaxTimeMinutes { get; set; }
|
||||
public string PaymentType { get; set; }
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string PaymentMethodName { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
|
||||
public bool AllowCoinConversion { get; set; }
|
||||
public string PeerInfo { 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;
|
||||
@ -24,6 +25,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
}
|
||||
|
||||
public bool CanDelete { get; set; }
|
||||
public string Id { get; set; }
|
||||
[Display(Name = "Store Name")]
|
||||
[Required]
|
||||
@ -34,7 +36,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Url]
|
||||
[Uri]
|
||||
[Display(Name = "Store Website")]
|
||||
[MaxLength(500)]
|
||||
public string StoreWebsite
|
||||
@ -85,5 +87,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
|
||||
{
|
||||
@ -207,6 +207,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
||||
.Select(p => p.Outpoint.Hash)
|
||||
@ -315,6 +317,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
foreach (var invoiceId in invoices)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
if (invoice == null)
|
||||
continue;
|
||||
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
|
||||
var strategy = GetDerivationStrategy(invoice, network);
|
||||
if (strategy == null)
|
||||
@ -332,8 +336,12 @@ 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);
|
||||
totalPayment++;
|
||||
{
|
||||
invoice = await ReceivedPayment(wallet, invoice, payment, strategy);
|
||||
if(invoice == null)
|
||||
continue;
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalPayment;
|
||||
@ -346,10 +354,12 @@ 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));
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
@ -358,13 +368,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)
|
||||
@ -111,7 +123,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||
{
|
||||
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)), cancellation);
|
||||
await tcp.ConnectAsync(new IPEndPoint(address, nodeInfo.Port)).WithCancellation(cancellation);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -119,15 +131,5 @@ namespace BTCPayServer.Payments.Lightning
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {nodeInfo.Host}:{nodeInfo.Port} ({ex.Message})");
|
||||
}
|
||||
}
|
||||
|
||||
static Task WithTimeout(Task task, CancellationToken token)
|
||||
{
|
||||
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||
var registration = token.Register(() => { try { tcs.TrySetResult(true); } catch { } });
|
||||
#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler
|
||||
var timeoutTask = tcs.Task;
|
||||
#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler
|
||||
return Task.WhenAny(task, timeoutTask).Unwrap().ContinueWith(t => registration.Dispose(), TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
await EnsureListening(inv.InvoiceId, false);
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
}
|
||||
}));
|
||||
|
||||
@ -143,11 +143,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
private async Task Listen(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
ILightningListenInvoiceSession session = null;
|
||||
try
|
||||
{
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Start listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
var charge = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var session = await charge.Listen(_Cts.Token);
|
||||
var lightningClient = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
session = await lightningClient.Listen(_Cts.Token);
|
||||
while (true)
|
||||
{
|
||||
var notification = await session.WaitInvoice(_Cts.Token);
|
||||
@ -179,6 +180,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
Logs.PayServer.LogError(ex, $"{supportedPaymentMethod.CryptoCode} (Lightning): Error while contacting {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
DoneListening(supportedPaymentMethod.GetLightningUrl());
|
||||
}
|
||||
finally
|
||||
{
|
||||
session?.Dispose();
|
||||
}
|
||||
Logs.PayServer.LogInformation($"{supportedPaymentMethod.CryptoCode} (Lightning): Stop listening {supportedPaymentMethod.GetLightningUrl().BaseUri}");
|
||||
}
|
||||
|
||||
@ -189,8 +194,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);
|
||||
}
|
||||
}
|
273
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
273
BTCPayServer/Payments/Lightning/Lnd/LndInvoiceClient.cs
Normal file
@ -0,0 +1,273 @@
|
||||
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.Runtime.ExceptionServices;
|
||||
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();
|
||||
|
||||
|
||||
HttpClient _Client;
|
||||
HttpResponseMessage _Response;
|
||||
Stream _Body;
|
||||
StreamReader _Reader;
|
||||
Task _ListenLoop;
|
||||
|
||||
public LndInvoiceClientSession(LndSwaggerClient parent)
|
||||
{
|
||||
_Parent = parent;
|
||||
}
|
||||
|
||||
public Task StartListening()
|
||||
{
|
||||
try
|
||||
{
|
||||
_Client = _Parent.CreateHttpClient();
|
||||
_Client.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, _Parent.BaseUrl.WithTrailingSlash() + "v1/invoices/subscribe");
|
||||
_Parent._Authentication.AddAuthentication(request);
|
||||
_ListenLoop = ListenLoop(request);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ListenLoop(HttpRequestMessage request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_Response = await _Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _Cts.Token);
|
||||
_Body = await _Response.Content.ReadAsStreamAsync();
|
||||
_Reader = new StreamReader(_Body);
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
string line = await _Reader.ReadLineAsync().WithCancellation(_Cts.Token);
|
||||
if (line != null)
|
||||
{
|
||||
if (line.StartsWith("{\"result\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var invoiceString = JObject.Parse(line)["result"].ToString();
|
||||
LnrpcInvoice parsedInvoice = _Parent.Deserialize<LnrpcInvoice>(invoiceString);
|
||||
await _Invoices.Writer.WriteAsync(ConvertLndInvoice(parsedInvoice), _Cts.Token);
|
||||
}
|
||||
else if (line.StartsWith("{\"error\":", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var errorString = JObject.Parse(line)["error"].ToString();
|
||||
var error = _Parent.Deserialize<LndError>(errorString);
|
||||
throw new LndException(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new LndException("Unknown result from LND: " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_Invoices.Writer.TryComplete(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
public async Task<LightningInvoice> WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _Invoices.Reader.ReadAsync(cancellation);
|
||||
}
|
||||
catch (ChannelClosedException ex) when (ex.InnerException == null)
|
||||
{
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
catch (ChannelClosedException ex)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
void Dispose(bool waitLoop)
|
||||
{
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
_Cts.Cancel();
|
||||
_Reader?.Dispose();
|
||||
_Reader = null;
|
||||
_Body?.Dispose();
|
||||
_Body = null;
|
||||
_Response?.Dispose();
|
||||
_Response = null;
|
||||
_Client?.Dispose();
|
||||
_Client = null;
|
||||
if (waitLoop)
|
||||
_ListenLoop?.Wait();
|
||||
_Invoices.Writer.TryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 async Task<ILightningListenInvoiceSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var session = new LndInvoiceClientSession(this._rpcClient);
|
||||
await session.StartListening();
|
||||
return 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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user