Compare commits
237 Commits
v1.0.3.165
...
torproxy
Author | SHA1 | Date | |
---|---|---|---|
effbf169d3 | |||
74b6aa7353 | |||
0e0297d650 | |||
8dd6ecc0b8 | |||
e84d4d3cb5 | |||
da54154ae7 | |||
a9dbbe1955 | |||
3fbe86c286 | |||
0022e71dd4 | |||
c74121fefc | |||
cf00784096 | |||
db209af787 | |||
30bd94ee43 | |||
f313a5f221 | |||
b9ef5af5d7 | |||
56283df05a | |||
ef271a088a | |||
6043b7c75d | |||
c338779f0e | |||
67758609a7 | |||
f2bb24f6ab | |||
44ee7c66ce | |||
117622200b | |||
340ba5c714 | |||
9e16b506e5 | |||
e865d85c2c | |||
284e66c730 | |||
587f244f1d | |||
379e5741e7 | |||
ec4c346e0a | |||
5099f98d2a | |||
ebc99adc58 | |||
048dff7e9e | |||
a70934938a | |||
d9a93f996f | |||
3456d87bb5 | |||
4947fa4d45 | |||
37f4b34b5e | |||
66206399e1 | |||
1e3f62718d | |||
114ab98059 | |||
47baa219fd | |||
96509717cb | |||
89bbcb6092 | |||
95dbb65612 | |||
7db8d52624 | |||
77073fa78c | |||
9e315f99d0 | |||
16ad3ee7fe | |||
559015c70d | |||
15d02dab4f | |||
332a1da167 | |||
e12aa9e657 | |||
9503e07dc8 | |||
9b44370832 | |||
568015c58f | |||
58f56eac90 | |||
03532e4063 | |||
de6081c2dc | |||
197b46880c | |||
b7ec22ee89 | |||
fad53a9fa2 | |||
5b4fec11ed | |||
0ef4602a65 | |||
c8f182f13f | |||
36630d9586 | |||
32f2acee53 | |||
3be2ef5c91 | |||
7e5d269c39 | |||
6e0c090622 | |||
cccf3ca617 | |||
9e9b5945fe | |||
77f6019d82 | |||
35432d919c | |||
25e6f82aa3 | |||
d22993871f | |||
80563de587 | |||
b5102c4269 | |||
9d215ea27d | |||
cdf6886c39 | |||
79b034b505 | |||
bfba105aec | |||
1bbdaa1251 | |||
ca462bec84 | |||
eb5dcab32d | |||
ca3acdacdc | |||
33f63508e8 | |||
5033cb3186 | |||
3d1122be7c | |||
1d092a15fb | |||
501a21b89e | |||
b9006e4417 | |||
5b3b96b372 | |||
338a0f9251 | |||
29ba761d7c | |||
b96e668dfd | |||
de3753d04e | |||
2a030fe7fb | |||
f595d823b6 | |||
78d191f7d8 | |||
3b6dbe76c5 | |||
e3b348b55c | |||
cf012a7946 | |||
8d1ff01ee8 | |||
e9bda50054 | |||
8ca2824b00 | |||
f7d70daff3 | |||
42d27d69dc | |||
5a9d1e3257 | |||
9cc9a16bb9 | |||
a3efe9a9dc | |||
94b0b9498d | |||
bb322d6bf3 | |||
b0f820e95a | |||
da588380ed | |||
8fa65408ed | |||
2d68d0da63 | |||
2bc7fa0316 | |||
55f416c48c | |||
aff91f49ef | |||
ba02372d13 | |||
e5a3ef3e22 | |||
137c3ef2ce | |||
87352f0b62 | |||
2226884946 | |||
9d2cd46464 | |||
f3b2b350ce | |||
96c04481da | |||
dad2642fa7 | |||
59bdb943dd | |||
67da6ee379 | |||
9c9c102e74 | |||
26241be6fa | |||
2bb4dd5d01 | |||
5312bb1dee | |||
bf6f5aa335 | |||
4a58763f98 | |||
b8202da7aa | |||
edfc82ac75 | |||
b28fc85974 | |||
ab1b36bcdc | |||
5443ac4688 | |||
d92d8ba0e4 | |||
0f19d303eb | |||
1332f597e5 | |||
de004074b7 | |||
29741f39ac | |||
33ea8984fc | |||
85517b0344 | |||
74c574255e | |||
70d4e98dff | |||
f1900d30f2 | |||
463567cb07 | |||
53b0e675c3 | |||
d323bb35cc | |||
3e13e478ad | |||
5f421b0679 | |||
c99fe54db1 | |||
05a2985c5b | |||
47408498b9 | |||
e75b4ec6bf | |||
ff99ab1239 | |||
2841cd8498 | |||
519f4af867 | |||
68cc3aba21 | |||
3a2970a495 | |||
b31fb1a269 | |||
3801eeec43 | |||
94cdd399d5 | |||
c784144a07 | |||
b600e5777e | |||
e49074d797 | |||
02d26467f9 | |||
e68b45c76a | |||
f410f7d4d1 | |||
d4dbe6fe17 | |||
c7305ba5e1 | |||
e11963aca0 | |||
2d77426e04 | |||
7e0f9e1d28 | |||
18e181bb9f | |||
c3c9585a95 | |||
4b5b941761 | |||
79c70b31a3 | |||
072139f707 | |||
9d80db98c5 | |||
a5df029d43 | |||
47f16aadd5 | |||
f8b2b18c6e | |||
4be6c06af5 | |||
92c58eea7f | |||
5e6049bf3f | |||
7adaa146dc | |||
e3b51f593e | |||
e64094dfcc | |||
cb6fcadb86 | |||
297b84a18b | |||
b7c0e049b5 | |||
aeef160d0b | |||
34c1a304a9 | |||
4f1ae4733c | |||
e009c1a25a | |||
79f12a7058 | |||
deb197cfa5 | |||
2710130667 | |||
48163961ed | |||
8658cb5f29 | |||
a6a56e4791 | |||
22e39998e2 | |||
70d1056d48 | |||
3bd5c3e1b5 | |||
0a1a4fd3b5 | |||
e508b22d34 | |||
a7815f107e | |||
ded5670108 | |||
c1ffeb331b | |||
ad1148d3e2 | |||
a7b926d907 | |||
1f7a821c09 | |||
bf45edb5d8 | |||
686f5bf151 | |||
426fe793e6 | |||
aee55103a3 | |||
778bf97079 | |||
2dcb3111f8 | |||
03d1f98402 | |||
34755b32dc | |||
6679ee1ca2 | |||
8420c74b31 | |||
0077105a2d | |||
51db617584 | |||
514b695907 | |||
b470ce2dad | |||
161850150a | |||
b02cfa9d41 | |||
dfe655393d | |||
8c81dae167 |
@ -119,22 +119,23 @@ workflows:
|
||||
# ignore any commit on any branch by default
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# only act on version tags v1.0.0.88
|
||||
# only act on version tags v1.0.0.88 or v1.0.2-1
|
||||
# OR feature tags like vlndseedbackup
|
||||
tags:
|
||||
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
|
||||
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
|
||||
- arm32v7:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
|
||||
- arm64v8:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
|
||||
- multiarch:
|
||||
requires:
|
||||
- amd64
|
||||
@ -144,4 +145,4 @@ workflows:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /(v[1-9]+(\.[0-9]+)*)|(v[a-z]+)/
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[0-9]+)?)|(v[a-z]+)/
|
||||
|
@ -5,7 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="5.0.29" />
|
||||
<PackageReference Include="NBitcoin" Version="5.0.40" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Client
|
||||
public virtual async Task RevokeCurrentAPIKeyInfo(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/api-keys/current", null, HttpMethod.Delete), token);
|
||||
HandleResponse(response);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task RevokeAPIKey(string apikey, CancellationToken token = default)
|
||||
@ -33,7 +33,7 @@ namespace BTCPayServer.Client
|
||||
if (apikey == null)
|
||||
throw new ArgumentNullException(nameof(apikey));
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/api-keys/{apikey}", null, HttpMethod.Delete), token);
|
||||
HandleResponse(response);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
BTCPayServer.Client/BTCPayServerClient.Health.cs
Normal file
16
BTCPayServer.Client/BTCPayServerClient.Health.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<ApiHealthData> GetHealth(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/health"), token);
|
||||
return await HandleResponse<ApiHealthData>(response);
|
||||
}
|
||||
}
|
||||
}
|
91
BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs
Normal file
91
BTCPayServer.Client/BTCPayServerClient.Lightning.Internal.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/info",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningNodeInformationData>(response);
|
||||
}
|
||||
|
||||
public async Task ConnectToLightningNode(string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/connect", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
|
||||
}
|
||||
|
||||
public async Task<string> OpenLightningChannel(string cryptoCode, OpenLightningChannelRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/channels", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
public async Task<string> GetLightningDepositAddress(string cryptoCode, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/address", method: HttpMethod.Post), token);
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
|
||||
public async Task PayLightningInvoice(string cryptoCode, PayLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> GetLightningInvoice(string cryptoCode,
|
||||
string invoiceId, CancellationToken token = default)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices/{invoiceId}",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> CreateLightningInvoice(string cryptoCode, CreateLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/server/lightning/{cryptoCode}/invoices", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
}
|
||||
}
|
93
BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs
Normal file
93
BTCPayServer.Client/BTCPayServerClient.Lightning.Store.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public async Task<LightningNodeInformationData> GetLightningNodeInfo(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/info",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningNodeInformationData>(response);
|
||||
}
|
||||
|
||||
public async Task ConnectToLightningNode(string storeId, string cryptoCode, ConnectToNodeRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/connect", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LightningChannelData>> GetLightningNodeChannels(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<IEnumerable<LightningChannelData>>(response);
|
||||
}
|
||||
|
||||
public async Task<string> OpenLightningChannel(string storeId, string cryptoCode, OpenLightningChannelRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/channels", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
public async Task<string> GetLightningDepositAddress(string storeId, string cryptoCode,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/address", method: HttpMethod.Post),
|
||||
token);
|
||||
return await HandleResponse<string>(response);
|
||||
}
|
||||
|
||||
public async Task PayLightningInvoice(string storeId, string cryptoCode, PayLightningInvoiceRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> GetLightningInvoice(string storeId, string cryptoCode,
|
||||
string invoiceId, CancellationToken token = default)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{invoiceId}",
|
||||
method: HttpMethod.Get), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
|
||||
public async Task<LightningInvoiceData> CreateLightningInvoice(string storeId, string cryptoCode,
|
||||
CreateLightningInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<LightningInvoiceData>(response);
|
||||
}
|
||||
}
|
||||
}
|
59
BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs
Normal file
59
BTCPayServer.Client/BTCPayServerClient.PaymentRequests.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token);
|
||||
return await HandleResponse<IEnumerable<PaymentRequestData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> GetPaymentRequest(string storeId, string paymentRequestId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}"), token);
|
||||
return await HandleResponse<PaymentRequestData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task ArchivePaymentRequest(string storeId, string paymentRequestId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<PaymentRequestData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<PaymentRequestData> UpdatePaymentRequest(string storeId, string paymentRequestId,
|
||||
UpdatePaymentRequestRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests/{paymentRequestId}", bodyPayload: request,
|
||||
method: HttpMethod.Put), token);
|
||||
return await HandleResponse<PaymentRequestData>(response);
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer.Client/BTCPayServerClient.ServerInfo.cs
Normal file
16
BTCPayServer.Client/BTCPayServerClient.ServerInfo.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<ServerInfoData> GetServerInfo(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/server/info"), token);
|
||||
return await HandleResponse<ServerInfoData>(response);
|
||||
}
|
||||
}
|
||||
}
|
51
BTCPayServer.Client/BTCPayServerClient.Stores.cs
Normal file
51
BTCPayServer.Client/BTCPayServerClient.Stores.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<StoreData>> GetStores(CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/stores"), token);
|
||||
return await HandleResponse<IEnumerable<StoreData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreData> GetStore(string storeId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}"), token);
|
||||
return await HandleResponse<StoreData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task RemoveStore(string storeId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}", method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreData> CreateStore(CreateStoreRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest("api/v1/stores", bodyPayload: request, method: HttpMethod.Post), token);
|
||||
return await HandleResponse<StoreData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<StoreData> UpdateStore(string storeId, UpdateStoreRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (storeId == null)
|
||||
throw new ArgumentNullException(nameof(storeId));
|
||||
var response = await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}", bodyPayload: request, method: HttpMethod.Put), token);
|
||||
return await HandleResponse<StoreData>(response);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -43,14 +43,25 @@ namespace BTCPayServer.Client
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
}
|
||||
|
||||
protected void HandleResponse(HttpResponseMessage message)
|
||||
protected async Task HandleResponse(HttpResponseMessage message)
|
||||
{
|
||||
if (message.StatusCode == System.Net.HttpStatusCode.UnprocessableEntity)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldValidationError[]>(await message.Content.ReadAsStringAsync()); ;
|
||||
throw new GreenFieldValidationException(err);
|
||||
}
|
||||
else if (message.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
var err = JsonConvert.DeserializeObject<Models.GreenfieldAPIError>(await message.Content.ReadAsStringAsync());
|
||||
throw new GreenFieldAPIException(err);
|
||||
}
|
||||
|
||||
message.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
protected async Task<T> HandleResponse<T>(HttpResponseMessage message)
|
||||
{
|
||||
HandleResponse(message);
|
||||
await HandleResponse(message);
|
||||
return JsonConvert.DeserializeObject<T>(await message.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
@ -96,7 +107,7 @@ namespace BTCPayServer.Client
|
||||
foreach (KeyValuePair<string, object> keyValuePair in payload)
|
||||
{
|
||||
UriBuilder uriBuilder = uri;
|
||||
if (keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
|
||||
if (!(keyValuePair.Value is string) && keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
|
||||
{
|
||||
foreach (var item in (IEnumerable)keyValuePair.Value)
|
||||
{
|
||||
|
18
BTCPayServer.Client/GreenFieldAPIException.cs
Normal file
18
BTCPayServer.Client/GreenFieldAPIException.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public class GreenFieldAPIException : Exception
|
||||
{
|
||||
public GreenFieldAPIException(Models.GreenfieldAPIError error):base(error.Message)
|
||||
{
|
||||
if (error == null)
|
||||
throw new ArgumentNullException(nameof(error));
|
||||
APIError = error;
|
||||
}
|
||||
public Models.GreenfieldAPIError APIError { get; }
|
||||
}
|
||||
}
|
30
BTCPayServer.Client/GreenFieldValidationException.cs
Normal file
30
BTCPayServer.Client/GreenFieldValidationException.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Client
|
||||
{
|
||||
public class GreenFieldValidationException : Exception
|
||||
{
|
||||
public GreenFieldValidationException(Models.GreenfieldValidationError[] errors) : base(BuildMessage(errors))
|
||||
{
|
||||
ValidationErrors = errors;
|
||||
}
|
||||
|
||||
private static string BuildMessage(GreenfieldValidationError[] errors)
|
||||
{
|
||||
if (errors == null)
|
||||
throw new ArgumentNullException(nameof(errors));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var error in errors)
|
||||
{
|
||||
builder.AppendLine($"{error.Path}: {error.Message}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public Models.GreenfieldValidationError[] ValidationErrors { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class DecimalStringJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return (objectType == typeof(decimal) || objectType == typeof(decimal?));
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
JToken token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Float:
|
||||
case JTokenType.Integer:
|
||||
case JTokenType.String:
|
||||
return decimal.Parse(token.ToString(), CultureInfo.InvariantCulture);
|
||||
case JTokenType.Null when objectType == typeof(decimal?):
|
||||
return null;
|
||||
default:
|
||||
throw new JsonSerializationException("Unexpected token type: " +
|
||||
token.Type);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(((decimal)value).ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class LightMoneyJsonConverter : BTCPayServer.Lightning.JsonConverters.LightMoneyJsonConverter
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(((LightMoney)value).MilliSatoshi.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
25
BTCPayServer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class MoneyJsonConverter : NBitcoin.JsonConverters.MoneyJsonConverter
|
||||
{
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
return new Money( long.Parse((string) reader.Value));
|
||||
}
|
||||
return base.ReadJson(reader, objectType, existingValue, serializer);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(((Money)value).Satoshi.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
29
BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs
Normal file
29
BTCPayServer.Client/JsonConverters/NodeUriJsonConverter.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class NodeUriJsonConverter : JsonConverter<NodeInfo>
|
||||
{
|
||||
public override NodeInfo ReadJson(JsonReader reader, Type objectType, [AllowNull] NodeInfo existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
throw new JsonObjectException(reader.Path, "Unexpected token type for NodeUri");
|
||||
if (NodeInfo.TryParse((string)reader.Value, out var info))
|
||||
return info;
|
||||
throw new JsonObjectException(reader.Path, "Invalid NodeUri");
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, [AllowNull] NodeInfo value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is NodeInfo)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
45
BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs
Normal file
45
BTCPayServer.Client/JsonConverters/TimeSpanJsonConverter.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TimeSpanJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
var nullable = objectType == typeof(TimeSpan?);
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
{
|
||||
if (nullable)
|
||||
return null;
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
if (reader.TokenType != JsonToken.Integer)
|
||||
throw new JsonObjectException("Invalid timespan, expected integer", reader);
|
||||
return TimeSpan.FromSeconds((long)reader.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new JsonObjectException("Invalid locktime", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is TimeSpan s)
|
||||
{
|
||||
writer.WriteValue((int)s.TotalSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
BTCPayServer.Client/Models/ApiHealthData.cs
Normal file
7
BTCPayServer.Client/Models/ApiHealthData.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ApiHealthData
|
||||
{
|
||||
public bool Synchronized { get; set; }
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
|
||||
public Permission[] Permissions { get; set; }
|
||||
}
|
||||
|
@ -6,33 +6,20 @@ namespace BTCPayServer.Client.Models
|
||||
/// the id of the user
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the email AND username of the user
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has verified their email
|
||||
/// </summary>
|
||||
public bool EmailConfirmed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// whether the user needed to verify their email on account creation
|
||||
/// </summary>
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
}
|
||||
|
||||
public class CreateApplicationUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// the email AND username of the new user
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
/// <summary>
|
||||
/// password of the new user
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
/// <summary>
|
||||
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
|
||||
/// </summary>
|
||||
public bool? IsAdministrator { get; set; }
|
||||
}
|
||||
}
|
||||
|
24
BTCPayServer.Client/Models/ConnectToNodeRequest.cs
Normal file
24
BTCPayServer.Client/Models/ConnectToNodeRequest.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ConnectToNodeRequest
|
||||
{
|
||||
public ConnectToNodeRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public ConnectToNodeRequest(NodeInfo nodeInfo)
|
||||
{
|
||||
NodeURI = nodeInfo;
|
||||
}
|
||||
[JsonConverter(typeof(NodeUriJsonConverter))]
|
||||
[JsonProperty("nodeURI")]
|
||||
public NodeInfo NodeURI { get; set; }
|
||||
}
|
||||
}
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
@ -9,6 +6,7 @@ namespace BTCPayServer.Client.Models
|
||||
public class CreateApiKeyRequest
|
||||
{
|
||||
public string Label { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(PermissionJsonConverter))]
|
||||
public Permission[] Permissions { get; set; }
|
||||
}
|
||||
|
20
BTCPayServer.Client/Models/CreateApplicationUserRequest.cs
Normal file
20
BTCPayServer.Client/Models/CreateApplicationUserRequest.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateApplicationUserRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// the email AND username of the new user
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// password of the new user
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this user is an administrator. If left null and there are no admins in the system, the user will be created as an admin.
|
||||
/// </summary>
|
||||
public bool? IsAdministrator { get; set; }
|
||||
}
|
||||
}
|
29
BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs
Normal file
29
BTCPayServer.Client/Models/CreateLightningInvoiceRequest.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateLightningInvoiceRequest
|
||||
{
|
||||
public CreateLightningInvoiceRequest()
|
||||
{
|
||||
|
||||
}
|
||||
public CreateLightningInvoiceRequest(LightMoney amount, string description, TimeSpan expiry)
|
||||
{
|
||||
Amount = amount;
|
||||
Description = description;
|
||||
Expiry = expiry;
|
||||
}
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreatePaymentRequestRequest : PaymentRequestBaseData
|
||||
{
|
||||
}
|
||||
}
|
6
BTCPayServer.Client/Models/CreateStoreRequest.cs
Normal file
6
BTCPayServer.Client/Models/CreateStoreRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateStoreRequest: StoreBaseData
|
||||
{
|
||||
}
|
||||
}
|
25
BTCPayServer.Client/Models/GreenfieldAPIError.cs
Normal file
25
BTCPayServer.Client/Models/GreenfieldAPIError.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class GreenfieldAPIError
|
||||
{
|
||||
public GreenfieldAPIError()
|
||||
{
|
||||
|
||||
}
|
||||
public GreenfieldAPIError(string code, string message)
|
||||
{
|
||||
if (code == null)
|
||||
throw new ArgumentNullException(nameof(code));
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
Code = code;
|
||||
Message = message;
|
||||
}
|
||||
public string Code { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
26
BTCPayServer.Client/Models/GreenfieldValidationError.cs
Normal file
26
BTCPayServer.Client/Models/GreenfieldValidationError.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class GreenfieldValidationError
|
||||
{
|
||||
public GreenfieldValidationError()
|
||||
{
|
||||
|
||||
}
|
||||
public GreenfieldValidationError(string path, string message)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
Path = path;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
30
BTCPayServer.Client/Models/LightningInvoiceData.cs
Normal file
30
BTCPayServer.Client/Models/LightningInvoiceData.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LightningInvoiceData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public LightningInvoiceStatus Status { get; set; }
|
||||
|
||||
[JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? PaidAt { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney AmountReceived { get; set; }
|
||||
}
|
||||
}
|
33
BTCPayServer.Client/Models/LightningNodeInformationData.cs
Normal file
33
BTCPayServer.Client/Models/LightningNodeInformationData.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class LightningNodeInformationData
|
||||
{
|
||||
[JsonProperty("nodeURIs", ItemConverterType = typeof(NodeUriJsonConverter))]
|
||||
public NodeInfo[] NodeURIs { get; set; }
|
||||
public int BlockHeight { get; set; }
|
||||
}
|
||||
|
||||
public class LightningChannelData
|
||||
{
|
||||
public string RemoteNode { get; set; }
|
||||
|
||||
public bool IsPublic { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Capacity { get; set; }
|
||||
|
||||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney LocalBalance { get; set; }
|
||||
|
||||
public string ChannelPoint { get; set; }
|
||||
}
|
||||
}
|
21
BTCPayServer.Client/Models/OpenLightningChannelRequest.cs
Normal file
21
BTCPayServer.Client/Models/OpenLightningChannelRequest.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using MoneyJsonConverter = BTCPayServer.Client.JsonConverters.MoneyJsonConverter;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class OpenLightningChannelRequest
|
||||
{
|
||||
[JsonConverter(typeof(NodeUriJsonConverter))]
|
||||
[JsonProperty("nodeURI")]
|
||||
public NodeInfo NodeURI { get; set; }
|
||||
[JsonConverter(typeof(MoneyJsonConverter))]
|
||||
public Money ChannelAmount { get; set; }
|
||||
|
||||
[JsonConverter(typeof(FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
}
|
||||
}
|
8
BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs
Normal file
8
BTCPayServer.Client/Models/PayLightningInvoiceRequest.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PayLightningInvoiceRequest
|
||||
{
|
||||
[Newtonsoft.Json.JsonProperty("BOLT11")]
|
||||
public string BOLT11 { get; set; }
|
||||
}
|
||||
}
|
26
BTCPayServer.Client/Models/PaymentRequestBaseData.cs
Normal file
26
BTCPayServer.Client/Models/PaymentRequestBaseData.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PaymentRequestBaseData
|
||||
{
|
||||
[JsonProperty(ItemConverterType = typeof(DecimalStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
}
|
22
BTCPayServer.Client/Models/PaymentRequestData.cs
Normal file
22
BTCPayServer.Client/Models/PaymentRequestData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class PaymentRequestData : PaymentRequestBaseData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public string Id { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Expired = 2
|
||||
}
|
||||
}
|
||||
}
|
47
BTCPayServer.Client/Models/ServerInfoData.cs
Normal file
47
BTCPayServer.Client/Models/ServerInfoData.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class ServerInfoData
|
||||
{
|
||||
/// <summary>
|
||||
/// the BTCPay Server version
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the Tor hostname
|
||||
/// </summary>
|
||||
public string Onion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the payment methods this server supports
|
||||
/// </summary>
|
||||
public IEnumerable<string> SupportedPaymentMethods { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// are all chains fully synched
|
||||
/// </summary>
|
||||
public bool FullySynched { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// detailed sync information per chain
|
||||
/// </summary>
|
||||
public IEnumerable<ServerInfoSyncStatusData> SyncStatus { get; set; }
|
||||
}
|
||||
|
||||
public class ServerInfoSyncStatusData
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public int ChainHeight { get; set; }
|
||||
public int? SyncHeight { get; set; }
|
||||
public ServerInfoNodeData NodeInformation { get; set; }
|
||||
}
|
||||
|
||||
public class ServerInfoNodeData
|
||||
{
|
||||
public int Headers { get; set; }
|
||||
public int Blocks { get; set; }
|
||||
public double VerificationProgress { get; set; }
|
||||
}
|
||||
}
|
79
BTCPayServer.Client/Models/StoreBaseData.cs
Normal file
79
BTCPayServer.Client/Models/StoreBaseData.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public abstract class StoreBaseData
|
||||
{
|
||||
/// <summary>
|
||||
/// the name of the store
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Website { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public SpeedPolicy SpeedPolicy { get; set; }
|
||||
public string LightningDescriptionTemplate { get; set; }
|
||||
public double PaymentTolerance { get; set; } = 0;
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool ShowRecommendedFee { get; set; } = true;
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int RecommendedFeeBlockTarget { get; set; } = 1;
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string DefaultLang { get; set; } = "en";
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
|
||||
public string CustomLogo { get; set; }
|
||||
|
||||
public string CustomCSS { get; set; }
|
||||
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
|
||||
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; }
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
|
||||
public enum SpeedPolicy
|
||||
{
|
||||
HighSpeed = 0,
|
||||
MediumSpeed = 1,
|
||||
LowSpeed = 2,
|
||||
LowMediumSpeed = 3
|
||||
}
|
||||
}
|
10
BTCPayServer.Client/Models/StoreData.cs
Normal file
10
BTCPayServer.Client/Models/StoreData.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class StoreData : StoreBaseData
|
||||
{
|
||||
/// <summary>
|
||||
/// the id of the store
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class UpdatePaymentRequestRequest : PaymentRequestBaseData
|
||||
{
|
||||
}
|
||||
}
|
6
BTCPayServer.Client/Models/UpdateStoreRequest.cs
Normal file
6
BTCPayServer.Client/Models/UpdateStoreRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class UpdateStoreRequest : StoreBaseData
|
||||
{
|
||||
}
|
||||
}
|
@ -6,10 +6,16 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
public class Policies
|
||||
{
|
||||
public const string CanCreateLightningInvoiceInternalNode = "btcpay.server.cancreatelightninginvoiceinternalnode";
|
||||
public const string CanCreateLightningInvoiceInStore = "btcpay.store.cancreatelightninginvoice";
|
||||
public const string CanUseInternalLightningNode = "btcpay.server.canuseinternallightningnode";
|
||||
public const string CanUseLightningNodeInStore = "btcpay.store.canuselightningnode";
|
||||
public const string CanModifyServerSettings = "btcpay.server.canmodifyserversettings";
|
||||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
|
||||
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
|
||||
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
|
||||
public const string CanModifyProfile = "btcpay.user.canmodifyprofile";
|
||||
public const string CanViewProfile = "btcpay.user.canviewprofile";
|
||||
public const string CanCreateUser = "btcpay.server.cancreateuser";
|
||||
@ -22,10 +28,16 @@ namespace BTCPayServer.Client
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
yield return CanViewStoreSettings;
|
||||
yield return CanViewPaymentRequests;
|
||||
yield return CanModifyPaymentRequests;
|
||||
yield return CanModifyProfile;
|
||||
yield return CanViewProfile;
|
||||
yield return CanCreateUser;
|
||||
yield return Unrestricted;
|
||||
yield return CanUseInternalLightningNode;
|
||||
yield return CanCreateLightningInvoiceInternalNode;
|
||||
yield return CanUseLightningNodeInStore;
|
||||
yield return CanCreateLightningInvoiceInStore;
|
||||
}
|
||||
}
|
||||
public static bool IsValidPolicy(string policy)
|
||||
@ -45,14 +57,14 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
public class Permission
|
||||
{
|
||||
public static Permission Create(string policy, string storeId = null)
|
||||
public static Permission Create(string policy, string scope = null)
|
||||
{
|
||||
if (TryCreatePermission(policy, storeId, out var r))
|
||||
if (TryCreatePermission(policy, scope, out var r))
|
||||
return r;
|
||||
throw new ArgumentException("Invalid Permission");
|
||||
}
|
||||
|
||||
public static bool TryCreatePermission(string policy, string storeId, out Permission permission)
|
||||
public static bool TryCreatePermission(string policy, string scope, out Permission permission)
|
||||
{
|
||||
permission = null;
|
||||
if (policy == null)
|
||||
@ -60,9 +72,9 @@ namespace BTCPayServer.Client
|
||||
policy = policy.Trim().ToLowerInvariant();
|
||||
if (!Policies.IsValidPolicy(policy))
|
||||
return false;
|
||||
if (storeId != null && !Policies.IsStorePolicy(policy))
|
||||
if (scope != null && !Policies.IsStorePolicy(policy))
|
||||
return false;
|
||||
permission = new Permission(policy, storeId);
|
||||
permission = new Permission(policy, scope);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -96,12 +108,10 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal Permission(string policy, string storeId)
|
||||
internal Permission(string policy, string scope)
|
||||
{
|
||||
Policy = policy;
|
||||
StoreId = storeId;
|
||||
Scope = scope;
|
||||
}
|
||||
|
||||
public bool Contains(Permission subpermission)
|
||||
@ -115,7 +125,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
if (!Policies.IsStorePolicy(subpermission.Policy))
|
||||
return true;
|
||||
return StoreId == null || subpermission.StoreId == this.StoreId;
|
||||
return Scope == null || subpermission.Scope == this.Scope;
|
||||
}
|
||||
|
||||
public static IEnumerable<Permission> ToPermissions(string[] permissions)
|
||||
@ -135,23 +145,30 @@ namespace BTCPayServer.Client
|
||||
return true;
|
||||
if (this.Policy == subpolicy)
|
||||
return true;
|
||||
if (subpolicy == Policies.CanViewStoreSettings && this.Policy == Policies.CanModifyStoreSettings)
|
||||
return true;
|
||||
if (subpolicy == Policies.CanCreateInvoice && this.Policy == Policies.CanModifyStoreSettings)
|
||||
return true;
|
||||
if (subpolicy == Policies.CanViewProfile && this.Policy == Policies.CanModifyProfile)
|
||||
return true;
|
||||
return false;
|
||||
switch (subpolicy)
|
||||
{
|
||||
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
|
||||
case Policies.CanModifyPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewPaymentRequests when this.Policy == Policies.CanViewStoreSettings:
|
||||
case Policies.CanCreateLightningInvoiceInternalNode when this.Policy == Policies.CanUseInternalLightningNode:
|
||||
case Policies.CanCreateLightningInvoiceInStore when this.Policy == Policies.CanUseLightningNodeInStore:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string StoreId { get; }
|
||||
public string Scope { get; }
|
||||
public string Policy { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (StoreId != null)
|
||||
if (Scope != null)
|
||||
{
|
||||
return $"{Policy}:{StoreId}";
|
||||
return $"{Policy}:{Scope}";
|
||||
}
|
||||
return Policy;
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/liquid-tether.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true
|
||||
SupportRBF = true,
|
||||
SupportLightning = false
|
||||
});
|
||||
|
||||
Add(new ElementsBTCPayNetwork()
|
||||
@ -49,7 +50,8 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/etb.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true
|
||||
SupportRBF = true,
|
||||
SupportLightning = false
|
||||
});
|
||||
|
||||
Add(new ElementsBTCPayNetwork()
|
||||
@ -71,7 +73,8 @@ namespace BTCPayServer
|
||||
CryptoImagePath = "imlegacy/lcad.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
SupportRBF = true
|
||||
SupportRBF = true,
|
||||
SupportLightning = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,31 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
public override GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
|
||||
{
|
||||
TransactionInformationSet Filter(TransactionInformationSet transactionInformationSet)
|
||||
{
|
||||
return new TransactionInformationSet()
|
||||
{
|
||||
Transactions =
|
||||
transactionInformationSet.Transactions.FindAll(information =>
|
||||
information.Outputs.Any(output =>
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId) ||
|
||||
information.Inputs.Any(output =>
|
||||
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId))
|
||||
};
|
||||
}
|
||||
|
||||
return new GetTransactionsResponse()
|
||||
{
|
||||
Height = response.Height,
|
||||
ConfirmedTransactions = Filter(response.ConfirmedTransactions),
|
||||
ReplacedTransactions = Filter(response.ReplacedTransactions),
|
||||
UnconfirmedTransactions = Filter(response.UnconfirmedTransactions)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
|
||||
{
|
||||
//precision 0: 10 = 0.00000010
|
||||
|
@ -62,6 +62,7 @@ namespace BTCPayServer
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
public string UriScheme { get; internal set; }
|
||||
public bool SupportPayJoin { get; set; } = false;
|
||||
public bool SupportLightning { get; set; } = true;
|
||||
|
||||
public KeyPath GetRootKeyPath(DerivationType type)
|
||||
{
|
||||
@ -119,6 +120,11 @@ namespace BTCPayServer
|
||||
{
|
||||
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
|
||||
}
|
||||
|
||||
public virtual GetTransactionsResponse FilterValidTransactions(GetTransactionsResponse response)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BTCPayNetworkBase
|
||||
|
@ -4,6 +4,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.9" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="3.0.15" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -3,10 +3,16 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.1.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.4" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Client\BTCPayServer.Client.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -79,6 +79,8 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool Archived { get; set; }
|
||||
public List<PendingInvoiceData> PendingInvoices { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -12,34 +13,13 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
public string StoreDataId { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
public StoreData StoreData { get; set; }
|
||||
|
||||
public PaymentRequestStatus Status { get; set; }
|
||||
public Client.Models.PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
||||
|
||||
public byte[] Blob { get; set; }
|
||||
|
||||
public class PaymentRequestBlob
|
||||
{
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public DateTime? ExpiryDate { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Email { get; set; }
|
||||
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public bool AllowCustomPaymentAmounts { get; set; }
|
||||
}
|
||||
|
||||
public enum PaymentRequestStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Expired = 2
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,98 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public enum SpeedPolicy
|
||||
{
|
||||
HighSpeed = 0,
|
||||
MediumSpeed = 1,
|
||||
LowSpeed = 2,
|
||||
LowMediumSpeed = 3
|
||||
}
|
||||
|
||||
public class StoreData
|
||||
{
|
||||
public string Id
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string Id { get; set; }
|
||||
public List<UserStore> UserStores { get; set; }
|
||||
|
||||
public List<UserStore> UserStores
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<AppData> Apps
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public List<AppData> Apps { get; set; }
|
||||
|
||||
public List<PaymentRequestData> PaymentRequests { get; set; }
|
||||
|
||||
public List<InvoiceData> Invoices { get; set; }
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DerivationStrategy { get; set; }
|
||||
|
||||
[Obsolete("Use GetDerivationStrategies instead")]
|
||||
public string DerivationStrategies
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DerivationStrategies { get; set; }
|
||||
|
||||
public string StoreName
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreName { get; set; }
|
||||
|
||||
public SpeedPolicy SpeedPolicy
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public SpeedPolicy SpeedPolicy { get; set; } = SpeedPolicy.MediumSpeed;
|
||||
|
||||
public string StoreWebsite
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string StoreWebsite { get; set; }
|
||||
|
||||
public byte[] StoreCertificate
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public byte[] StoreCertificate { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public string Role
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[NotMapped] public string Role { get; set; }
|
||||
|
||||
public byte[] StoreBlob { get; set; }
|
||||
|
||||
public byte[] StoreBlob
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
[Obsolete("Use GetDefaultPaymentId instead")]
|
||||
public string DefaultCrypto { get; set; }
|
||||
|
||||
public List<PairedSINData> PairedSINs { get; set; }
|
||||
public IEnumerable<APIKeyData> APIKeys { get; set; }
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Emit;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -16,69 +17,9 @@ namespace BTCPayServer.Data
|
||||
public byte[] Blob { get; set; }
|
||||
}
|
||||
|
||||
public class Label
|
||||
{
|
||||
public Label(string value, string color)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
if (color == null)
|
||||
throw new ArgumentNullException(nameof(color));
|
||||
Value = value;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
public string Color { get; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
Label item = obj as Label;
|
||||
if (item == null)
|
||||
return false;
|
||||
return Value.Equals(item.Value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public static bool operator ==(Label a, Label b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.Value == b.Value;
|
||||
}
|
||||
|
||||
public static bool operator !=(Label a, Label b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class WalletBlobInfo
|
||||
{
|
||||
public Dictionary<string, string> LabelColors { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public IEnumerable<Label> GetLabels(WalletTransactionInfo transactionInfo)
|
||||
{
|
||||
foreach (var label in transactionInfo.Labels)
|
||||
{
|
||||
if (LabelColors.TryGetValue(label, out var color))
|
||||
{
|
||||
yield return new Label(label, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Label> GetLabels()
|
||||
{
|
||||
foreach (var kv in LabelColors)
|
||||
{
|
||||
yield return new Label(kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,8 +114,8 @@ namespace BTCPayServer.Migrations
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
ProviderKey = table.Column<string>(nullable: false),
|
||||
LoginProvider = table.Column<string>(nullable: false, maxLength: 255),
|
||||
ProviderKey = table.Column<string>(nullable: false, maxLength: 255),
|
||||
ProviderDisplayName = table.Column<string>(nullable: true),
|
||||
UserId = table.Column<string>(nullable: false, maxLength: maxLength)
|
||||
},
|
||||
@ -159,8 +159,8 @@ namespace BTCPayServer.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<string>(nullable: false, maxLength: maxLength),
|
||||
LoginProvider = table.Column<string>(nullable: false),
|
||||
Name = table.Column<string>(nullable: false),
|
||||
LoginProvider = table.Column<string>(nullable: false, maxLength: 64),
|
||||
Name = table.Column<string>(nullable: false, maxLength: 64),
|
||||
Value = table.Column<string>(nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Migrations
|
||||
Label = table.Column<string>(nullable: true),
|
||||
Name = table.Column<string>(nullable: true),
|
||||
PairingTime = table.Column<DateTimeOffset>(nullable: false),
|
||||
SIN = table.Column<string>(nullable: true),
|
||||
SIN = table.Column<string>(nullable: true, maxLength: maxLength),
|
||||
StoreDataId = table.Column<string>(nullable: true, maxLength: maxLength)
|
||||
},
|
||||
constraints: table =>
|
||||
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceDataId = table.Column<string>(nullable: false, maxLength: maxLength),
|
||||
Address = table.Column<string>(nullable: false),
|
||||
Address = table.Column<string>(nullable: false, maxLength: this.IsMySql(migrationBuilder.ActiveProvider) ? (int?)512 : null),
|
||||
Assigned = table.Column<DateTimeOffset>(nullable: false),
|
||||
UnAssigned = table.Column<DateTimeOffset>(nullable: true)
|
||||
},
|
||||
|
@ -0,0 +1,38 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200507092343_AddArchivedToInvoice")]
|
||||
public class AddArchivedToInvoice : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "Invoices",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "PaymentRequests",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "Invoices");
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "PaymentRequests");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -190,6 +190,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
@ -348,6 +351,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
|
@ -4,14 +4,16 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="NBitcoin" Version="5.0.40" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.6.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Common\BTCPayServer.Common.csproj" />
|
||||
<EmbeddedResource Include="Currencies.json" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
1276
BTCPayServer.Rating/Currencies.json
Normal file
1276
BTCPayServer.Rating/Currencies.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,35 +6,22 @@ using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Rating;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CurrencyData
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Code
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public int Divisibility
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Symbol
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public string Name { get; set; }
|
||||
public string Code { get; set; }
|
||||
public int Divisibility { get; set; }
|
||||
public string Symbol { get; set; }
|
||||
public bool Crypto { get; set; }
|
||||
}
|
||||
public class CurrencyNameTable
|
||||
{
|
||||
public static CurrencyNameTable Instance = new CurrencyNameTable();
|
||||
public CurrencyNameTable()
|
||||
{
|
||||
_Currencies = LoadCurrency().ToDictionary(k => k.Code);
|
||||
@ -94,16 +81,10 @@ namespace BTCPayServer.Services.Rates
|
||||
catch { }
|
||||
}
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
foreach (var curr in _Currencies.Where(pair => pair.Value.Crypto))
|
||||
{
|
||||
AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode);
|
||||
AddCurrency(_CurrencyProviders, curr.Key, curr.Value.Divisibility, curr.Value.Symbol?? curr.Value.Code);
|
||||
}
|
||||
|
||||
_CurrencyProviders.TryAdd("SATS",
|
||||
new NumberFormatInfo()
|
||||
{
|
||||
CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3
|
||||
});
|
||||
}
|
||||
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
|
||||
}
|
||||
@ -153,58 +134,15 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
static CurrencyData[] LoadCurrency()
|
||||
{
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Currencies.txt");
|
||||
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("BTCPayServer.Rating.Currencies.json");
|
||||
string content = null;
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
content = reader.ReadToEnd();
|
||||
}
|
||||
var currencies = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
Dictionary<string, CurrencyData> dico = new Dictionary<string, CurrencyData>();
|
||||
foreach (var currency in currencies)
|
||||
{
|
||||
var splitted = currency.Split(new[] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted.Length < 3)
|
||||
continue;
|
||||
CurrencyData info = new CurrencyData();
|
||||
info.Name = splitted[0];
|
||||
info.Code = splitted[1];
|
||||
int divisibility;
|
||||
if (!int.TryParse(splitted[2], out divisibility))
|
||||
continue;
|
||||
info.Divisibility = divisibility;
|
||||
if (!dico.ContainsKey(info.Code))
|
||||
dico.Add(info.Code, info);
|
||||
if (splitted.Length >= 4)
|
||||
{
|
||||
info.Symbol = splitted[3];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
|
||||
{
|
||||
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
|
||||
{
|
||||
Code = network.CryptoCode,
|
||||
Divisibility = network.Divisibility,
|
||||
Name = network.CryptoCode,
|
||||
Crypto = true
|
||||
}))
|
||||
{
|
||||
dico[network.CryptoCode].Crypto = true;
|
||||
}
|
||||
}
|
||||
|
||||
dico.TryAdd("SATS", new CurrencyData()
|
||||
{
|
||||
Code = "SATS",
|
||||
Crypto = true,
|
||||
Divisibility = 0,
|
||||
Name = "Satoshis",
|
||||
Symbol = "Sats",
|
||||
});
|
||||
|
||||
return dico.Values.ToArray();
|
||||
var currencies = JsonConvert.DeserializeObject<CurrencyData[]>(content);
|
||||
return currencies;
|
||||
}
|
||||
|
||||
public CurrencyData GetCurrencyData(string currency, bool useFallback)
|
@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public class CurrencyPair
|
||||
{
|
||||
static readonly BTCPayNetworkProvider _NetworkProvider = new BTCPayNetworkProvider(NBitcoin.NetworkType.Mainnet);
|
||||
public CurrencyPair(string left, string right)
|
||||
{
|
||||
if (right == null)
|
||||
@ -47,13 +47,14 @@ namespace BTCPayServer.Rating
|
||||
value = new CurrencyPair(currencyPair.Substring(0,3), currencyPair.Substring(3, 3));
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 3; i < 5; i++)
|
||||
{
|
||||
var potentialCryptoName = currencyPair.Substring(0, i);
|
||||
var network = _NetworkProvider.GetNetwork<BTCPayNetworkBase>(potentialCryptoName);
|
||||
if (network != null)
|
||||
var currency = CurrencyNameTable.Instance.GetCurrencyData(potentialCryptoName, false);
|
||||
if (currency != null)
|
||||
{
|
||||
value = new CurrencyPair(network.CryptoCode, currencyPair.Substring(i));
|
||||
value = new CurrencyPair(currency.Code, currencyPair.Substring(i));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
25
BTCPayServer.Rating/Extensions.cs
Normal file
25
BTCPayServer.Rating/Extensions.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Rating
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
|
||||
{
|
||||
if (value != 0m)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
}
|
||||
divisibility++;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Rating;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using BTCPayServer.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
using System.Globalization;
|
||||
|
@ -530,7 +530,10 @@ namespace BTCPayServer.Rating
|
||||
var rewriter = new ReplaceExchangeRateRewriter();
|
||||
rewriter.Rates = ExchangeRates;
|
||||
var result = rewriter.Visit(this.expression);
|
||||
Errors.AddRange(rewriter.Errors);
|
||||
foreach (var item in rewriter.Errors)
|
||||
{
|
||||
Errors.Add(item);
|
||||
}
|
||||
_Evaluated = result.NormalizeWhitespace("", "\n").ToString();
|
||||
if (HasError)
|
||||
return false;
|
||||
@ -539,7 +542,10 @@ namespace BTCPayServer.Rating
|
||||
calculate.Visit(result);
|
||||
if (calculate.Values.Count != 1 || calculate.Errors.Count != 0)
|
||||
{
|
||||
Errors.AddRange(calculate.Errors);
|
||||
foreach (var item in calculate.Errors)
|
||||
{
|
||||
Errors.Add(item);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
_BidAsk = calculate.Values.Pop();
|
||||
|
@ -14,6 +14,7 @@ using Newtonsoft.Json;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -191,7 +192,7 @@ namespace BTCPayServer.Tests
|
||||
var canModifyAllStores = Permission.Create(Policies.CanModifyStoreSettings, null);
|
||||
var canModifyServer = Permission.Create(Policies.CanModifyServerSettings, null);
|
||||
var unrestricted = Permission.Create(Policies.Unrestricted, null);
|
||||
var selectiveStorePermissions = permissions.Where(p => p.StoreId != null && p.Policy == Policies.CanModifyStoreSettings);
|
||||
var selectiveStorePermissions = permissions.Where(p => p.Scope != null && p.Policy == Policies.CanModifyStoreSettings);
|
||||
if (permissions.Contains(canModifyAllStores) || selectiveStorePermissions.Any())
|
||||
{
|
||||
var resultStores =
|
||||
@ -201,11 +202,11 @@ namespace BTCPayServer.Tests
|
||||
foreach (var selectiveStorePermission in selectiveStorePermissions)
|
||||
{
|
||||
Assert.True(await TestApiAgainstAccessToken<bool>(accessToken,
|
||||
$"{TestApiPath}/me/stores/{selectiveStorePermission.StoreId}/can-edit",
|
||||
$"{TestApiPath}/me/stores/{selectiveStorePermission.Scope}/can-edit",
|
||||
tester.PayTester.HttpClient));
|
||||
|
||||
Assert.Contains(resultStores,
|
||||
data => data.Id.Equals(selectiveStorePermission.StoreId, StringComparison.InvariantCultureIgnoreCase));
|
||||
data => data.Id.Equals(selectiveStorePermission.Scope, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
bool shouldBeAuthorized = false;
|
||||
|
@ -23,10 +23,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="80.0.3987.10600" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="83.0.4103.3900" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -147,6 +147,10 @@ namespace BTCPayServer.Tests
|
||||
config.AppendLine($"socksendpoint={SocksEndpoint}");
|
||||
config.AppendLine($"debuglog=debug.log");
|
||||
|
||||
if (SocksHTTPProxy is string v)
|
||||
{
|
||||
config.AppendLine($"sockshttpproxy={v}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
|
||||
config.AppendLine($"sshpassword={SSHPassword}");
|
||||
@ -291,6 +295,8 @@ namespace BTCPayServer.Tests
|
||||
public string SSHPassword { get; internal set; }
|
||||
public string SSHKeyFile { get; internal set; }
|
||||
public string SSHConnection { get; set; }
|
||||
public string SocksHTTPProxy { get; set; }
|
||||
|
||||
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
|
@ -37,14 +37,13 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
tester.ActivateLBTC();
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("LBTC");
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("USDT");
|
||||
|
||||
Assert.Single(Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets);
|
||||
Assert.Equal(3, Assert.IsType<ListWalletsViewModel>(Assert.IsType<ViewResult>(await user.GetController<WalletsController>().ListWallets()).Model).Wallets.Count);
|
||||
}
|
||||
}
|
||||
|
||||
|
74
BTCPayServer.Tests/FakeServer.cs
Normal file
74
BTCPayServer.Tests/FakeServer.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System.Threading.Channels;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class FakeServer : IDisposable
|
||||
{
|
||||
IWebHost webHost;
|
||||
SemaphoreSlim semaphore;
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
public FakeServer()
|
||||
{
|
||||
_channel = Channel.CreateUnbounded<HttpContext>();
|
||||
semaphore = new SemaphoreSlim(0);
|
||||
}
|
||||
|
||||
Channel<HttpContext> _channel;
|
||||
public async Task Start()
|
||||
{
|
||||
webHost = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseUrls("http://127.0.0.1:0")
|
||||
.Configure(appBuilder =>
|
||||
{
|
||||
appBuilder.Run(async ctx =>
|
||||
{
|
||||
await _channel.Writer.WriteAsync(ctx);
|
||||
await semaphore.WaitAsync(cts.Token);
|
||||
});
|
||||
})
|
||||
.Build();
|
||||
await webHost.StartAsync();
|
||||
var port = new Uri(webHost.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First(), UriKind.Absolute)
|
||||
.Port;
|
||||
ServerUri = new Uri($"http://127.0.0.1:{port}/");
|
||||
}
|
||||
|
||||
public Uri ServerUri { get; set; }
|
||||
|
||||
public void Done()
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
|
||||
public async Task Stop()
|
||||
{
|
||||
await webHost.StopAsync();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
cts.Dispose();
|
||||
webHost?.Dispose();
|
||||
semaphore.Dispose();
|
||||
}
|
||||
|
||||
public async Task<HttpContext> GetNextRequest()
|
||||
{
|
||||
return await _channel.Reader.ReadAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,27 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNet.SignalR.Client;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Org.BouncyCastle.Utilities.Collections;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
using JsonReader = Newtonsoft.Json.JsonReader;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -23,7 +33,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public GreenfieldAPITests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
@ -44,10 +54,10 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(apiKeyData);
|
||||
Assert.Equal(client.APIKey, apiKeyData.ApiKey);
|
||||
Assert.Single(apiKeyData.Permissions);
|
||||
|
||||
|
||||
//a client using Basic Auth has no business here
|
||||
await AssertHttpError(401, async () => await clientBasic.GetCurrentAPIKeyInfo());
|
||||
|
||||
|
||||
//revoke current api key
|
||||
await client.RevokeCurrentAPIKeyInfo();
|
||||
await AssertHttpError(401, async () => await client.GetCurrentAPIKeyInfo());
|
||||
@ -55,6 +65,7 @@ namespace BTCPayServer.Tests
|
||||
await AssertHttpError(401, async () => await clientBasic.RevokeCurrentAPIKeyInfo());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteAPIKeyViaAPI()
|
||||
@ -68,18 +79,19 @@ namespace BTCPayServer.Tests
|
||||
var apiKey = await unrestricted.CreateAPIKey(new CreateApiKeyRequest()
|
||||
{
|
||||
Label = "Hello world",
|
||||
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
|
||||
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
|
||||
});
|
||||
Assert.Equal("Hello world", apiKey.Label);
|
||||
var p = Assert.Single(apiKey.Permissions);
|
||||
Assert.Equal(Policies.CanViewProfile, p.Policy);
|
||||
|
||||
var restricted = acc.CreateClientFromAPIKey(apiKey.ApiKey);
|
||||
await AssertHttpError(403, async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
|
||||
{
|
||||
Label = "Hello world2",
|
||||
Permissions = new Permission[] { Permission.Create(Policies.CanViewProfile) }
|
||||
}));
|
||||
await AssertHttpError(403,
|
||||
async () => await restricted.CreateAPIKey(new CreateApiKeyRequest()
|
||||
{
|
||||
Label = "Hello world2",
|
||||
Permissions = new Permission[] {Permission.Create(Policies.CanViewProfile)}
|
||||
}));
|
||||
|
||||
await unrestricted.RevokeAPIKey(apiKey.ApiKey);
|
||||
await AssertHttpError(404, async () => await unrestricted.RevokeAPIKey(apiKey.ApiKey));
|
||||
@ -95,35 +107,54 @@ namespace BTCPayServer.Tests
|
||||
tester.PayTester.DisableRegistration = true;
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
|
||||
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com" }));
|
||||
await AssertValidationError(new[] { "Email", "Password" },
|
||||
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()));
|
||||
await AssertValidationError(new[] { "Password" },
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test@gmail.com"}));
|
||||
// Pass too simple
|
||||
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "a" }));
|
||||
await AssertValidationError(new[] { "Password" },
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "a"}));
|
||||
|
||||
// We have no admin, so it should work
|
||||
var user1 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
|
||||
var user1 = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test@gmail.com", Password = "abceudhqw"});
|
||||
// We have no admin, so it should work
|
||||
var user2 = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
|
||||
var user2 = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"});
|
||||
|
||||
// Duplicate email
|
||||
await AssertHttpError(400, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" }));
|
||||
await AssertValidationError(new[] { "Email" },
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test2@gmail.com", Password = "abceudhqw"}));
|
||||
|
||||
// Let's make an admin
|
||||
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true });
|
||||
var admin = await unauthClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin@gmail.com", Password = "abceudhqw", IsAdministrator = true
|
||||
});
|
||||
|
||||
// Creating a new user without proper creds is now impossible (unauthorized)
|
||||
// Because if registration are locked and that an admin exists, we don't accept unauthenticated connection
|
||||
await AssertHttpError(401, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" }));
|
||||
await AssertHttpError(401,
|
||||
async () => await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"}));
|
||||
|
||||
|
||||
// But should be ok with subscriptions unlocked
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
|
||||
await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "test3@gmail.com", Password = "afewfoiewiou" });
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
|
||||
await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test3@gmail.com", Password = "afewfoiewiou"});
|
||||
|
||||
// But it should be forbidden to create an admin without being authenticated
|
||||
await AssertHttpError(403, async () => await unauthClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = true });
|
||||
await AssertHttpError(403,
|
||||
async () => await unauthClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin2@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
}));
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = true});
|
||||
|
||||
var adminAcc = tester.NewAccount();
|
||||
adminAcc.UserId = admin.Id;
|
||||
@ -131,32 +162,110 @@ namespace BTCPayServer.Tests
|
||||
var adminClient = await adminAcc.CreateClient(Policies.CanModifyProfile);
|
||||
|
||||
// We should be forbidden to create a new user without proper admin permissions
|
||||
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" }));
|
||||
await AssertHttpError(403, async () => await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
|
||||
await AssertHttpError(403,
|
||||
async () => await adminClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"}));
|
||||
await AssertHttpError(403,
|
||||
async () => await adminClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "test4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
}));
|
||||
|
||||
// However, should be ok with the unrestricted permissions of an admin
|
||||
adminClient = await adminAcc.CreateClient(Policies.Unrestricted);
|
||||
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "test4@gmail.com", Password = "afewfoiewiou" });
|
||||
await adminClient.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test4@gmail.com", Password = "afewfoiewiou"});
|
||||
// Even creating new admin should be ok
|
||||
await adminClient.CreateUser(new CreateApplicationUserRequest() { Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true });
|
||||
await adminClient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin4@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
});
|
||||
|
||||
var user1Acc = tester.NewAccount();
|
||||
user1Acc.UserId = user1.Id;
|
||||
user1Acc.IsAdmin = false;
|
||||
var user1Client = await user1Acc.CreateClient(Policies.CanModifyServerSettings);
|
||||
|
||||
|
||||
// User1 trying to get server management would still fail to create user
|
||||
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" }));
|
||||
await AssertHttpError(403,
|
||||
async () => await user1Client.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"}));
|
||||
|
||||
// User1 should be able to create user if subscription unlocked
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() { LockSubscription = false });
|
||||
await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "test8@gmail.com", Password = "afewfoiewiou" });
|
||||
await settings.UpdateSetting<PoliciesSettings>(new PoliciesSettings() {LockSubscription = false});
|
||||
await user1Client.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = "test8@gmail.com", Password = "afewfoiewiou"});
|
||||
|
||||
// But not an admin
|
||||
await AssertHttpError(403, async () => await user1Client.CreateUser(new CreateApplicationUserRequest() { Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true }));
|
||||
await AssertHttpError(403,
|
||||
async () => await user1Client.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = "admin8@gmail.com", Password = "afewfoiewiou", IsAdministrator = true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task StoresControllerTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
|
||||
//create store
|
||||
var newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
|
||||
|
||||
//update store
|
||||
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest() {Name = "B"});
|
||||
Assert.Equal("B", updatedStore.Name);
|
||||
Assert.Equal("B", (await client.GetStore(newStore.Id)).Name);
|
||||
|
||||
//list stores
|
||||
var stores = await client.GetStores();
|
||||
var storeIds = stores.Select(data => data.Id);
|
||||
var storeNames = stores.Select(data => data.Name);
|
||||
Assert.NotNull(stores);
|
||||
Assert.Equal(2, stores.Count());
|
||||
Assert.Contains(newStore.Id, storeIds);
|
||||
Assert.Contains(user.StoreId, storeIds);
|
||||
|
||||
//get store
|
||||
var store = await client.GetStore(user.StoreId);
|
||||
Assert.Equal(user.StoreId, store.Id);
|
||||
Assert.Contains(store.Name, storeNames);
|
||||
|
||||
//remove store
|
||||
await client.RemoveStore(newStore.Id);
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await client.GetStore(newStore.Id);
|
||||
});
|
||||
Assert.Single(await client.GetStores());
|
||||
|
||||
newStore = await client.CreateStore(new CreateStoreRequest() {Name = "A"});
|
||||
var scopedClient =
|
||||
await user.CreateClient(Permission.Create(Policies.CanViewStoreSettings, user.StoreId).ToString());
|
||||
Assert.Single(await scopedClient.GetStores());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AssertValidationError(string[] fields, Func<Task> act)
|
||||
{
|
||||
var remainingFields = fields.ToHashSet();
|
||||
var ex = await Assert.ThrowsAsync<GreenFieldValidationException>(act);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
Assert.Contains(field, ex.ValidationErrors.Select(e => e.Path).ToArray());
|
||||
remainingFields.Remove(field);
|
||||
}
|
||||
Assert.Empty(remainingFields);
|
||||
}
|
||||
|
||||
private async Task AssertHttpError(int code, Func<Task> act)
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(act);
|
||||
@ -190,43 +299,200 @@ namespace BTCPayServer.Tests
|
||||
await clientProfile.GetCurrentUser();
|
||||
await clientBasic.GetCurrentUser();
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () =>
|
||||
await clientInsufficient.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
|
||||
var newUser = await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
});
|
||||
Assert.NotNull(newUser);
|
||||
|
||||
var newUser2 = await clientBasic.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
Email = $"{Guid.NewGuid()}@g.com", Password = Guid.NewGuid().ToString()
|
||||
});
|
||||
Assert.NotNull(newUser2);
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}",
|
||||
Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
await AssertValidationError(new[] { "Email" }, async () =>
|
||||
await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}", Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Email = $"{Guid.NewGuid()}@g.com",
|
||||
}));
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientServer.CreateUser(new CreateApplicationUserRequest()
|
||||
{
|
||||
Password = Guid.NewGuid().ToString()
|
||||
}));
|
||||
await AssertValidationError(new[] { "Password" }, async () =>
|
||||
await clientServer.CreateUser(
|
||||
new CreateApplicationUserRequest() {Email = $"{Guid.NewGuid()}@g.com",}));
|
||||
|
||||
await AssertValidationError(new[] { "Email" }, async () =>
|
||||
await clientServer.CreateUser(
|
||||
new CreateApplicationUserRequest() {Password = Guid.NewGuid().ToString()}));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task HealthControllerTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
|
||||
var apiHealthData = await unauthClient.GetHealth();
|
||||
Assert.NotNull(apiHealthData);
|
||||
Assert.True(apiHealthData.Synchronized);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ServerInfoControllerTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var unauthClient = new BTCPayServerClient(tester.PayTester.ServerUri);
|
||||
await AssertHttpError(401, async () => await unauthClient.GetServerInfo());
|
||||
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var clientBasic = await user.CreateClient();
|
||||
var serverInfoData = await clientBasic.GetServerInfo();
|
||||
Assert.NotNull(serverInfoData);
|
||||
Assert.NotNull(serverInfoData.Version);
|
||||
Assert.NotNull(serverInfoData.Onion);
|
||||
Assert.True(serverInfoData.FullySynched);
|
||||
Assert.Contains("BTC", serverInfoData.SupportedPaymentMethods);
|
||||
Assert.Contains("BTC_LightningLike", serverInfoData.SupportedPaymentMethods);
|
||||
Assert.NotNull(serverInfoData.SyncStatus);
|
||||
Assert.Single(serverInfoData.SyncStatus.Select(s => s.CryptoCode == "BTC"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task PaymentControllerTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
||||
|
||||
//create payment request
|
||||
|
||||
//validation errors
|
||||
await AssertValidationError(new[] { "Amount", "Currency" }, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId, new CreatePaymentRequestRequest() {Title = "A"});
|
||||
});
|
||||
await AssertValidationError(new[] { "Amount" }, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "BTC", Amount = 0});
|
||||
});
|
||||
await AssertValidationError(new[] { "Currency" }, async () =>
|
||||
{
|
||||
await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
|
||||
});
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "helloinvalid", Amount = 1});
|
||||
});
|
||||
var newPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Title = "A", Currency = "USD", Amount = 1});
|
||||
|
||||
//list payment request
|
||||
var paymentRequests = await viewOnly.GetPaymentRequests(user.StoreId);
|
||||
|
||||
Assert.NotNull(paymentRequests);
|
||||
Assert.Single(paymentRequests);
|
||||
Assert.Equal(newPaymentRequest.Id, paymentRequests.First().Id);
|
||||
|
||||
//get payment request
|
||||
var paymentRequest = await viewOnly.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
|
||||
Assert.Equal(newPaymentRequest.Title, paymentRequest.Title);
|
||||
|
||||
//update payment request
|
||||
var updateRequest = JObject.FromObject(paymentRequest).ToObject<UpdatePaymentRequestRequest>();
|
||||
updateRequest.Title = "B";
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
|
||||
});
|
||||
await client.UpdatePaymentRequest(user.StoreId, paymentRequest.Id, updateRequest);
|
||||
paymentRequest = await client.GetPaymentRequest(user.StoreId, newPaymentRequest.Id);
|
||||
Assert.Equal(updateRequest.Title, paymentRequest.Title);
|
||||
|
||||
//archive payment request
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
|
||||
});
|
||||
|
||||
await client.ArchivePaymentRequest(user.StoreId, paymentRequest.Id);
|
||||
Assert.DoesNotContain(paymentRequest.Id,
|
||||
(await client.GetPaymentRequests(user.StoreId)).Select(data => data.Id));
|
||||
|
||||
//let's test some payment stuff
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var paymentTestPaymentRequest = await client.CreatePaymentRequest(user.StoreId,
|
||||
new CreatePaymentRequestRequest() {Amount = 0.1m, Currency = "BTC", Title = "Payment test title"});
|
||||
|
||||
var invoiceId = Assert.IsType<string>(Assert.IsType<OkObjectResult>(await user.GetController<PaymentRequestController>()
|
||||
.PayPaymentRequest(paymentTestPaymentRequest.Id, false)).Value);
|
||||
var invoice = user.BitPay.GetInvoice(invoiceId);
|
||||
await tester.WaitForEvent<InvoiceDataChangedEvent>(async () =>
|
||||
{
|
||||
await tester.ExplorerNode.SendToAddressAsync(
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void DecimalStringJsonConverterTests()
|
||||
{
|
||||
JsonReader Get(string val)
|
||||
{
|
||||
return new JsonTextReader(new StringReader(val));
|
||||
}
|
||||
|
||||
var jsonConverter = new DecimalStringJsonConverter();
|
||||
Assert.True(jsonConverter.CanConvert(typeof(decimal)));
|
||||
Assert.True(jsonConverter.CanConvert(typeof(decimal?)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(double)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(float)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(int)));
|
||||
Assert.False(jsonConverter.CanConvert(typeof(string)));
|
||||
|
||||
var numberJson = "1";
|
||||
var numberDecimalJson = "1.2";
|
||||
var stringJson = "\"1.2\"";
|
||||
Assert.Equal(1m, jsonConverter.ReadJson(Get(numberJson), typeof(decimal), null, null));
|
||||
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(numberDecimalJson), typeof(decimal), null, null));
|
||||
Assert.Null(jsonConverter.ReadJson(Get("null"), typeof(decimal?), null, null));
|
||||
Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
|
||||
});
|
||||
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal), null, null));
|
||||
Assert.Equal(1.2m, jsonConverter.ReadJson(Get(stringJson), typeof(decimal?), null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,10 +66,6 @@ namespace BTCPayServer.Tests
|
||||
FeeSatoshiPerByte = 1,
|
||||
CurrentBalance = 1.5m
|
||||
};
|
||||
var vmLedger = await walletController.WalletSend(walletId, sendModel, command: "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
|
||||
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
|
||||
Assert.NotNull(vmLedger.WebsocketPath);
|
||||
|
||||
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
|
||||
@ -79,20 +75,25 @@ namespace BTCPayServer.Tests
|
||||
var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
|
||||
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
|
||||
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
SigningContext = new SigningContextModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
}
|
||||
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
|
||||
Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);
|
||||
|
||||
var signedPSBT = unsignedPSBT.Clone();
|
||||
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
|
||||
vmPSBT.PSBT = signedPSBT.ToBase64();
|
||||
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
SigningContext = new SigningContextModel()
|
||||
{
|
||||
PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
|
||||
}
|
||||
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
|
||||
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
|
||||
@ -120,8 +121,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(signedPSBT2.TryFinalize(out _));
|
||||
Assert.Equal(signedPSBT, signedPSBT2);
|
||||
|
||||
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
|
||||
var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
|
||||
{
|
||||
SigningContext = new SigningContextModel(signedPSBT)
|
||||
})).AssertViewModel<WalletPSBTReadyViewModel>();
|
||||
Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
|
||||
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
|
||||
Assert.Equal(signedPSBT.ToBase64(), psbt);
|
||||
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
|
||||
@ -134,7 +138,7 @@ namespace BTCPayServer.Tests
|
||||
var postRedirectView = Assert.IsType<ViewResult>(view);
|
||||
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
|
||||
Assert.Equal(actionName, postRedirectViewModel.AspAction);
|
||||
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value;
|
||||
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt" || p.Key == "SigningContext.PSBT").Value;
|
||||
return redirectedPSBT;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@ -26,6 +28,9 @@ using NBitcoin;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitcoin.Payment;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
@ -38,7 +43,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public PayJoinTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
@ -73,16 +78,16 @@ namespace BTCPayServer.Tests
|
||||
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var repo = tester.PayTester.GetService<PayJoinRepository>();
|
||||
var outpoint = RandomOutpoint();
|
||||
|
||||
|
||||
// Should not be locked
|
||||
Assert.False(await repo.TryUnlock(outpoint));
|
||||
|
||||
|
||||
// Can lock input
|
||||
Assert.True(await repo.TryLockInputs(new [] { outpoint }));
|
||||
Assert.True(await repo.TryLockInputs(new[] { outpoint }));
|
||||
// Can't twice
|
||||
Assert.False(await repo.TryLockInputs(new [] { outpoint }));
|
||||
Assert.False(await repo.TryLockInputs(new[] { outpoint }));
|
||||
Assert.False(await repo.TryUnlock(outpoint));
|
||||
|
||||
|
||||
// Lock and unlock outpoint utxo
|
||||
Assert.True(await repo.TryLock(outpoint));
|
||||
Assert.True(await repo.TryUnlock(outpoint));
|
||||
@ -90,6 +95,45 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task ChooseBestUTXOsForPayjoin()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
|
||||
|
||||
//Only one utxo, so obvious result
|
||||
var utxos = new[] { FakeUTXO(1.0m) };
|
||||
var paymentAmount = 0.5m;
|
||||
var otherOutputs = new[] { 0.5m };
|
||||
var inputs = new[] { 1m };
|
||||
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
||||
Assert.Contains(result.selectedUTXO, utxo => utxos.Contains(utxo));
|
||||
|
||||
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
|
||||
utxos = new[] { FakeUTXO(0.3m), FakeUTXO(0.7m) };
|
||||
paymentAmount = 0.5m;
|
||||
otherOutputs = new[] { 0.5m };
|
||||
inputs = new[] { 1m };
|
||||
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
|
||||
|
||||
//when there is no change, anything works
|
||||
utxos = new[] { FakeUTXO(1), FakeUTXO(0.1m), FakeUTXO(0.001m), FakeUTXO(0.003m) };
|
||||
paymentAmount = 0.5m;
|
||||
otherOutputs = new decimal[0];
|
||||
inputs = new[] { 0.03m, 0.07m };
|
||||
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
|
||||
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Transaction RandomTransaction(BTCPayNetwork network)
|
||||
{
|
||||
var tx = network.NBitcoinNetwork.CreateTransaction();
|
||||
@ -98,6 +142,15 @@ namespace BTCPayServer.Tests
|
||||
return tx;
|
||||
}
|
||||
|
||||
private UTXO FakeUTXO(decimal amount)
|
||||
{
|
||||
return new UTXO()
|
||||
{
|
||||
Value = new Money(amount, MoneyUnit.BTC),
|
||||
Outpoint = RandomOutpoint()
|
||||
};
|
||||
}
|
||||
|
||||
private OutPoint RandomOutpoint()
|
||||
{
|
||||
return new OutPoint(RandomUtils.GetUInt256(), 0);
|
||||
@ -121,18 +174,18 @@ namespace BTCPayServer.Tests
|
||||
var unsupportedFormats = Enum.GetValues(typeof(ScriptPubKeyType))
|
||||
.AssertType<ScriptPubKeyType[]>()
|
||||
.Where(type => !PayjoinClient.SupportedFormats.Contains(type));
|
||||
|
||||
|
||||
|
||||
|
||||
foreach (ScriptPubKeyType senderAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||
{
|
||||
var senderUser = tester.NewAccount();
|
||||
senderUser.GrantAccess(true);
|
||||
senderUser.RegisterDerivationScheme("BTC", senderAddressType);
|
||||
|
||||
|
||||
foreach (ScriptPubKeyType receiverAddressType in Enum.GetValues(typeof(ScriptPubKeyType)))
|
||||
{
|
||||
var senderCoin = await senderUser.ReceiveUTXO(Money.Satoshis(100000), network);
|
||||
|
||||
|
||||
Logs.Tester.LogInformation($"Testing payjoin with sender: {senderAddressType} receiver: {receiverAddressType}");
|
||||
var receiverUser = tester.NewAccount();
|
||||
receiverUser.GrantAccess(true);
|
||||
@ -141,13 +194,16 @@ namespace BTCPayServer.Tests
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
|
||||
var clientShouldError = unsupportedFormats.Contains(senderAddressType);
|
||||
var errorCode = ( unsupportedFormats.Contains( receiverAddressType) || receiverAddressType != senderAddressType)? "unsupported-inputs" : null;
|
||||
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = 50000, Currency = "sats", FullNotifications = true});
|
||||
|
||||
string errorCode = receiverAddressType == senderAddressType ? null : "unavailable|any UTXO available";
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = 50000, Currency = "sats", FullNotifications = true });
|
||||
if (unsupportedFormats.Contains(receiverAddressType))
|
||||
{
|
||||
Assert.Null(TestAccount.GetPayjoinEndpoint(invoice, cashCow.Network));
|
||||
continue;
|
||||
}
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
|
||||
|
||||
txBuilder.AddCoins(senderCoin);
|
||||
txBuilder.Send(invoiceAddress, invoice.BtcDue);
|
||||
txBuilder.SetChange(await senderUser.GetNewAddress(network));
|
||||
@ -157,132 +213,314 @@ namespace BTCPayServer.Tests
|
||||
var pj = await senderUser.SubmitPayjoin(invoice, psbt, errorCode, clientShouldError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanUsePayjoinViaUI()
|
||||
{
|
||||
using (var s = SeleniumTester.Create())
|
||||
{
|
||||
await s.StartAsync();
|
||||
var invoiceRepository = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
// var payjoinRepository = s.Server.PayTester.GetService<PayJoinRepository>();
|
||||
// var broadcaster = s.Server.PayTester.GetService<DelayedTransactionBroadcaster>();
|
||||
s.RegisterNewUser(true);
|
||||
var receiver = s.CreateNewStore();
|
||||
var receiverSeed = s.GenerateWallet("BTC", "", true, true);
|
||||
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
|
||||
|
||||
//payjoin is not enabled by default.
|
||||
var invoiceId = s.CreateInvoice(receiver.storeId);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
s.GoToHome();
|
||||
s.GoToStore(receiver.storeId);
|
||||
//payjoin is not enabled by default.
|
||||
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
s.SetCheckbox(s,"PayJoinEnabled", true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
var sender = s.CreateNewStore();
|
||||
var senderSeed = s.GenerateWallet("BTC", "", true, true);
|
||||
var senderWalletId = new WalletId(sender.storeId, "BTC");
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
await s.FundStoreWallet(senderWalletId);
|
||||
|
||||
invoiceId = s.CreateInvoice(receiver.storeId);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
s.GoToWalletSend(senderWalletId);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
|
||||
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||
foreach (var format in PayjoinClient.SupportedFormats)
|
||||
{
|
||||
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
//no funds in receiver wallet to do payjoin
|
||||
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
||||
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
|
||||
});
|
||||
var receiver = s.CreateNewStore();
|
||||
var receiverSeed = s.GenerateWallet("BTC", "", true, true, format);
|
||||
var receiverWalletId = new WalletId(receiver.storeId, "BTC");
|
||||
|
||||
s.GoToInvoices();
|
||||
var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
|
||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
|
||||
//payjoin is not enabled by default.
|
||||
var invoiceId = s.CreateInvoice(receiver.storeName);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
//let's do it all again, except now the receiver has funds and is able to payjoin
|
||||
invoiceId = s.CreateInvoice(receiver.storeId);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
s.GoToHome();
|
||||
s.GoToStore(receiver.storeId);
|
||||
//payjoin is not enabled by default.
|
||||
Assert.False(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
s.SetCheckbox(s, "PayJoinEnabled", true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.Id("PayJoinEnabled")).Selected);
|
||||
var sender = s.CreateNewStore();
|
||||
var senderSeed = s.GenerateWallet("BTC", "", true, true, format);
|
||||
var senderWalletId = new WalletId(sender.storeId, "BTC");
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
await s.FundStoreWallet(senderWalletId);
|
||||
|
||||
s.GoToWalletSend(senderWalletId);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl")).GetAttribute("value")));
|
||||
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
|
||||
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("1");
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
|
||||
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||
{
|
||||
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||
var payments = invoice.GetPayments();
|
||||
Assert.Equal(2, payments.Count);
|
||||
var originalPayment = payments[0];
|
||||
var coinjoinPayment = payments[1];
|
||||
Assert.Equal(-1, ((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||
Assert.Equal(0, ((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||
Assert.False(originalPayment.Accounted);
|
||||
Assert.True(coinjoinPayment.Accounted);
|
||||
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
|
||||
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
|
||||
Assert.Equal(originalPayment.GetCryptoPaymentData()
|
||||
.AssertType<BitcoinLikePaymentData>()
|
||||
.Value,
|
||||
coinjoinPayment.GetCryptoPaymentData()
|
||||
.AssertType<BitcoinLikePaymentData>()
|
||||
.Value);
|
||||
});
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
|
||||
});
|
||||
s.GoToInvoices();
|
||||
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}")).FindElement(By.ClassName("payment-value"));
|
||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin", StringComparison.InvariantCultureIgnoreCase));
|
||||
invoiceId = s.CreateInvoice(receiver.storeName);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}=", bip21);
|
||||
|
||||
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl"))
|
||||
.GetAttribute("value")));
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
|
||||
await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||
{
|
||||
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
//no funds in receiver wallet to do payjoin
|
||||
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Warning);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
||||
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
|
||||
});
|
||||
|
||||
s.GoToInvoices();
|
||||
var paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}"))
|
||||
.FindElement(By.ClassName("payment-value"));
|
||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
//let's do it all again, except now the receiver has funds and is able to payjoin
|
||||
invoiceId = s.CreateInvoice(receiver.storeName);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
bip21 = s.Driver.FindElement(By.ClassName("payment__details__instruction__open-wallet__btn"))
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
s.GoToWallet(senderWalletId, WalletsNavPages.Send);
|
||||
s.Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
s.Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
s.Driver.SwitchTo().Alert().Accept();
|
||||
Assert.False(string.IsNullOrEmpty(s.Driver.FindElement(By.Id("PayJoinEndpointUrl"))
|
||||
.GetAttribute("value")));
|
||||
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).Clear();
|
||||
s.Driver.FindElement(By.Id("FeeSatoshiPerByte")).SendKeys("1");
|
||||
s.Driver.ScrollTo(By.Id("SendMenu"));
|
||||
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
|
||||
var txId = await s.Server.WaitForEvent<NewOnChainTransactionEvent>(() =>
|
||||
{
|
||||
s.Driver.FindElement(By.CssSelector("button[value=payjoin]")).ForceClick();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await invoiceRepository.GetInvoice(invoiceId);
|
||||
var payments = invoice.GetPayments();
|
||||
Assert.Equal(2, payments.Count);
|
||||
var originalPayment = payments[0];
|
||||
var coinjoinPayment = payments[1];
|
||||
Assert.Equal(-1,
|
||||
((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||
Assert.Equal(0,
|
||||
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).ConfirmationCount);
|
||||
Assert.False(originalPayment.Accounted);
|
||||
Assert.True(coinjoinPayment.Accounted);
|
||||
Assert.Equal(((BitcoinLikePaymentData)originalPayment.GetCryptoPaymentData()).Value,
|
||||
((BitcoinLikePaymentData)coinjoinPayment.GetCryptoPaymentData()).Value);
|
||||
Assert.Equal(originalPayment.GetCryptoPaymentData()
|
||||
.AssertType<BitcoinLikePaymentData>()
|
||||
.Value,
|
||||
coinjoinPayment.GetCryptoPaymentData()
|
||||
.AssertType<BitcoinLikePaymentData>()
|
||||
.Value);
|
||||
});
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var invoice = await s.Server.PayTester.GetService<InvoiceRepository>().GetInvoice(invoiceId);
|
||||
var dto = invoice.EntityToDTO();
|
||||
Assert.Equal(InvoiceStatus.Paid, invoice.Status);
|
||||
});
|
||||
s.GoToInvoices();
|
||||
paymentValueRowColumn = s.Driver.FindElement(By.Id($"invoice_{invoiceId}"))
|
||||
.FindElement(By.ClassName("payment-value"));
|
||||
Assert.False(paymentValueRowColumn.Text.Contains("payjoin",
|
||||
StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.GoToWallet(receiverWalletId, WalletsNavPages.Transactions);
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
Assert.Contains("payjoin", s.Driver.PageSource);
|
||||
//this label does not always show since input gets used
|
||||
// Assert.Contains("payjoin-exposed", s.Driver.PageSource);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePayjoin2()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var pjClient = tester.PayTester.GetService<PayjoinClient>();
|
||||
var nbx = tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC");
|
||||
var notifications = await nbx.CreateWebsocketNotificationSessionAsync();
|
||||
var alice = tester.NewAccount();
|
||||
await alice.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||
await notifications.ListenDerivationSchemesAsync(new[] { alice.DerivationScheme });
|
||||
var address = (await nbx.GetUnusedAsync(alice.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.0m));
|
||||
await notifications.NextEventAsync();
|
||||
var psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
|
||||
{
|
||||
Destinations =
|
||||
{
|
||||
new CreatePSBTDestination()
|
||||
{
|
||||
Amount = Money.Coins(0.5m),
|
||||
Destination = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest)
|
||||
}
|
||||
},
|
||||
FeePreference = new FeePreference()
|
||||
{
|
||||
ExplicitFee = Money.Satoshis(3000)
|
||||
}
|
||||
})).PSBT;
|
||||
var derivationSchemeSettings = alice.GetController<WalletsController>().GetDerivationSchemeSettings(new WalletId(alice.StoreId, "BTC"));
|
||||
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
||||
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
var changeIndex = Array.FindIndex(psbt.Outputs.ToArray(), (PSBTOutput o) => o.ScriptPubKey.IsScriptType(ScriptType.P2WPKH));
|
||||
using var fakeServer = new FakeServer();
|
||||
await fakeServer.Start();
|
||||
var requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||
var request = await fakeServer.GetNextRequest();
|
||||
Assert.Equal("1", request.Request.Query["v"][0]);
|
||||
Assert.Equal(changeIndex.ToString(), request.Request.Query["additionalfeeoutputindex"][0]);
|
||||
Assert.Equal("3000", request.Request.Query["maxadditionalfeecontribution"][0]);
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to make us pay lots of fee");
|
||||
var originalPSBT = await ParsePSBT(request);
|
||||
var proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3001);
|
||||
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||
fakeServer.Done();
|
||||
var ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||
Assert.Contains("too much fee", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver tries to send money to himself");
|
||||
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
proposalTx.Outputs.Where((o, i) => i != changeIndex).First().Value += Money.Satoshis(1);
|
||||
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(1);
|
||||
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||
fakeServer.Done();
|
||||
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||
Assert.Contains("money to himself", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver can't increase the fee rate too much");
|
||||
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3000);
|
||||
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||
fakeServer.Done();
|
||||
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||
Assert.Contains("increased the fee rate", ex.Message);
|
||||
|
||||
Logs.Tester.LogInformation("The payjoin receiver can't decrease the fee rate too much");
|
||||
pjClient.MinimumFeeRate = new FeeRate(50m);
|
||||
requesting = pjClient.RequestPayjoin(fakeServer.ServerUri, derivationSchemeSettings, psbt, default);
|
||||
request = await fakeServer.GetNextRequest();
|
||||
originalPSBT = await ParsePSBT(request);
|
||||
proposalTx = originalPSBT.GetGlobalTransaction();
|
||||
proposalTx.Outputs[changeIndex].Value -= Money.Satoshis(3000);
|
||||
await request.Response.WriteAsync(PSBT.FromTransaction(proposalTx, Network.RegTest).ToBase64(), Encoding.UTF8);
|
||||
fakeServer.Done();
|
||||
ex = await Assert.ThrowsAsync<PayjoinSenderException>(async () => await requesting);
|
||||
Assert.Contains("a too low fee rate", ex.Message);
|
||||
pjClient.MinimumFeeRate = null;
|
||||
|
||||
Logs.Tester.LogInformation("Make sure the receiver implementation do not take more fee than allowed");
|
||||
var bob = tester.NewAccount();
|
||||
await bob.GrantAccessAsync();
|
||||
await bob.RegisterDerivationSchemeAsync("BTC", ScriptPubKeyType.Segwit, true);
|
||||
await notifications.ListenDerivationSchemesAsync(new[] { bob.DerivationScheme });
|
||||
address = (await nbx.GetUnusedAsync(bob.DerivationScheme, DerivationFeature.Deposit)).Address;
|
||||
tester.ExplorerNode.SendToAddress(address, Money.Coins(1.1m));
|
||||
await notifications.NextEventAsync();
|
||||
bob.ModifyStore(s => s.PayJoinEnabled = true);
|
||||
var invoice = bob.BitPay.CreateInvoice(
|
||||
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
|
||||
var invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
|
||||
{
|
||||
Destinations =
|
||||
{
|
||||
new CreatePSBTDestination()
|
||||
{
|
||||
Amount = invoiceBIP21.Amount,
|
||||
Destination = invoiceBIP21.Address
|
||||
}
|
||||
},
|
||||
FeePreference = new FeePreference()
|
||||
{
|
||||
ExplicitFee = Money.Satoshis(3001)
|
||||
}
|
||||
})).PSBT;
|
||||
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
|
||||
pjClient.MaxFeeBumpContribution = Money.Satoshis(50);
|
||||
var proposal = await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default);
|
||||
Assert.True(proposal.TryGetFee(out var newFee));
|
||||
Assert.Equal(Money.Satoshis(3001 + 50), newFee);
|
||||
proposal = proposal.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
proposal.Finalize();
|
||||
await tester.ExplorerNode.SendRawTransactionAsync(proposal.ExtractTransaction());
|
||||
await notifications.NextEventAsync();
|
||||
|
||||
Logs.Tester.LogInformation("Abusing minFeeRate should give not enough money error");
|
||||
invoice = bob.BitPay.CreateInvoice(
|
||||
new Invoice() { Price = 0.1m, Currency = "BTC", FullNotifications = true });
|
||||
invoiceBIP21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
psbt = (await nbx.CreatePSBTAsync(alice.DerivationScheme, new CreatePSBTRequest()
|
||||
{
|
||||
Destinations =
|
||||
{
|
||||
new CreatePSBTDestination()
|
||||
{
|
||||
Amount = invoiceBIP21.Amount,
|
||||
Destination = invoiceBIP21.Address
|
||||
}
|
||||
},
|
||||
FeePreference = new FeePreference()
|
||||
{
|
||||
ExplicitFee = Money.Satoshis(3001)
|
||||
}
|
||||
})).PSBT;
|
||||
psbt.SignAll(derivationSchemeSettings.AccountDerivation, alice.GenerateWalletResponseV.AccountHDKey, signingAccount.GetRootedKeyPath());
|
||||
endpoint = TestAccount.GetPayjoinEndpoint(invoice, Network.RegTest);
|
||||
pjClient.MinimumFeeRate = new FeeRate(100_000_000.2m);
|
||||
var ex2 = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, derivationSchemeSettings, psbt, default));
|
||||
Assert.Equal(PayjoinReceiverWellknownErrors.NotEnoughMoney, ex2.WellknownError);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PSBT> ParsePSBT(Microsoft.AspNetCore.Http.HttpContext request)
|
||||
{
|
||||
var bytes = await request.Request.Body.ReadBytesAsync(int.Parse(request.Request.Headers["Content-Length"].First()));
|
||||
var str = Encoding.UTF8.GetString(bytes);
|
||||
return PSBT.Parse(str, Network.RegTest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePayjoinFeeCornerCase()
|
||||
@ -309,14 +547,15 @@ namespace BTCPayServer.Tests
|
||||
var receiverCoin = await receiverUser.ReceiveUTXO(Money.Satoshis(810), network);
|
||||
string lastInvoiceId = null;
|
||||
|
||||
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
|
||||
var vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money", OriginalTxBroadcasted: true);
|
||||
async Task<PSBT> RunVector(bool skipLockedCheck = false)
|
||||
{
|
||||
var coin = await senderUser.ReceiveUTXO(vector.SpentCoin, network);
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() {Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true});
|
||||
var invoice = receiverUser.BitPay.CreateInvoice(new Invoice() { Price = vector.InvoiceAmount.ToDecimal(MoneyUnit.BTC), Currency = "BTC", FullNotifications = true });
|
||||
lastInvoiceId = invoice.Id;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
txBuilder.OptInRBF = true;
|
||||
txBuilder.AddCoins(coin);
|
||||
txBuilder.Send(invoiceAddress, vector.Paid);
|
||||
txBuilder.SendFees(vector.Fee);
|
||||
@ -327,6 +566,10 @@ namespace BTCPayServer.Tests
|
||||
if (vector.ExpectedError is null)
|
||||
{
|
||||
Assert.Contains(pj.Inputs, o => o.PrevOut == receiverCoin.Outpoint);
|
||||
foreach (var input in pj.GetGlobalTransaction().Inputs)
|
||||
{
|
||||
Assert.Equal(Sequence.OptInRBF, input.Sequence);
|
||||
}
|
||||
if (!skipLockedCheck)
|
||||
Assert.True(await payjoinRepository.TryUnlock(receiverCoin.Outpoint));
|
||||
}
|
||||
@ -345,6 +588,19 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
}
|
||||
|
||||
psbt.Finalize();
|
||||
var broadcasted = await tester.PayTester.GetService<ExplorerClientProvider>().GetExplorerClient("BTC").BroadcastAsync(psbt.ExtractTransaction(), true);
|
||||
if (vector.OriginalTxBroadcasted)
|
||||
{
|
||||
Assert.Equal("txn-already-in-mempool", broadcasted.RPCCodeMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(broadcasted.Success);
|
||||
}
|
||||
receiverCoin = await receiverUser.ReceiveUTXO(receiverCoin.Amount, network);
|
||||
await LockAllButReceiverCoin();
|
||||
return pj;
|
||||
}
|
||||
|
||||
@ -363,32 +619,35 @@ namespace BTCPayServer.Tests
|
||||
Logs.Tester.LogInformation("Here we send exactly the right amount. This should fails as\n" +
|
||||
"there is not enough to pay the additional payjoin input. (going below the min relay fee" +
|
||||
"However, the original tx has been broadcasted!");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "not-enough-money", OriginalTxBroadcasted: true);
|
||||
await RunVector();
|
||||
await LockAllButReceiverCoin();
|
||||
|
||||
|
||||
Logs.Tester.LogInformation("We don't pay enough");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(700), Paid: Money.Satoshis(690), Fee: Money.Satoshis(110), InvoicePaid: false, ExpectedError: "invoice-not-fully-paid", OriginalTxBroadcasted: true);
|
||||
await RunVector();
|
||||
|
||||
Logs.Tester.LogInformation("We pay correctly");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
|
||||
await RunVector();
|
||||
await LockAllButReceiverCoin();
|
||||
|
||||
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
|
||||
"The receiver should have added a fake output");
|
||||
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string);
|
||||
var proposedPSBT = await RunVector();
|
||||
Assert.Equal(2, proposedPSBT.Outputs.Count);
|
||||
await LockAllButReceiverCoin();
|
||||
|
||||
|
||||
PSBT proposedPSBT = null;
|
||||
var outputCountReceived = new bool[2];
|
||||
do
|
||||
{
|
||||
Logs.Tester.LogInformation("We pay a little bit more the invoice with enough fees to support additional input\n" +
|
||||
"The receiver should have added a fake output");
|
||||
vector = (SpentCoin: Money.Satoshis(910), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(700), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
|
||||
proposedPSBT = await RunVector();
|
||||
Assert.True(proposedPSBT.Outputs.Count == 1 || proposedPSBT.Outputs.Count == 2);
|
||||
outputCountReceived[proposedPSBT.Outputs.Count - 1] = true;
|
||||
cashCow.Generate(1);
|
||||
} while (outputCountReceived.All(o => o));
|
||||
|
||||
Logs.Tester.LogInformation("We pay correctly, but no utxo\n" +
|
||||
"However, this has the side effect of having the receiver broadcasting the original tx");
|
||||
await payjoinRepository.TryLock(receiverCoin.Outpoint);
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "out-of-utxos");
|
||||
vector = (SpentCoin: Money.Satoshis(810), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(500), Fee: Money.Satoshis(110), InvoicePaid: true, ExpectedError: "unavailable|any UTXO available", OriginalTxBroadcasted: true);
|
||||
await RunVector(true);
|
||||
await LockAllButReceiverCoin();
|
||||
|
||||
var originalSenderUser = senderUser;
|
||||
retry:
|
||||
@ -398,7 +657,7 @@ namespace BTCPayServer.Tests
|
||||
// The send pay remaining 86 sat from his pocket
|
||||
// So total paid by sender should be 86 + 510 + 200 so we should get 1090 - (86 + 510 + 200) == 294 back)
|
||||
Logs.Tester.LogInformation($"Check if we can take fee on overpaid utxo{(senderUser == receiverUser ? " (to self)" : "")}");
|
||||
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
|
||||
vector = (SpentCoin: Money.Satoshis(1090), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
|
||||
proposedPSBT = await RunVector();
|
||||
Assert.Equal(2, proposedPSBT.Outputs.Count);
|
||||
Assert.Contains(proposedPSBT.Outputs, o => o.Value == Money.Satoshis(500) + receiverCoin.Amount);
|
||||
@ -430,14 +689,14 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
else
|
||||
{
|
||||
senderUser = originalSenderUser;
|
||||
senderUser = originalSenderUser;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Same as above. Except the sender send one satoshi less, so the change
|
||||
// output would get below dust and would be removed completely.
|
||||
// So we remove as much fee as we can, and still accept the transaction because it is above minrelay fee
|
||||
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string);
|
||||
vector = (SpentCoin: Money.Satoshis(1089), InvoiceAmount: Money.Satoshis(500), Paid: Money.Satoshis(510), Fee: Money.Satoshis(200), InvoicePaid: true, ExpectedError: null as string, OriginalTxBroadcasted: false);
|
||||
proposedPSBT = await RunVector();
|
||||
Assert.Equal(2, proposedPSBT.Outputs.Count);
|
||||
// We should have our payment
|
||||
@ -451,7 +710,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(result.Success);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUsePayjoin()
|
||||
@ -459,7 +718,7 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
|
||||
|
||||
////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>();
|
||||
var btcPayNetwork = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var btcPayWallet = tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet(btcPayNetwork);
|
||||
@ -471,7 +730,7 @@ namespace BTCPayServer.Tests
|
||||
senderUser.RegisterDerivationScheme("BTC", ScriptPubKeyType.Segwit, true);
|
||||
|
||||
var invoice = senderUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 100, Currency = "USD", FullNotifications = true});
|
||||
new Invoice() { Price = 100, Currency = "USD", FullNotifications = true });
|
||||
//payjoin is not enabled by default.
|
||||
Assert.DoesNotContain($"{PayjoinClient.BIP21EndpointKey}", invoice.CryptoInfo.First().PaymentUrls.BIP21);
|
||||
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
||||
@ -484,7 +743,7 @@ namespace BTCPayServer.Tests
|
||||
await receiverUser.EnablePayJoin();
|
||||
// payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer
|
||||
invoice = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||
cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network),
|
||||
Money.Coins(0.06m));
|
||||
var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC");
|
||||
@ -506,13 +765,21 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//Let's start the harassment
|
||||
invoice = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||
// Bad version should throw incorrect version
|
||||
var endpoint = TestAccount.GetPayjoinEndpoint(invoice, btcPayNetwork.NBitcoinNetwork);
|
||||
var response = await tester.PayTester.HttpClient.PostAsync(endpoint.AbsoluteUri + "?v=2",
|
||||
new StringContent("", Encoding.UTF8, "text/plain"));
|
||||
Assert.False(response.IsSuccessStatusCode);
|
||||
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
Assert.Equal("version-unsupported", error["errorCode"].Value<string>());
|
||||
Assert.Equal(1, ((JArray)error["supported"]).Single().Value<int>());
|
||||
|
||||
var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
var invoice2 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.02m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true });
|
||||
var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
@ -585,6 +852,7 @@ namespace BTCPayServer.Tests
|
||||
//Attempt 2: Create two transactions using different inputs and send them to the same invoice.
|
||||
//Result: Second Tx should be rejected.
|
||||
var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork);
|
||||
|
||||
await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid");
|
||||
var contributedInputsInvoice1Coin1ResponseTx =
|
||||
Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut);
|
||||
@ -592,9 +860,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted
|
||||
//Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1
|
||||
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used");
|
||||
await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, expectedError: "unavailable|Some of those inputs have already been used");
|
||||
var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork);
|
||||
|
||||
|
||||
var contributedInputsInvoice2Coin2ResponseTx =
|
||||
Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut);
|
||||
Assert.Single(contributedInputsInvoice2Coin2ResponseTx);
|
||||
@ -603,13 +871,13 @@ namespace BTCPayServer.Tests
|
||||
//Result: reject on 4: the protocol should not worry about this complexity
|
||||
|
||||
var invoice3 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||
var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
|
||||
var invoice4 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||
var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
@ -630,7 +898,7 @@ namespace BTCPayServer.Tests
|
||||
//Result: proposed tx consolidates the outputs
|
||||
|
||||
var invoice5 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||
var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
@ -655,7 +923,7 @@ namespace BTCPayServer.Tests
|
||||
new Money(0.1m, MoneyUnit.BTC)));
|
||||
|
||||
var invoice6 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||
var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
@ -670,7 +938,7 @@ namespace BTCPayServer.Tests
|
||||
var invoice6Coin5 = invoice6Coin5TxBuilder
|
||||
.BuildTransaction(true);
|
||||
|
||||
var Invoice6Coin5Response1Tx =await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
|
||||
var Invoice6Coin5Response1Tx = await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork);
|
||||
var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx);
|
||||
//broadcast the first payjoin
|
||||
await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned);
|
||||
@ -698,17 +966,18 @@ namespace BTCPayServer.Tests
|
||||
new Money(0.1m, MoneyUnit.BTC)));
|
||||
|
||||
var invoice7 = receiverUser.BitPay.CreateInvoice(
|
||||
new Invoice() {Price = 0.01m, Currency = "BTC", FullNotifications = true});
|
||||
new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true });
|
||||
var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21,
|
||||
tester.ExplorerClient.Network.NBitcoinNetwork);
|
||||
|
||||
var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder()
|
||||
var txBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
txBuilder.OptInRBF = true;
|
||||
var invoice7Coin6TxBuilder = txBuilder
|
||||
.SetChange(senderChange)
|
||||
.Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount)
|
||||
.AddCoins(coin6.Coin)
|
||||
.AddKeys(extKey.Derive(coin6.KeyPath))
|
||||
.SendEstimatedFees(new FeeRate(100m))
|
||||
.SetLockTime(0);
|
||||
.SendEstimatedFees(new FeeRate(100m));
|
||||
|
||||
var invoice7Coin6Tx = invoice7Coin6TxBuilder
|
||||
.BuildTransaction(true);
|
||||
@ -717,14 +986,14 @@ namespace BTCPayServer.Tests
|
||||
var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx);
|
||||
var contributedInputsInvoice7Coin6Response1TxSigned =
|
||||
Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut);
|
||||
|
||||
|
||||
|
||||
|
||||
////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId);
|
||||
////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id);
|
||||
//broadcast the payjoin
|
||||
var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned));
|
||||
Assert.True(res.Success);
|
||||
|
||||
|
||||
// Paid with coinjoin
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
|
@ -61,34 +61,38 @@ namespace BTCPayServer.Tests
|
||||
Description = "description"
|
||||
};
|
||||
var id = (Assert
|
||||
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(null, request)).RouteValues.Values.First().ToString());
|
||||
|
||||
|
||||
|
||||
//permission guard for guests editing
|
||||
Assert
|
||||
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
|
||||
.IsType<NotFoundResult>(await guestpaymentRequestController.EditPaymentRequest(id));
|
||||
|
||||
request.Title = "update";
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
|
||||
Assert.IsType<RedirectToActionResult>(await paymentRequestController.EditPaymentRequest(id, request));
|
||||
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
|
||||
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Title);
|
||||
|
||||
Assert.False(string.IsNullOrEmpty(id));
|
||||
|
||||
Assert.IsType<ViewPaymentRequestViewModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
|
||||
.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model);
|
||||
|
||||
//Delete
|
||||
|
||||
Assert.IsType<ConfirmModel>(Assert
|
||||
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
|
||||
//Archive
|
||||
|
||||
Assert
|
||||
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
|
||||
Assert.True(Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived);
|
||||
|
||||
Assert.Empty(Assert.IsType<ListPaymentRequestsViewModel>(Assert.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items);
|
||||
//unarchive
|
||||
Assert
|
||||
.IsType<RedirectToActionResult>(await paymentRequestController.TogglePaymentRequestArchival(id));
|
||||
|
||||
Assert.False(Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(await paymentRequestController.ViewPaymentRequest(id)).Model).Archived);
|
||||
|
||||
Assert.Single(Assert.IsType<ListPaymentRequestsViewModel>(Assert.IsType<ViewResult>(await paymentRequestController.GetPaymentRequests()).Model).Items);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium.Interactions;
|
||||
@ -123,7 +124,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false)
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool importkeys = false, bool privkeys = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
{
|
||||
Driver.FindElement(By.Id($"Modify{cryptoCode}")).ForceClick();
|
||||
Driver.FindElement(By.Id("import-from-btn")).ForceClick();
|
||||
@ -131,6 +132,8 @@ namespace BTCPayServer.Tests
|
||||
Driver.WaitForElement(By.Id("ExistingMnemonic")).SendKeys(seed);
|
||||
SetCheckbox(Driver.WaitForElement(By.Id("SavePrivateKeys")), privkeys);
|
||||
SetCheckbox(Driver.WaitForElement(By.Id("ImportKeysToRPC")), importkeys);
|
||||
Driver.WaitForElement(By.Id("ScriptPubKeyType")).Click();
|
||||
Driver.WaitForElement(By.CssSelector($"#ScriptPubKeyType option[value={format}]")).Click();
|
||||
Logs.Tester.LogInformation("Trying to click btn-generate");
|
||||
Driver.WaitForElement(By.Id("btn-generate")).ForceClick();
|
||||
AssertHappyMessage();
|
||||
@ -296,7 +299,7 @@ namespace BTCPayServer.Tests
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
}
|
||||
|
||||
public string CreateInvoice(string store, decimal amount = 100, string currency = "USD", string refundEmail = "")
|
||||
public string CreateInvoice(string storeName, decimal amount = 100, string currency = "USD", string refundEmail = "")
|
||||
{
|
||||
GoToInvoices();
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
@ -305,7 +308,7 @@ namespace BTCPayServer.Tests
|
||||
currencyEl.Clear();
|
||||
currencyEl.SendKeys(currency);
|
||||
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
|
||||
Driver.FindElement(By.Name("StoreId")).SendKeys(store + Keys.Enter);
|
||||
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName + Keys.Enter);
|
||||
Driver.FindElement(By.Id("Create")).ForceClick();
|
||||
Assert.True(Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
|
||||
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
|
||||
@ -316,7 +319,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task FundStoreWallet(WalletId walletId, int coins = 1, decimal denomination = 1m)
|
||||
{
|
||||
GoToWalletReceive(walletId);
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = Driver.FindElement(By.Id("vue-address")).GetProperty("value");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
@ -333,7 +336,7 @@ namespace BTCPayServer.Tests
|
||||
.GetAttribute("href");
|
||||
Assert.Contains($"{PayjoinClient.BIP21EndpointKey}", bip21);
|
||||
|
||||
GoToWalletSend(walletId);
|
||||
GoToWallet(walletId, WalletsNavPages.Send);
|
||||
Driver.FindElement(By.Id("bip21parse")).Click();
|
||||
Driver.SwitchTo().Alert().SendKeys(bip21);
|
||||
Driver.SwitchTo().Alert().Accept();
|
||||
@ -369,14 +372,13 @@ namespace BTCPayServer.Tests
|
||||
|
||||
}
|
||||
|
||||
public void GoToWalletSend(WalletId walletId)
|
||||
public void GoToWallet(WalletId walletId, WalletsNavPages navPages = WalletsNavPages.Send)
|
||||
{
|
||||
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/send"));
|
||||
}
|
||||
|
||||
internal void GoToWalletReceive(WalletId walletId)
|
||||
{
|
||||
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}/receive"));
|
||||
Driver.Navigate().GoToUrl(new Uri(Server.PayTester.ServerUri, $"wallets/{walletId}"));
|
||||
if (navPages != WalletsNavPages.Transactions)
|
||||
{
|
||||
Driver.FindElement(By.Id($"Wallet{navPages}")).Click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,9 @@ using NBitcoin.Payment;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Views.Wallets;
|
||||
using Newtonsoft.Json;
|
||||
using BTCPayServer.Client.Models;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -239,10 +242,26 @@ namespace BTCPayServer.Tests
|
||||
var storeUrl = s.Driver.Url;
|
||||
s.ClickOnAllSideMenus();
|
||||
s.GoToInvoices();
|
||||
s.CreateInvoice(store);
|
||||
var invoiceId = s.CreateInvoice(store);
|
||||
s.AssertHappyMessage();
|
||||
s.Driver.FindElement(By.ClassName("invoice-details-link")).Click();
|
||||
var invoiceUrl = s.Driver.Url;
|
||||
|
||||
//let's test archiving an invoice
|
||||
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
s.AssertHappyMessage();
|
||||
Assert.Contains("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
|
||||
//check that it no longer appears in list
|
||||
s.GoToInvoices();
|
||||
Assert.DoesNotContain(invoiceId, s.Driver.PageSource);
|
||||
//ok, let's unarchive and see that it shows again
|
||||
s.Driver.Navigate().GoToUrl(invoiceUrl);
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
s.AssertHappyMessage();
|
||||
Assert.DoesNotContain("Archived", s.Driver.FindElement(By.Id("btn-archive-toggle")).Text);
|
||||
s.GoToInvoices();
|
||||
Assert.Contains(invoiceId, s.Driver.PageSource);
|
||||
|
||||
// When logout we should not be able to access store and invoice details
|
||||
s.Driver.FindElement(By.Id("Logout")).Click();
|
||||
@ -361,11 +380,20 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("SelectedAppType")).SendKeys("PointOfSale" + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("SelectedStore")).SendKeys(store + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.Id("EnableShoppingCart")).Click();
|
||||
s.Driver.FindElement(By.Id("DefaultView")).SendKeys("Cart" + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).ForceClick();
|
||||
s.Driver.FindElement(By.Id("ViewApp")).ForceClick();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
var posBaseUrl = s.Driver.Url.Replace("/Cart", "");
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/cart";
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "Cart PoS not showing correct view");
|
||||
|
||||
s.Driver.Quit();
|
||||
}
|
||||
}
|
||||
@ -432,7 +460,7 @@ namespace BTCPayServer.Tests
|
||||
var storeId = s.CreateNewStore().storeId;
|
||||
s.GenerateWallet("BTC", "", false, true);
|
||||
var walletId = new WalletId(storeId, "BTC");
|
||||
s.GoToWalletReceive(walletId);
|
||||
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = s.Driver.FindElement(By.Id("vue-address")).GetProperty("value");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
||||
@ -456,7 +484,7 @@ namespace BTCPayServer.Tests
|
||||
coin => coin.OutPoint == spentOutpoint);
|
||||
});
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
s.GoToWalletSend(walletId);
|
||||
s.GoToWallet(walletId, WalletsNavPages.Send);
|
||||
s.Driver.FindElement(By.Id("advancedSettings")).Click();
|
||||
s.Driver.FindElement(By.Id("toggleInputSelection")).Click();
|
||||
s.Driver.WaitForElement(By.Id(spentOutpoint.ToString()));
|
||||
@ -540,7 +568,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
|
||||
|
||||
|
||||
var invoiceId = s.CreateInvoice(storeId.storeId);
|
||||
var invoiceId = s.CreateInvoice(storeId.storeName);
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
var address = invoice.EntityToDTO().Addresses["BTC"];
|
||||
|
||||
@ -553,7 +581,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//lets import and save private keys
|
||||
var root = mnemonic.DeriveExtKey();
|
||||
invoiceId = s.CreateInvoice(storeId.storeId);
|
||||
invoiceId = s.CreateInvoice(storeId.storeName);
|
||||
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
|
||||
address = invoice.EntityToDTO().Addresses["BTC"];
|
||||
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
|
||||
@ -638,6 +666,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
|
||||
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
|
||||
|
||||
|
||||
s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings);
|
||||
|
||||
s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click();
|
||||
s.AssertHappyMessage();
|
||||
Assert.Equal(mnemonic.ToString(), s.Driver.FindElements(By.ClassName("alert-success")).First().FindElement(By.TagName("code")).Text);
|
||||
}
|
||||
}
|
||||
void SetTransactionOutput(SeleniumTester s, int index, BitcoinAddress dest, decimal amount, bool subtract = false)
|
||||
|
@ -56,6 +56,7 @@ namespace BTCPayServer.Tests
|
||||
// TODO: The fact that we use same conn string as development database can cause huge problems with tests
|
||||
// since in dev we already can have some users / stores registered, while on CI database is being initalized
|
||||
// for the first time and first registered user gets admin status by default
|
||||
SocksHTTPProxy = GetEnvironment("TESTS_SOCKSHTTP", "http://127.0.0.1:8118/"),
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"),
|
||||
MySQL = GetEnvironment("TESTS_MYSQL", "User ID=root;Host=127.0.0.1;Port=33036;Database=btcpayserver")
|
||||
};
|
||||
@ -96,7 +97,7 @@ namespace BTCPayServer.Tests
|
||||
CustomerLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_CUSTOMERLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30992/"), btc);
|
||||
MerchantLightningD = LightningClientFactory.CreateClient(GetEnvironment("TEST_MERCHANTLIGHTNINGD", "type=clightning;server=tcp://127.0.0.1:30993/"), 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);
|
||||
MerchantLnd = new LndMockTester(this, "TEST_MERCHANTLND", "https://lnd:lnd@127.0.0.1:35531/", "merchant_lnd", btc);
|
||||
PayTester.UseLightning = true;
|
||||
PayTester.IntegratedLightning = MerchantCharge.Client.Uri;
|
||||
}
|
||||
@ -118,6 +119,7 @@ namespace BTCPayServer.Tests
|
||||
public async Task EnsureChannelsSetup()
|
||||
{
|
||||
Logs.Tester.LogInformation("Connecting channels");
|
||||
BTCPayServer.Lightning.Tests.ConnectChannels.Logs = Logs.LogProvider.CreateLogger("Connect channels");
|
||||
await BTCPayServer.Lightning.Tests.ConnectChannels.ConnectAll(ExplorerNode, GetLightningSenderClients(), GetLightningDestClients()).ConfigureAwait(false);
|
||||
Logs.Tester.LogInformation("Channels connected");
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NBXplorer.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Stores;
|
||||
@ -70,10 +71,22 @@ namespace BTCPayServer.Tests
|
||||
var x = Assert.IsType<RedirectToActionResult>(await manageController.AddApiKey(
|
||||
new ManageController.AddApiKeyViewModel()
|
||||
{
|
||||
PermissionValues = permissions.Select(s => new ManageController.AddApiKeyViewModel.PermissionValueItem()
|
||||
PermissionValues = permissions.Select(s =>
|
||||
{
|
||||
Permission = s,
|
||||
Value = true
|
||||
Permission.TryParse(s, out var p);
|
||||
return p;
|
||||
}).GroupBy(permission => permission.Policy).Select(p =>
|
||||
{
|
||||
var stores = p.Where(permission => !string.IsNullOrEmpty(permission.Scope))
|
||||
.Select(permission => permission.Scope).ToList();
|
||||
return new ManageController.AddApiKeyViewModel.PermissionValueItem()
|
||||
{
|
||||
Permission = p.Key,
|
||||
Forbidden = false,
|
||||
StoreMode = stores.Any()? ManageController.AddApiKeyViewModel.ApiKeyStoreMode.Specific: ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores,
|
||||
SpecificStores = stores,
|
||||
Value = true
|
||||
};
|
||||
}).ToList()
|
||||
}));
|
||||
var statusMessage = manageController.TempData.GetStatusMessageModel();
|
||||
@ -124,6 +137,13 @@ namespace BTCPayServer.Tests
|
||||
modify(store);
|
||||
storeController.UpdateStore(store).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task ModifyStoreAsync(Action<StoreViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<StoresController>();
|
||||
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
|
||||
modify(store);
|
||||
return storeController.UpdateStore(store);
|
||||
}
|
||||
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
@ -133,6 +153,10 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task CreateStoreAsync()
|
||||
{
|
||||
if (UserId is null)
|
||||
{
|
||||
await RegisterAsync();
|
||||
}
|
||||
var store = this.GetController<UserStoresController>();
|
||||
await store.CreateStore(new CreateStoreViewModel() {Name = "Test Store"});
|
||||
StoreId = store.CreatedStoreId;
|
||||
@ -149,6 +173,8 @@ namespace BTCPayServer.Tests
|
||||
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, ScriptPubKeyType segwit = ScriptPubKeyType.Legacy,
|
||||
bool importKeysToNBX = false)
|
||||
{
|
||||
if (StoreId is null)
|
||||
await CreateStoreAsync();
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
|
||||
@ -174,18 +200,9 @@ namespace BTCPayServer.Tests
|
||||
return new WalletId(StoreId, cryptoCode);
|
||||
}
|
||||
|
||||
public async Task EnablePayJoin()
|
||||
public Task EnablePayJoin()
|
||||
{
|
||||
var storeController = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
var storeVM =
|
||||
Assert.IsType<StoreViewModel>(Assert
|
||||
.IsType<ViewResult>(storeController.UpdateStore()).Model);
|
||||
|
||||
storeVM.PayJoinEnabled = true;
|
||||
|
||||
Assert.Equal(nameof(storeController.UpdateStore),
|
||||
Assert.IsType<RedirectToActionResult>(
|
||||
await storeController.UpdateStore(storeVM)).ActionName);
|
||||
return ModifyStoreAsync(s => s.PayJoinEnabled = true);
|
||||
}
|
||||
|
||||
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
|
||||
@ -235,23 +252,39 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public bool IsAdmin { get; internal set; }
|
||||
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType)
|
||||
public void RegisterLightningNode(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
{
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType).GetAwaiter().GetResult();
|
||||
RegisterLightningNodeAsync(cryptoCode, connectionType, isMerchant).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType)
|
||||
public async Task RegisterLightningNodeAsync(string cryptoCode, LightningConnectionType connectionType, bool isMerchant = true)
|
||||
{
|
||||
var storeController = this.GetController<StoresController>();
|
||||
|
||||
string connectionString = null;
|
||||
if (connectionType == LightningConnectionType.Charge)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=charge;server=" + parent.MerchantCharge.Client.Uri.AbsoluteUri;
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (connectionType == LightningConnectionType.CLightning)
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.MerchantLightningD).Address.AbsoluteUri;
|
||||
else
|
||||
connectionString = "type=clightning;server=" +
|
||||
((CLightningClient)parent.CustomerLightningD).Address.AbsoluteUri;
|
||||
}
|
||||
else if (connectionType == LightningConnectionType.LndREST)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
{
|
||||
if (isMerchant)
|
||||
connectionString = $"type=lnd-rest;server={parent.MerchantLnd.Swagger.BaseUrl};allowinsecure=true";
|
||||
else
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(connectionType.ToString());
|
||||
|
||||
@ -314,7 +347,7 @@ namespace BTCPayServer.Tests
|
||||
var endpoint = GetPayjoinEndpoint(invoice, psbt.Network);
|
||||
if (endpoint == null)
|
||||
{
|
||||
return null;
|
||||
throw new InvalidOperationException("No payjoin endpoint for the invoice");
|
||||
}
|
||||
var pjClient = parent.PayTester.GetService<PayjoinClient>();
|
||||
var storeRepository = parent.PayTester.GetService<StoreRepository>();
|
||||
@ -338,7 +371,10 @@ namespace BTCPayServer.Tests
|
||||
else
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<PayjoinReceiverException>(async () => await pjClient.RequestPayjoin(endpoint, settings, psbt, default));
|
||||
Assert.Equal(expectedError, ex.ErrorCode);
|
||||
var split = expectedError.Split('|');
|
||||
Assert.Equal(split[0], ex.ErrorCode);
|
||||
if (split.Length > 1)
|
||||
Assert.Contains(split[1], ex.ReceiverMessage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -363,9 +399,13 @@ namespace BTCPayServer.Tests
|
||||
new StringContent(content, Encoding.UTF8, "text/plain"));
|
||||
if (expectedError != null)
|
||||
{
|
||||
var split = expectedError.Split('|');
|
||||
Assert.False(response.IsSuccessStatusCode);
|
||||
var error = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
Assert.Equal(expectedError, error["errorCode"].Value<string>());
|
||||
if (split.Length > 0)
|
||||
Assert.Equal(split[0], error["errorCode"].Value<string>());
|
||||
if (split.Length > 1)
|
||||
Assert.Contains(split[1], error["message"].Value<string>());
|
||||
return null;
|
||||
}
|
||||
else
|
||||
@ -381,7 +421,7 @@ namespace BTCPayServer.Tests
|
||||
return response;
|
||||
}
|
||||
|
||||
private static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
|
||||
public static Uri GetPayjoinEndpoint(Invoice invoice, Network network)
|
||||
{
|
||||
var parsedBip21 = new BitcoinUrlBuilder(
|
||||
invoice.CryptoInfo.First(c => c.CryptoCode == network.NetworkSet.CryptoCode).PaymentUrls.BIP21,
|
||||
|
@ -63,6 +63,8 @@ using BTCPayServer.Security.Bitpay;
|
||||
using MemoryCache = Microsoft.Extensions.Caching.Memory.MemoryCache;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using TwentyTwenty.Storage;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -101,9 +103,13 @@ namespace BTCPayServer.Tests
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var sresp = await tester.PayTester.HttpClient.GetAsync("swagger/v1/swagger.json");
|
||||
var acc = tester.NewAccount();
|
||||
|
||||
JObject swagger = JObject.Parse(await sresp.Content.ReadAsStringAsync());
|
||||
var sresp = Assert
|
||||
.IsType<JsonResult>(await tester.PayTester.GetController<HomeController>(acc.UserId, acc.StoreId)
|
||||
.Swagger()).Value.ToJson();
|
||||
|
||||
JObject swagger = JObject.Parse(sresp);
|
||||
using HttpClient client = new HttpClient();
|
||||
var resp = await client.GetAsync(
|
||||
"https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json");
|
||||
@ -111,12 +117,13 @@ namespace BTCPayServer.Tests
|
||||
IList<ValidationError> errors;
|
||||
bool valid = swagger.IsValid(schema, out errors);
|
||||
//the schema is not fully compliant to the spec. We ARE allowed to have multiple security schemas.
|
||||
if (!valid && errors.Count == 1 && errors.Any(error =>
|
||||
error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf))
|
||||
var matchedError = errors.Where(error =>
|
||||
error.Path == "components.securitySchemes.Basic" && error.ErrorType == ErrorType.OneOf).ToList();
|
||||
foreach (ValidationError validationError in matchedError)
|
||||
{
|
||||
errors = new List<ValidationError>();
|
||||
valid = true;
|
||||
errors.Remove(validationError);
|
||||
}
|
||||
valid = !errors.Any();
|
||||
|
||||
Assert.Empty(errors);
|
||||
Assert.True(valid);
|
||||
@ -158,9 +165,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(HttpStatusCode.OK, (await httpClient.SendAsync(request)).StatusCode);
|
||||
Logs.Tester.LogInformation($"OK: {url} ({file})");
|
||||
}
|
||||
catch (EqualException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Tester.LogInformation($"FAILED: {url} ({file}) {ex.Actual}");
|
||||
var details = ex is EqualException ? (ex as EqualException).Actual : ex.Message;
|
||||
Logs.Tester.LogInformation($"FAILED: {url} ({file}) {details}");
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -604,6 +613,12 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
Assert.Equal("Object not found", ex.Errors.First());
|
||||
}
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "/invoices/Cy9jfK82eeEED1T3qhwF3Y");
|
||||
req.Headers.TryAddWithoutValidation("Authorization", "Basic dGVzdA==");
|
||||
req.Content = new StringContent("{}", Encoding.UTF8, "application/json");
|
||||
var result = await tester.PayTester.HttpClient.SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
|
||||
Assert.Equal(0, result.Content.Headers.ContentLength.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -618,7 +633,7 @@ namespace BTCPayServer.Tests
|
||||
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
|
||||
})
|
||||
{
|
||||
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
var actual = CurrencyNameTable.Instance.DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
@ -728,6 +743,91 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = 60 * 2 * 1000)]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUseLightningAPI()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.ActivateLightning();
|
||||
await tester.StartAsync();
|
||||
await tester.EnsureChannelsSetup();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning, false);
|
||||
|
||||
var merchant = tester.NewAccount();
|
||||
merchant.GrantAccess(true);
|
||||
merchant.RegisterLightningNode("BTC", LightningConnectionType.LndREST);
|
||||
var merchantClient = await merchant.CreateClient($"btcpay.store.canuselightningnode:{merchant.StoreId}");
|
||||
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60)));
|
||||
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var client = await user.CreateClient("btcpay.server.canuseinternallightningnode");
|
||||
var err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||
Assert.Contains("503", err.Message);
|
||||
// Not permission for the store!
|
||||
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels(user.StoreId, "BTC"));
|
||||
Assert.Contains("403", err.Message);
|
||||
var invoiceData = await client.CreateLightningInvoice("BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
Expiry = TimeSpan.FromSeconds(400),
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
var chargeInvoice = invoiceData;
|
||||
Assert.NotNull(await client.GetLightningInvoice("BTC", invoiceData.Id));
|
||||
|
||||
client = await user.CreateClient($"btcpay.store.canuselightningnode:{user.StoreId}");
|
||||
// Not permission for the server
|
||||
err = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetLightningNodeChannels("BTC"));
|
||||
Assert.Contains("403", err.Message);
|
||||
|
||||
var data = await client.GetLightningNodeChannels(user.StoreId, "BTC");
|
||||
Assert.Equal(2, data.Count());
|
||||
BitcoinAddress.Create(await client.GetLightningDepositAddress(user.StoreId, "BTC"), Network.RegTest);
|
||||
|
||||
invoiceData = await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = LightMoney.Satoshis(1000),
|
||||
Description = "lol",
|
||||
Expiry = TimeSpan.FromSeconds(400),
|
||||
PrivateRouteHints = false
|
||||
});
|
||||
|
||||
Assert.NotNull(await client.GetLightningInvoice(user.StoreId, "BTC", invoiceData.Id));
|
||||
|
||||
await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = merchantInvoice.BOLT11
|
||||
});
|
||||
await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.PayLightningInvoice(user.StoreId, "BTC", new PayLightningInvoiceRequest()
|
||||
{
|
||||
BOLT11 = "lol"
|
||||
}));
|
||||
|
||||
var validationErr = await Assert.ThrowsAsync<GreenFieldValidationException>(async () => await client.CreateLightningInvoice(user.StoreId, "BTC", new CreateLightningInvoiceRequest()
|
||||
{
|
||||
Amount = -1,
|
||||
Expiry = TimeSpan.FromSeconds(-1),
|
||||
Description = null
|
||||
}));
|
||||
Assert.Equal(2, validationErr.ValidationErrors.Length);
|
||||
|
||||
var invoice = await merchantClient.GetLightningInvoice(merchant.StoreId, "BTC", merchantInvoice.Id);
|
||||
Assert.NotNull(invoice.PaidAt);
|
||||
Assert.Equal(LightMoney.Satoshis(1000), invoice.Amount);
|
||||
// Amount received might be bigger because of internal implementation shit from lightning
|
||||
Assert.True(LightMoney.Satoshis(1000) <= invoice.AmountReceived);
|
||||
|
||||
var info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
}
|
||||
}
|
||||
|
||||
async Task CanSendLightningPaymentCore(ServerTester tester, TestAccount user)
|
||||
{
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice()
|
||||
@ -889,12 +989,16 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTorClient()
|
||||
[Theory(Timeout = TestTimeout)]
|
||||
[Xunit.InlineData(true)]
|
||||
[Xunit.InlineData(false)]
|
||||
public async Task CanUseTorClient(bool useInternalTorSocksProxy)
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
if (useInternalTorSocksProxy)
|
||||
tester.PayTester.SocksHTTPProxy = null;
|
||||
await tester.StartAsync();
|
||||
var proxy = tester.PayTester.GetService<Socks5HttpProxyServer>();
|
||||
void AssertConnectionDropped()
|
||||
@ -1895,6 +1999,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void HasCurrencyDataForNetworks()
|
||||
{
|
||||
var btcPayNetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
foreach (var network in btcPayNetworkProvider.GetAll())
|
||||
{
|
||||
var cd = CurrencyNameTable.Instance.GetCurrencyData(network.CryptoCode, false);
|
||||
Assert.NotNull(cd);
|
||||
Assert.Equal(network.Divisibility, cd.Divisibility);
|
||||
Assert.True(cd.Crypto);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
@ -2107,7 +2225,7 @@ namespace BTCPayServer.Tests
|
||||
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
string content =
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
|
||||
derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet.json", content);
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
|
||||
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
Assert.False(derivationVM
|
||||
@ -2118,7 +2236,7 @@ namespace BTCPayServer.Tests
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
|
||||
derivationVM = (DerivationSchemeViewModel)Assert
|
||||
.IsType<ViewResult>(await controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
|
||||
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet2.json", content);
|
||||
derivationVM.ElectrumWalletFile = TestUtils.GetFormFile("wallet2.json", content);
|
||||
derivationVM.Enabled = true;
|
||||
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller
|
||||
.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
|
||||
@ -2130,7 +2248,7 @@ namespace BTCPayServer.Tests
|
||||
var store = tester.PayTester.StoreRepository.FindStore(user.StoreId).GetAwaiter().GetResult();
|
||||
var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks)
|
||||
.OfType<DerivationSchemeSettings>().First(o => o.PaymentId.IsBTCOnChain);
|
||||
DerivationSchemeSettings.TryParseFromColdcard(content, onchainBTC.Network, out var expected);
|
||||
DerivationSchemeSettings.TryParseFromElectrumWallet(content, onchainBTC.Network, out var expected);
|
||||
Assert.Equal(expected.ToJson(), onchainBTC.ToJson());
|
||||
|
||||
// Let's check that the root hdkey and account key path are taken into account when making a PSBT
|
||||
@ -2256,14 +2374,17 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
public async Task CanUsePoSApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.ActivateLTC();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
@ -2297,7 +2418,7 @@ donation:
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
var vmview =
|
||||
Assert.IsType<ViewPointOfSaleViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
|
||||
Assert.Equal("hello", vmview.Title);
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
@ -2309,7 +2430,7 @@ donation:
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
|
||||
//
|
||||
var invoices = user.BitPay.GetInvoices();
|
||||
@ -2320,7 +2441,7 @@ donation:
|
||||
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
@ -2330,7 +2451,7 @@ donation:
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
@ -2346,7 +2467,7 @@ donation:
|
||||
ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true),
|
||||
(Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0,
|
||||
ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false),
|
||||
(Code: "BTC", ExpectedSymbol: "BTC", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8,
|
||||
(Code: "BTC", ExpectedSymbol: "₿", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8,
|
||||
ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true),
|
||||
})
|
||||
{
|
||||
@ -2371,7 +2492,7 @@ donation:
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
|
||||
publicApps = user.GetController<AppsPublicController>();
|
||||
vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId).Result).Model);
|
||||
.IsType<ViewResult>(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model);
|
||||
Assert.Equal(test.Code, vmview.CurrencyCode);
|
||||
Assert.Equal(test.ExpectedSymbol,
|
||||
vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well);
|
||||
@ -2402,17 +2523,17 @@ noninventoryitem:
|
||||
|
||||
//inventoryitem has 1 item available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
|
||||
//inventoryitem has unlimited items available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
|
||||
//verify invoices where created
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
@ -2434,6 +2555,41 @@ noninventoryitem:
|
||||
Assert.Equal(1,
|
||||
appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
|
||||
}, 10000);
|
||||
|
||||
|
||||
//test payment methods option
|
||||
|
||||
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
|
||||
vmpos.Title = "hello";
|
||||
vmpos.Currency = "BTC";
|
||||
vmpos.Template = @"
|
||||
btconly:
|
||||
price: 1.0
|
||||
title: good apple
|
||||
payment_methods:
|
||||
- BTC
|
||||
normal:
|
||||
price: 1.0";
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
Assert.Single(btcOnlyInvoice.CryptoInfo);
|
||||
Assert.Equal("BTC",
|
||||
btcOnlyInvoice.CryptoInfo.First().CryptoCode);
|
||||
Assert.Equal(PaymentTypes.BTCLike.ToString(),
|
||||
btcOnlyInvoice.CryptoInfo.First().PaymentType);
|
||||
|
||||
Assert.Equal(2, normalInvoice.CryptoInfo.Length);
|
||||
Assert.Contains(
|
||||
normalInvoice.CryptoInfo,
|
||||
s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] {"BTC", "LTC"}.Contains(
|
||||
s.CryptoCode));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3396,7 +3552,7 @@ noninventoryitem:
|
||||
var root = new Mnemonic(
|
||||
"usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage")
|
||||
.DeriveExtKey();
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
mainnet, out var settings));
|
||||
Assert.Equal(root.GetPublicKey().GetHDFingerPrint(), settings.AccountKeySettings[0].RootFingerprint);
|
||||
@ -3413,20 +3569,20 @@ noninventoryitem:
|
||||
var testnet = new BTCPayNetworkProvider(NetworkType.Testnet).GetNetwork<BTCPayNetwork>("BTC");
|
||||
|
||||
// Should be legacy
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"tpubDDWYqT3P24znfsaGX7kZcQhNc5LAjnQiKQvUCHF2jS6dsgJBRtymopEU5uGpMaR5YChjuiExZG1X2aTbqXkp82KqH5qnqwWHp6EWis9ZvKr\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/44'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s && !s.Segwit);
|
||||
|
||||
// Should be segwit p2sh
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DSddA9NoRUyJrQ4p86nsCiTSY7kLHrSxx3joEJXjHd4HPARhdXUATuk585FdWPVC2GdjsMePHb6BMDmf7c6KG4K4RPX6LVqBLtDcWpQJmh\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is P2SHDerivationStrategy p &&
|
||||
p.Inner is DirectDerivationStrategy s2 && s2.Segwit);
|
||||
|
||||
// Should be segwit
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromColdcard(
|
||||
Assert.True(DerivationSchemeSettings.TryParseFromElectrumWallet(
|
||||
"{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}",
|
||||
testnet, out settings));
|
||||
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
|
||||
@ -3541,6 +3697,34 @@ noninventoryitem:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CheckOnionlocationForNonOnionHtmlRequests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var url = tester.PayTester.ServerUri.AbsoluteUri;
|
||||
|
||||
// check onion location is present for HTML page request
|
||||
using var htmlRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||
htmlRequest.Headers.TryAddWithoutValidation("Accept", "text/html,*/*");
|
||||
|
||||
var htmlResponse = await tester.PayTester.HttpClient.SendAsync(htmlRequest);
|
||||
htmlResponse.EnsureSuccessStatusCode();
|
||||
Assert.True(htmlResponse.Headers.TryGetValues("Onion-Location", out var onionLocation));
|
||||
Assert.StartsWith("http://wsaxew3qa5ljfuenfebmaf3m5ykgatct3p6zjrqwoouj3foererde3id.onion", onionLocation.FirstOrDefault() ?? "no-onion-location-header");
|
||||
|
||||
// no onion location for other mime types
|
||||
using var otherRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(url));
|
||||
otherRequest.Headers.TryAddWithoutValidation("Accept", "*/*");
|
||||
|
||||
var otherResponse = await tester.PayTester.HttpClient.SendAsync(otherRequest);
|
||||
otherResponse.EnsureSuccessStatusCode();
|
||||
Assert.False(otherResponse.Headers.Contains("Onion-Location"));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();
|
||||
|
@ -12,6 +12,7 @@ services:
|
||||
environment:
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_SOCKSHTTP: http://tor:8118/
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_DB: "Postgres"
|
||||
@ -78,7 +79,7 @@ services:
|
||||
- customer_lnd
|
||||
- merchant_lnd
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.1.24
|
||||
image: nicolasdorier/nbxplorer:2.1.30
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -127,6 +128,7 @@ services:
|
||||
deprecatedrpc=signrawtransaction
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
@ -136,7 +138,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v0.8.0-dev
|
||||
image: btcpayserver/lightning:v0.8.2-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -163,7 +165,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
lightning-charged:
|
||||
image: shesek/lightning-charge:0.4.11-standalone
|
||||
image: shesek/lightning-charge:0.4.19-standalone
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NETWORK: regtest
|
||||
@ -183,7 +185,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v0.8.0-dev
|
||||
image: btcpayserver/lightning:v0.8.2-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -228,7 +230,7 @@ services:
|
||||
elementsd-liquid:
|
||||
restart: always
|
||||
container_name: btcpayserver_elementsd_liquid
|
||||
image: btcpayserver/elements:0.18.1.1-1
|
||||
image: btcpayserver/elements:0.18.1.7
|
||||
environment:
|
||||
ELEMENTS_CHAIN: elementsregtest
|
||||
ELEMENTS_EXTRA_ARGS: |
|
||||
@ -261,7 +263,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.8.2-beta
|
||||
image: btcpayserver/lnd:v0.10.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -281,7 +283,7 @@ services:
|
||||
debuglevel=debug
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53280:8080"
|
||||
- "35531:8080"
|
||||
expose:
|
||||
- "9735"
|
||||
volumes:
|
||||
@ -291,7 +293,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.8.2-beta
|
||||
image: btcpayserver/lnd:v0.10.1-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -311,7 +313,7 @@ services:
|
||||
debuglevel=debug
|
||||
trickledelay=1000
|
||||
ports:
|
||||
- "53281:8080"
|
||||
- "35532:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
- "10009"
|
||||
@ -327,9 +329,13 @@ services:
|
||||
container_name: tor
|
||||
environment:
|
||||
TOR_PASSWORD: btcpayserver
|
||||
TOR_EXTRA_ARGS: |
|
||||
CookieAuthentication 1
|
||||
HTTPTunnelPort 0.0.0.0:8118
|
||||
ports:
|
||||
- "9050:9050" # SOCKS
|
||||
- "9051:9051" # Tor Control
|
||||
- "8118:8118" # HTTP Proxy
|
||||
volumes:
|
||||
- "tor_datadir:/home/tor/.tor"
|
||||
- "torrcdir:/usr/local/etc/tor"
|
||||
|
@ -1,2 +1,2 @@
|
||||
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli $args
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc $args
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli "$@"
|
||||
docker exec -ti $customer_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc "$@"
|
||||
|
1
BTCPayServer.Tests/docker-elements.ps1
Normal file
1
BTCPayServer.Tests/docker-elements.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_elementsd_liquid elements-cli -datadir="/data" $args
|
@ -1,2 +1,2 @@
|
||||
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli $args
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc $args
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"
|
||||
docker exec -ti $merchant_lightning_container_id lightning-cli --rpc-file=/root/.lightning/lightning-rpc "$@"
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"parallelizeTestCollections": false,
|
||||
"longRunningTestSeconds": 60,
|
||||
"diagnosticMessages": true
|
||||
"diagnosticMessages": true,
|
||||
"methodDisplay": "method"
|
||||
}
|
||||
|
@ -27,21 +27,20 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="bundleconfig.json" />
|
||||
<EmbeddedResource Include="Currencies.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.9" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.0" />
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />
|
||||
<PackageReference Include="BundlerMinifier.TagHelpers" Version="3.2.435" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="4.0.217" />
|
||||
<PackageReference Include="LedgerWallet" Version="2.0.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.9.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IO.Pipelines" Version="4.7.2" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.38" />
|
||||
<PackageReference Include="DBriize" Version="1.0.1.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
@ -202,9 +201,6 @@
|
||||
<Content Update="Views\Wallets\WalletSendVault.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletSendLedger.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\Wallets\WalletTransactions.cshtml">
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
</Content>
|
||||
@ -223,5 +219,5 @@
|
||||
<_ContentIncludedByDefault Remove="Views\Authorization\Authorize.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1serverinfo_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" wwwroot_4swagger_4v1_4swagger_1template_1stores_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" /></VisualStudio></ProjectExtensions>
|
||||
</Project>
|
||||
|
@ -43,7 +43,7 @@ namespace BTCPayServer.Configuration
|
||||
private set;
|
||||
}
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
public Uri SocksHttpProxy { get; set; }
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
@ -171,7 +171,11 @@ namespace BTCPayServer.Configuration
|
||||
throw new ConfigException("Invalid value for socksendpoint");
|
||||
SocksEndpoint = endpoint;
|
||||
}
|
||||
|
||||
var socksuri = conf.GetOrDefault<Uri>("sockshttpproxy", null);
|
||||
if (socksuri != null)
|
||||
{
|
||||
SocksHttpProxy = socksuri;
|
||||
}
|
||||
|
||||
var sshSettings = ParseSSHConfiguration(conf);
|
||||
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
|
||||
|
@ -44,6 +44,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--torrcfile", "Path to torrc file containing hidden services directories (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--socksendpoint", "Socks endpoint to connect to onion urls (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--sockshttpproxy", "Socks v5 HTTP proxy, this is currently used only for sending payjoin to tor endpoints. You can get a socks http proxy with the HTTPTunnelPort setting on Tor 0.3.2.x. (default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
|
@ -4,12 +4,9 @@ using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -55,7 +52,7 @@ namespace BTCPayServer.Controllers
|
||||
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
|
||||
" inventory: 5\n" +
|
||||
" custom: true";
|
||||
EnableShoppingCart = false;
|
||||
DefaultView = PosViewType.Static;
|
||||
ShowCustomAmount = true;
|
||||
ShowDiscount = true;
|
||||
EnableTips = true;
|
||||
@ -64,6 +61,7 @@ namespace BTCPayServer.Controllers
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool EnableShoppingCart { get; set; }
|
||||
public PosViewType DefaultView { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public bool ShowDiscount { get; set; }
|
||||
public bool EnableTips { get; set; }
|
||||
@ -95,13 +93,15 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
settings.EnableShoppingCart = false;
|
||||
|
||||
var vm = new UpdatePointOfSaleViewModel()
|
||||
{
|
||||
Id = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Title = settings.Title,
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
DefaultView = settings.DefaultView,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
@ -179,7 +179,7 @@ namespace BTCPayServer.Controllers
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
EnableShoppingCart = vm.EnableShoppingCart,
|
||||
DefaultView = vm.DefaultView,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
ShowDiscount = vm.ShowDiscount,
|
||||
EnableTips = vm.EnableTips,
|
||||
@ -194,7 +194,6 @@ namespace BTCPayServer.Controllers
|
||||
Description = vm.Description,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
|
||||
|
||||
});
|
||||
await _AppService.UpdateOrCreateApp(app);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
|
@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -40,23 +41,23 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId)
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
|
||||
{
|
||||
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
|
||||
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
|
||||
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
|
||||
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
EnableShoppingCart = settings.EnableShoppingCart,
|
||||
ViewType = (PosViewType)viewType,
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
ShowDiscount = settings.ShowDiscount,
|
||||
EnableTips = settings.EnableTips,
|
||||
@ -84,11 +85,12 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
[Route("/apps/{appId}/pos/{viewType?}")]
|
||||
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
PosViewType viewType,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal amount,
|
||||
string email,
|
||||
string orderId,
|
||||
@ -105,12 +107,14 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && !settings.EnableShoppingCart)
|
||||
settings.DefaultView = settings.EnableShoppingCart? PosViewType.Cart : settings.DefaultView;
|
||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
|
||||
}
|
||||
string title = null;
|
||||
var price = 0.0m;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
@ -130,17 +134,23 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
}
|
||||
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!settings.ShowCustomAmount && !settings.EnableShoppingCart)
|
||||
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
|
||||
return NotFound();
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
|
||||
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
if (!string.IsNullOrEmpty(posData) &&
|
||||
settings.EnableShoppingCart &&
|
||||
if (!string.IsNullOrEmpty(posData) &&
|
||||
settings.DefaultView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(posData, out var cartItems))
|
||||
{
|
||||
|
||||
@ -182,6 +192,7 @@ namespace BTCPayServer.Controllers
|
||||
ExtendedNotifications = true,
|
||||
PosData = string.IsNullOrEmpty(posData) ? null : posData,
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string>() { AppService.GetAppInternalTag(appId) },
|
||||
cancellationToken);
|
||||
@ -270,6 +281,7 @@ namespace BTCPayServer.Controllers
|
||||
var store = await _AppService.GetStore(app);
|
||||
var title = settings.Title;
|
||||
var price = request.Amount;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
if (!string.IsNullOrEmpty(request.ChoiceKey))
|
||||
{
|
||||
@ -290,6 +302,13 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound("Option was out of stock");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (choice?.PaymentMethods?.Any() is true)
|
||||
{
|
||||
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
|
||||
s => new InvoiceSupportedTransactionCurrency() {Enabled = true});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
|
||||
@ -311,6 +330,7 @@ namespace BTCPayServer.Controllers
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RedirectURL = request.RedirectUrl ??
|
||||
new Uri(new Uri( new Uri(HttpContext.Request.GetAbsoluteRoot()), _BtcPayServerOptions.RootPath), $"apps/{appId}/crowdfund").ToString()
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
|
@ -12,16 +12,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public IActionResult Handle(int? statusCode = null)
|
||||
{
|
||||
if (statusCode.HasValue)
|
||||
if (Request.Headers.TryGetValue("Accept", out var v) && v.Any(v => v.Contains("text/html", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var specialPages = new[] { 404, 429, 500 };
|
||||
if (specialPages.Any(a => a == statusCode.Value))
|
||||
if (statusCode.HasValue)
|
||||
{
|
||||
var viewName = statusCode.ToString();
|
||||
return View(viewName);
|
||||
var specialPages = new[] { 404, 429, 500 };
|
||||
if (specialPages.Any(a => a == statusCode.Value))
|
||||
{
|
||||
var viewName = statusCode.ToString();
|
||||
return View(viewName);
|
||||
}
|
||||
}
|
||||
return View(statusCode);
|
||||
}
|
||||
return View(statusCode);
|
||||
return this.StatusCode(statusCode.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public async Task<ActionResult<ApiKeyData>> CreateKey(CreateApiKeyRequest request)
|
||||
{
|
||||
if (request is null)
|
||||
return BadRequest();
|
||||
return NotFound();
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
|
||||
@ -74,7 +74,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
public async Task<IActionResult> RevokeKey(string apikey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(apikey))
|
||||
return BadRequest();
|
||||
return NotFound();
|
||||
if (await _apiKeyRepository.Remove(apikey, _userManager.GetUserId(User)))
|
||||
return Ok();
|
||||
else
|
||||
|
29
BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs
Normal file
29
BTCPayServer/Controllers/GreenField/GreenFieldUtils.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
public static class GreenFieldUtils
|
||||
{
|
||||
public static IActionResult CreateValidationError(this ControllerBase controller, ModelStateDictionary modelState)
|
||||
{
|
||||
List<GreenfieldValidationError> errors = new List<GreenfieldValidationError>();
|
||||
foreach (var error in modelState)
|
||||
{
|
||||
foreach (var errorMessage in error.Value.Errors)
|
||||
{
|
||||
errors.Add(new GreenfieldValidationError(error.Key, errorMessage.ErrorMessage));
|
||||
}
|
||||
}
|
||||
return controller.UnprocessableEntity(errors.ToArray());
|
||||
}
|
||||
public static IActionResult CreateAPIError(this ControllerBase controller, string errorCode, string errorMessage)
|
||||
{
|
||||
return controller.BadRequest(new GreenfieldAPIError(errorCode, errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
22
BTCPayServer/Controllers/GreenField/HealthController.cs
Normal file
22
BTCPayServer/Controllers/GreenField/HealthController.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[Controller]
|
||||
public class HealthController : ControllerBase
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpGet("~/api/v1/health")]
|
||||
public ActionResult GetHealth(NBXplorerDashboard dashBoard)
|
||||
{
|
||||
ApiHealthData model = new ApiHealthData()
|
||||
{
|
||||
Synchronized = dashBoard.IsFullySynched()
|
||||
};
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[LightningUnavailableExceptionFilter]
|
||||
public class InternalLightningNodeApiController : LightningNodeApiController
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
|
||||
|
||||
public InternalLightningNodeApiController(BTCPayServerOptions btcPayServerOptions,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider, BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
CssThemeManager cssThemeManager, LightningClientFactoryService lightningClientFactory) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
|
||||
{
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/info")]
|
||||
public override Task<IActionResult> GetInfo(string cryptoCode)
|
||||
{
|
||||
return base.GetInfo(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/connect")]
|
||||
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
|
||||
{
|
||||
return base.ConnectToNode(cryptoCode, request);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/channels")]
|
||||
public override Task<IActionResult> GetChannels(string cryptoCode)
|
||||
{
|
||||
return base.GetChannels(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/channels")]
|
||||
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
|
||||
{
|
||||
return base.OpenChannel(cryptoCode, request);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/address")]
|
||||
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
{
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/lightning/{cryptoCode}/invoices/{id}")]
|
||||
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
{
|
||||
return base.GetInvoice(cryptoCode, id);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseInternalLightningNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices/pay")]
|
||||
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
|
||||
{
|
||||
return base.PayInvoice(cryptoCode, lightningInvoice);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateLightningInvoiceInternalNode,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/server/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
|
||||
{
|
||||
return base.CreateInvoice(cryptoCode, request);
|
||||
}
|
||||
|
||||
protected override Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings)
|
||||
{
|
||||
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
|
||||
out var internalLightningNode);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (network == null || !CanUseInternalLightning(doingAdminThings) || internalLightningNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Task.FromResult(_lightningClientFactory.Create(internalLightningNode, network));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[LightningUnavailableExceptionFilter]
|
||||
public class StoreLightningNodeApiController : LightningNodeApiController
|
||||
{
|
||||
private readonly BTCPayServerOptions _btcPayServerOptions;
|
||||
private readonly LightningClientFactoryService _lightningClientFactory;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
|
||||
public StoreLightningNodeApiController(
|
||||
BTCPayServerOptions btcPayServerOptions,
|
||||
LightningClientFactoryService lightningClientFactory, BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager) : base(
|
||||
btcPayNetworkProvider, btcPayServerEnvironment, cssThemeManager)
|
||||
{
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_lightningClientFactory = lightningClientFactory;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/info")]
|
||||
public override Task<IActionResult> GetInfo(string cryptoCode)
|
||||
{
|
||||
return base.GetInfo(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/connect")]
|
||||
public override Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
|
||||
{
|
||||
return base.ConnectToNode(cryptoCode, request);
|
||||
}
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||
public override Task<IActionResult> GetChannels(string cryptoCode)
|
||||
{
|
||||
return base.GetChannels(cryptoCode);
|
||||
}
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/channels")]
|
||||
public override Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
|
||||
{
|
||||
return base.OpenChannel(cryptoCode, request);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/address")]
|
||||
public override Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
{
|
||||
return base.GetDepositAddress(cryptoCode);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/pay")]
|
||||
public override Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
|
||||
{
|
||||
return base.PayInvoice(cryptoCode, lightningInvoice);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanUseLightningNodeInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices/{id}")]
|
||||
public override Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
{
|
||||
return base.GetInvoice(cryptoCode, id);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateLightningInvoiceInStore,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/lightning/{cryptoCode}/invoices")]
|
||||
public override Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
|
||||
{
|
||||
return base.CreateInvoice(cryptoCode, request);
|
||||
}
|
||||
|
||||
protected override Task<ILightningClient> GetLightningClient(string cryptoCode,
|
||||
bool doingAdminThings)
|
||||
{
|
||||
_btcPayServerOptions.InternalLightningByCryptoCode.TryGetValue(cryptoCode,
|
||||
out var internalLightningNode);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (network == null || store == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
if (existing == null || (existing.GetLightningUrl().IsInternalNode(internalLightningNode) &&
|
||||
!CanUseInternalLightning(doingAdminThings)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Task.FromResult(_lightningClientFactory.Create(existing.GetLightningUrl(), network));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
public class LightningUnavailableExceptionFilter : Attribute, IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is NBitcoin.JsonConverters.JsonObjectException jsonObject)
|
||||
{
|
||||
context.Result = new ObjectResult(new GreenfieldValidationError(jsonObject.Path, jsonObject.Message));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Result = new StatusCodeResult(503);
|
||||
}
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
public abstract class LightningNodeApiController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly CssThemeManager _cssThemeManager;
|
||||
|
||||
protected LightningNodeApiController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
BTCPayServerEnvironment btcPayServerEnvironment, CssThemeManager cssThemeManager)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_cssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInfo(string cryptoCode)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var info = await lightningClient.GetInfo();
|
||||
return Ok(new LightningNodeInformationData()
|
||||
{
|
||||
BlockHeight = info.BlockHeight,
|
||||
NodeURIs = info.NodeInfoList.Select(nodeInfo => nodeInfo).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> ConnectToNode(string cryptoCode, ConnectToNodeRequest request)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request?.NodeURI is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.NodeURI), "A valid node info was not provided to connect to");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var result = await lightningClient.ConnectTo(request.NodeURI);
|
||||
switch (result)
|
||||
{
|
||||
case ConnectionResult.Ok:
|
||||
return Ok();
|
||||
case ConnectionResult.CouldNotConnect:
|
||||
return this.CreateAPIError("could-not-connect", "Could not connect to the remote node");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetChannels(string cryptoCode)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var channels = await lightningClient.ListChannels();
|
||||
return Ok(channels.Select(channel => new LightningChannelData()
|
||||
{
|
||||
Capacity = channel.Capacity,
|
||||
ChannelPoint = channel.ChannelPoint.ToString(),
|
||||
IsActive = channel.IsActive,
|
||||
IsPublic = channel.IsPublic,
|
||||
LocalBalance = channel.LocalBalance,
|
||||
RemoteNode = channel.RemoteNode.ToString()
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<IActionResult> OpenChannel(string cryptoCode, OpenLightningChannelRequest request)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request?.NodeURI is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.NodeURI),
|
||||
"A valid node info was not provided to open a channel with");
|
||||
}
|
||||
|
||||
if (request.ChannelAmount == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount is missing");
|
||||
}
|
||||
else if (request.ChannelAmount.Satoshi <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.ChannelAmount), "ChannelAmount must be more than 0");
|
||||
}
|
||||
|
||||
if (request.FeeRate == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate is missing");
|
||||
}
|
||||
else if (request.FeeRate.SatoshiPerByte <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.FeeRate), "FeeRate must be more than 0");
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var response = await lightningClient.OpenChannel(new Lightning.OpenChannelRequest()
|
||||
{
|
||||
ChannelAmount = request.ChannelAmount,
|
||||
FeeRate = request.FeeRate,
|
||||
NodeInfo = request.NodeURI
|
||||
});
|
||||
|
||||
string errorCode, errorMessage;
|
||||
switch (response.Result)
|
||||
{
|
||||
case OpenChannelResult.Ok:
|
||||
return Ok();
|
||||
case OpenChannelResult.AlreadyExists:
|
||||
errorCode = "channel-already-exists";
|
||||
errorMessage = "The channel already exists";
|
||||
break;
|
||||
case OpenChannelResult.CannotAffordFunding:
|
||||
errorCode = "cannot-afford-funding";
|
||||
errorMessage = "Not enough money to open a channel";
|
||||
break;
|
||||
case OpenChannelResult.NeedMoreConf:
|
||||
errorCode = "need-more-confirmations";
|
||||
errorMessage = "Need to wait for more confirmations";
|
||||
break;
|
||||
case OpenChannelResult.PeerNotConnected:
|
||||
errorCode = "peer-not-connected";
|
||||
errorMessage = "Not connected to peer";
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("Unknown OpenChannelResult");
|
||||
}
|
||||
return this.CreateAPIError(errorCode, errorMessage);
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetDepositAddress(string cryptoCode)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new JValue((await lightningClient.GetDepositAddress()).ToString()));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> PayInvoice(string cryptoCode, PayLightningInvoiceRequest lightningInvoice)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, true);
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
|
||||
if (lightningClient == null || network == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (lightningInvoice?.BOLT11 is null ||
|
||||
!BOLT11PaymentRequest.TryParse(lightningInvoice.BOLT11, out _, network.NBitcoinNetwork))
|
||||
{
|
||||
ModelState.AddModelError(nameof(lightningInvoice.BOLT11), "The BOLT11 invoice was invalid.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
var result = await lightningClient.Pay(lightningInvoice.BOLT11);
|
||||
switch (result.Result)
|
||||
{
|
||||
case PayResult.CouldNotFindRoute:
|
||||
return this.CreateAPIError("could-not-find-route", "Impossible to find a route to the peer");
|
||||
case PayResult.Error:
|
||||
return this.CreateAPIError("generic-error", result.ErrorDetail);
|
||||
case PayResult.Ok:
|
||||
return Ok();
|
||||
default:
|
||||
throw new NotSupportedException("Unsupported Payresult");
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> GetInvoice(string cryptoCode, string id)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var inv = await lightningClient.GetInvoice(id);
|
||||
if (inv == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(ToModel(inv));
|
||||
}
|
||||
|
||||
public virtual async Task<IActionResult> CreateInvoice(string cryptoCode, CreateLightningInvoiceRequest request)
|
||||
{
|
||||
var lightningClient = await GetLightningClient(cryptoCode, false);
|
||||
|
||||
if (lightningClient == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.Amount < LightMoney.Zero)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "Amount should be more or equals to 0");
|
||||
}
|
||||
|
||||
if (request.Expiry <= TimeSpan.Zero)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Expiry), "Expiry should be more than 0");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var invoice = await lightningClient.CreateInvoice(
|
||||
new CreateInvoiceParams(request.Amount, request.Description, request.Expiry)
|
||||
{
|
||||
PrivateRouteHints = request.PrivateRouteHints
|
||||
},
|
||||
CancellationToken.None);
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
|
||||
private LightningInvoiceData ToModel(LightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoiceData()
|
||||
{
|
||||
Amount = invoice.Amount,
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
AmountReceived = invoice.AmountReceived,
|
||||
PaidAt = invoice.PaidAt,
|
||||
BOLT11 = invoice.BOLT11,
|
||||
ExpiresAt = invoice.ExpiresAt
|
||||
};
|
||||
}
|
||||
|
||||
protected bool CanUseInternalLightning(bool doingAdminThings)
|
||||
{
|
||||
return (_btcPayServerEnvironment.IsDevelopping || User.IsInRole(Roles.ServerAdmin) ||
|
||||
(_cssThemeManager.AllowLightningInternalNodeForAll && !doingAdminThings));
|
||||
}
|
||||
|
||||
protected abstract Task<ILightningClient> GetLightningClient(string cryptoCode, bool doingAdminThings);
|
||||
}
|
||||
}
|
164
BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs
Normal file
164
BTCPayServer/Controllers/GreenField/PaymentRequestsController.cs
Normal file
@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenFieldPaymentRequestsController : ControllerBase
|
||||
{
|
||||
private readonly PaymentRequestRepository _paymentRequestRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
|
||||
public GreenFieldPaymentRequestsController(PaymentRequestRepository paymentRequestRepository,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_paymentRequestRepository = paymentRequestRepository;
|
||||
_currencyNameTable = currencyNameTable;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
|
||||
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
|
||||
{
|
||||
var prs = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() {StoreId = storeId, IncludeArchived = false});
|
||||
return Ok(prs.Items.Select(FromModel));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||
public async Task<ActionResult<PaymentRequestData>> GetPaymentRequest(string storeId, string paymentRequestId)
|
||||
{
|
||||
var pr = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
|
||||
|
||||
if (pr.Total == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(FromModel(pr.Items.First()));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||
public async Task<ActionResult> ArchivePaymentRequest(string storeId, string paymentRequestId)
|
||||
{
|
||||
var pr = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}, IncludeArchived = false});
|
||||
if (pr.Total == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var updatedPr = pr.Items.First();
|
||||
updatedPr.Archived = true;
|
||||
await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores/{storeId}/payment-requests")]
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreatePaymentRequest(string storeId,
|
||||
CreatePaymentRequestRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
var pr = new PaymentRequestData()
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Status = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending,
|
||||
Created = DateTimeOffset.Now
|
||||
};
|
||||
pr.SetBlob(request);
|
||||
pr = await _paymentRequestRepository.CreateOrUpdatePaymentRequest(pr);
|
||||
return Ok(FromModel(pr));
|
||||
}
|
||||
|
||||
[HttpPut("~/api/v1/stores/{storeId}/payment-requests/{paymentRequestId}")]
|
||||
[Authorize(Policy = Policies.CanModifyPaymentRequests,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdatePaymentRequest(string storeId,
|
||||
string paymentRequestId, [FromBody] UpdatePaymentRequestRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
var pr = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() {StoreId = storeId, Ids = new[] {paymentRequestId}});
|
||||
if (pr.Total == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var updatedPr = pr.Items.First();
|
||||
updatedPr.SetBlob(request);
|
||||
|
||||
return Ok(FromModel(await _paymentRequestRepository.CreateOrUpdatePaymentRequest(updatedPr)));
|
||||
}
|
||||
|
||||
private IActionResult Validate(PaymentRequestBaseData data)
|
||||
{
|
||||
if (data is null)
|
||||
return BadRequest();
|
||||
if (data.Amount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(data.Amount), "Please provide an amount greater than 0");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(data.Currency) ||
|
||||
_currencyNameTable.GetCurrencyData(data.Currency, false) == null)
|
||||
ModelState.AddModelError(nameof(data.Currency), "Invalid currency");
|
||||
|
||||
if (string.IsNullOrEmpty(data.Title))
|
||||
ModelState.AddModelError(nameof(data.Title), "Title is required");
|
||||
|
||||
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
|
||||
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
|
||||
|
||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) :null;
|
||||
}
|
||||
|
||||
private static Client.Models.PaymentRequestData FromModel(PaymentRequestData data)
|
||||
{
|
||||
var blob = data.GetBlob();
|
||||
return new Client.Models.PaymentRequestData()
|
||||
{
|
||||
Created = data.Created,
|
||||
Id = data.Id,
|
||||
Status = data.Status,
|
||||
Archived = data.Archived,
|
||||
Amount = blob.Amount,
|
||||
Currency = blob.Currency,
|
||||
Description = blob.Description,
|
||||
Title = blob.Title,
|
||||
ExpiryDate = blob.ExpiryDate,
|
||||
Email = blob.Email,
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
||||
EmbeddedCSS = blob.EmbeddedCSS,
|
||||
CustomCSSLink = blob.CustomCSSLink
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
76
BTCPayServer/Controllers/GreenField/ServerInfoController.cs
Normal file
76
BTCPayServer/Controllers/GreenField/ServerInfoController.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
public class GreenFieldServerInfoController : Controller
|
||||
{
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly NBXplorerDashboard _dashBoard;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
|
||||
public GreenFieldServerInfoController(
|
||||
BTCPayServerEnvironment env,
|
||||
NBXplorerDashboard dashBoard,
|
||||
StoreRepository storeRepository,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary)
|
||||
{
|
||||
_env = env;
|
||||
_dashBoard = dashBoard;
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_networkProvider = networkProvider;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/server/info")]
|
||||
public async Task<ActionResult> ServerInfo()
|
||||
{
|
||||
var stores = await _storeRepository.GetStoresByUserId(_userManager.GetUserId(User));
|
||||
var supportedPaymentMethods = _paymentMethodHandlerDictionary
|
||||
.SelectMany(handler => handler.GetSupportedPaymentMethods().Select(id => id.ToString()))
|
||||
.Distinct();
|
||||
var syncStatus = _dashBoard.GetAll()
|
||||
.Where(summary => summary.Network.ShowSyncSummary)
|
||||
.Select(summary => new ServerInfoSyncStatusData
|
||||
{
|
||||
CryptoCode = summary.Network.CryptoCode,
|
||||
NodeInformation = summary.Status.BitcoinStatus is BitcoinStatus s ? new ServerInfoNodeData()
|
||||
{
|
||||
Headers = s.Headers,
|
||||
Blocks = s.Blocks,
|
||||
VerificationProgress = s.VerificationProgress
|
||||
}: null,
|
||||
ChainHeight = summary.Status.ChainHeight,
|
||||
SyncHeight = summary.Status.SyncHeight
|
||||
});
|
||||
ServerInfoData model = new ServerInfoData
|
||||
{
|
||||
FullySynched = _dashBoard.IsFullySynched(),
|
||||
SyncStatus = syncStatus,
|
||||
Onion = _env.OnionUrl,
|
||||
Version = _env.Version,
|
||||
SupportedPaymentMethods = supportedPaymentMethods
|
||||
};
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
}
|
201
BTCPayServer/Controllers/GreenField/StoresController.cs
Normal file
201
BTCPayServer/Controllers/GreenField/StoresController.cs
Normal file
@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class GreenFieldController : ControllerBase
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
|
||||
public GreenFieldController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
}
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores")]
|
||||
public ActionResult<IEnumerable<Client.Models.StoreData>> GetStores()
|
||||
{
|
||||
var stores = HttpContext.GetStoresData();
|
||||
return Ok(stores.Select(FromModel));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}")]
|
||||
public ActionResult<Client.Models.StoreData> GetStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}")]
|
||||
public async Task<IActionResult> RemoveStore(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!_storeRepository.CanDeleteStores())
|
||||
{
|
||||
return this.CreateAPIError("unsupported",
|
||||
"BTCPay Server is using a database server that does not allow you to remove stores.");
|
||||
}
|
||||
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("~/api/v1/stores")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
|
||||
{
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
var store = new Data.StoreData();
|
||||
ToModel(request, store);
|
||||
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null)
|
||||
{
|
||||
return validationResult;
|
||||
}
|
||||
|
||||
ToModel(request, store);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
|
||||
private static Client.Models.StoreData FromModel(Data.StoreData data)
|
||||
{
|
||||
var storeBlob = data.GetStoreBlob();
|
||||
return new Client.Models.StoreData()
|
||||
{
|
||||
Id = data.Id,
|
||||
Name = data.StoreName,
|
||||
Website = data.StoreWebsite,
|
||||
SpeedPolicy = data.SpeedPolicy,
|
||||
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
|
||||
//blob
|
||||
//we do not include DefaultCurrencyPairs,Spread, PreferredExchange, RateScripting, RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
|
||||
//we do not include ChangellySettings in this model and instead opt to set it in stores/storeid/changelly endpoints
|
||||
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
|
||||
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
||||
DefaultLang = storeBlob.DefaultLang,
|
||||
MonitoringExpiration = TimeSpan.FromMinutes(storeBlob.MonitoringExpiration),
|
||||
InvoiceExpiration = TimeSpan.FromMinutes(storeBlob.InvoiceExpiration),
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
CustomLogo = storeBlob.CustomLogo,
|
||||
CustomCSS = storeBlob.CustomCSS,
|
||||
HtmlTitle = storeBlob.HtmlTitle,
|
||||
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
|
||||
LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate,
|
||||
PaymentTolerance = storeBlob.PaymentTolerance,
|
||||
RedirectAutomatically = storeBlob.RedirectAutomatically,
|
||||
PayJoinEnabled = storeBlob.PayJoinEnabled,
|
||||
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints
|
||||
};
|
||||
}
|
||||
|
||||
private static void ToModel(StoreBaseData restModel, Data.StoreData model)
|
||||
{
|
||||
var blob = model.GetStoreBlob();
|
||||
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreWebsite = restModel.Website;
|
||||
model.SpeedPolicy = restModel.SpeedPolicy;
|
||||
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
|
||||
//blob
|
||||
//we do not include DefaultCurrencyPairs;Spread; PreferredExchange; RateScripting; RateScript in this model and instead opt to set it in stores/storeid/rates endpoints
|
||||
//we do not include ChangellySettings in this model and instead opt to set it in stores/storeid/changelly endpoints
|
||||
//we do not include CoinSwitchSettings in this model and instead opt to set it in stores/storeid/coinswitch endpoints
|
||||
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
||||
blob.DefaultLang = restModel.DefaultLang;
|
||||
blob.MonitoringExpiration = (int)restModel.MonitoringExpiration.TotalMinutes;
|
||||
blob.InvoiceExpiration = (int)restModel.InvoiceExpiration.TotalMinutes;
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.CustomLogo = restModel.CustomLogo;
|
||||
blob.CustomCSS = restModel.CustomCSS;
|
||||
blob.HtmlTitle = restModel.HtmlTitle;
|
||||
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice;
|
||||
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance;
|
||||
blob.RedirectAutomatically = restModel.RedirectAutomatically;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
||||
model.SetStoreBlob(blob);
|
||||
}
|
||||
|
||||
private IActionResult Validate(StoreBaseData request)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Name))
|
||||
ModelState.AddModelError(nameof(request.Name), "Name is missing");
|
||||
else if(request.Name.Length < 1 || request.Name.Length > 50)
|
||||
ModelState.AddModelError(nameof(request.Name), "Name can only be between 1 and 50 characters");
|
||||
if (!string.IsNullOrEmpty(request.Website) && !Uri.TryCreate(request.Website, UriKind.Absolute, out _))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Website), "Website is not a valid url");
|
||||
}
|
||||
if(request.InvoiceExpiration < TimeSpan.FromMinutes(1) && request.InvoiceExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
|
||||
ModelState.AddModelError(nameof(request.InvoiceExpiration), "InvoiceExpiration can only be between 1 and 34560 mins");
|
||||
if(request.MonitoringExpiration < TimeSpan.FromMinutes(10) && request.MonitoringExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
|
||||
ModelState.AddModelError(nameof(request.MonitoringExpiration), "InvoiceExpiration can only be between 10 and 34560 mins");
|
||||
if(request.PaymentTolerance < 0 && request.PaymentTolerance > 100)
|
||||
ModelState.AddModelError(nameof(request.PaymentTolerance), "PaymentTolerance can only be between 0 and 100 percent");
|
||||
|
||||
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NicolasDorier.RateLimits;
|
||||
using BTCPayServer.Client;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
@ -62,16 +63,21 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("~/api/v1/users")]
|
||||
public async Task<ActionResult<ApplicationUserData>> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
|
||||
public async Task<IActionResult> CreateUser(CreateApplicationUserRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request?.Email is null)
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Email), "Email is missing"));
|
||||
if (!Validation.EmailValidator.IsEmail(request.Email))
|
||||
ModelState.AddModelError(nameof(request.Email), "Email is missing");
|
||||
if (!string.IsNullOrEmpty(request?.Email) && !Validation.EmailValidator.IsEmail(request.Email))
|
||||
{
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Email), "Invalid email"));
|
||||
ModelState.AddModelError(nameof(request.Email), "Invalid email");
|
||||
}
|
||||
if (request?.Password is null)
|
||||
return BadRequest(CreateValidationProblem(nameof(request.Password), "Password is missing"));
|
||||
ModelState.AddModelError(nameof(request.Password), "Password is missing");
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
var anyAdmin = (await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin)).Any();
|
||||
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();
|
||||
var isAuth = User.Identity.AuthenticationType == GreenFieldConstants.AuthenticationType;
|
||||
@ -113,7 +119,7 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Password), error.Description);
|
||||
}
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
if (!isAdmin)
|
||||
{
|
||||
@ -125,9 +131,12 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
foreach (var error in identityResult.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
if (error.Code == "DuplicateUserName")
|
||||
ModelState.AddModelError(nameof(request.Email), error.Description);
|
||||
else
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
return BadRequest(new ValidationProblemDetails(ModelState));
|
||||
return this.CreateValidationError(ModelState);
|
||||
}
|
||||
|
||||
if (request.IsAdministrator is true)
|
||||
@ -152,13 +161,6 @@ namespace BTCPayServer.Controllers.GreenField
|
||||
return CreatedAtAction(string.Empty, user);
|
||||
}
|
||||
|
||||
private ValidationProblemDetails CreateValidationProblem(string propertyName, string errorMessage)
|
||||
{
|
||||
var modelState = new ModelStateDictionary();
|
||||
modelState.AddModelError(propertyName, errorMessage);
|
||||
return new ValidationProblemDetails(modelState);
|
||||
}
|
||||
|
||||
private static ApplicationUserData FromModel(ApplicationUser data)
|
||||
{
|
||||
return new ApplicationUserData()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user