mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 06:21:44 +01:00
Merge branch 'master' into MonetaryUnit
This commit is contained in:
commit
f14ad8be58
179 changed files with 5922 additions and 2209 deletions
|
@ -2,12 +2,36 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Company>BTCPay Server</Company>
|
||||
<Copyright>Copyright © BTCPay Server 2020</Copyright>
|
||||
<Description>A client library for BTCPay Server Greenfield API</Description>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageTags>btcpay,btcpayserver</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/btcpayserver/btcpayserver/tree/master/BTCPayServer.Client</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.1.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<NoWarn>1591;1573;1572;1584;1570;3021</NoWarn>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="5.0.51" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace BTCPayServer.Client
|
|||
{
|
||||
|
||||
public static Uri GenerateAuthorizeUri(Uri btcpayHost, string[] permissions, bool strict = true,
|
||||
bool selectiveStores = false)
|
||||
bool selectiveStores = false, (string ApplicationIdentifier, Uri Redirect) applicationDetails = default)
|
||||
{
|
||||
var result = new UriBuilder(btcpayHost);
|
||||
result.Path = "api-keys/authorize";
|
||||
|
@ -18,6 +18,15 @@ namespace BTCPayServer.Client
|
|||
{"strict", strict}, {"selectiveStores", selectiveStores}, {"permissions", permissions}
|
||||
});
|
||||
|
||||
if (applicationDetails.Redirect != null)
|
||||
{
|
||||
AppendPayloadToQuery(result, new KeyValuePair<string, object>("redirect", applicationDetails.Redirect));
|
||||
if (!string.IsNullOrEmpty(applicationDetails.ApplicationIdentifier))
|
||||
{
|
||||
AppendPayloadToQuery(result, new KeyValuePair<string, object>("applicationIdentifier", applicationDetails.ApplicationIdentifier));
|
||||
}
|
||||
}
|
||||
|
||||
return result.Uri;
|
||||
}
|
||||
}
|
||||
|
|
71
BTCPayServer.Client/BTCPayServerClient.Invoices.cs
Normal file
71
BTCPayServer.Client/BTCPayServerClient.Invoices.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
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<InvoiceData>> GetInvoices(string storeId, bool includeArchived = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices",
|
||||
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
|
||||
return await HandleResponse<IEnumerable<InvoiceData>>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> GetInvoice(string storeId, string invoiceId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}"), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task ArchiveInvoice(string storeId, string invoiceId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}",
|
||||
method: HttpMethod.Delete), token);
|
||||
await HandleResponse(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> CreateInvoice(string storeId,
|
||||
CreateInvoiceRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request, CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (request.Status!= InvoiceStatus.Complete && request.Status!= InvoiceStatus.Invalid)
|
||||
throw new ArgumentOutOfRangeException(nameof(request.Status), "Status can only be Invalid or Complete");
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/status", bodyPayload: request,
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
|
||||
public virtual async Task<InvoiceData> UnarchiveInvoice(string storeId, string invoiceId, CancellationToken token = default)
|
||||
{
|
||||
var response = await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive",
|
||||
method: HttpMethod.Post), token);
|
||||
return await HandleResponse<InvoiceData>(response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,10 +10,13 @@ namespace BTCPayServer.Client
|
|||
public partial class BTCPayServerClient
|
||||
{
|
||||
public virtual async Task<IEnumerable<PaymentRequestData>> GetPaymentRequests(string storeId,
|
||||
bool includeArchived = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var response =
|
||||
await _httpClient.SendAsync(CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests"), token);
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-requests",
|
||||
new Dictionary<string, object>() {{nameof(includeArchived), includeArchived}}), token);
|
||||
return await HandleResponse<IEnumerable<PaymentRequestData>>(response);
|
||||
}
|
||||
|
||||
|
|
|
@ -103,29 +103,37 @@ namespace BTCPayServer.Client
|
|||
return request;
|
||||
}
|
||||
|
||||
private static void AppendPayloadToQuery(UriBuilder uri, Dictionary<string, object> payload)
|
||||
public static void AppendPayloadToQuery(UriBuilder uri, KeyValuePair<string, object> keyValuePair)
|
||||
{
|
||||
if (uri.Query.Length > 1)
|
||||
uri.Query += "&";
|
||||
|
||||
UriBuilder uriBuilder = uri;
|
||||
if (!(keyValuePair.Value is string) &&
|
||||
keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
|
||||
{
|
||||
foreach (var item in (IEnumerable)keyValuePair.Value)
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
|
||||
Uri.EscapeDataString(item.ToString()) + "&";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
|
||||
Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&";
|
||||
}
|
||||
uri.Query = uri.Query.Trim('&');
|
||||
}
|
||||
|
||||
public static void AppendPayloadToQuery(UriBuilder uri, Dictionary<string, object> payload)
|
||||
{
|
||||
if (uri.Query.Length > 1)
|
||||
uri.Query += "&";
|
||||
foreach (KeyValuePair<string, object> keyValuePair in payload)
|
||||
{
|
||||
UriBuilder uriBuilder = uri;
|
||||
if (!(keyValuePair.Value is string) && keyValuePair.Value.GetType().GetInterfaces().Contains((typeof(IEnumerable))))
|
||||
{
|
||||
foreach (var item in (IEnumerable)keyValuePair.Value)
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
|
||||
Uri.EscapeDataString(item.ToString()) + "&";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uriBuilder.Query = uriBuilder.Query + Uri.EscapeDataString(keyValuePair.Key) + "=" +
|
||||
Uri.EscapeDataString(keyValuePair.Value.ToString()) + "&";
|
||||
}
|
||||
AppendPayloadToQuery(uri, keyValuePair);
|
||||
}
|
||||
|
||||
uri.Query = uri.Query.Trim('&');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,38 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace BTCPayServer.Client.JsonConverters
|
||||
{
|
||||
public class TimeSpanJsonConverter : JsonConverter
|
||||
public abstract class TimeSpanJsonConverter : JsonConverter
|
||||
{
|
||||
public class Seconds : TimeSpanJsonConverter
|
||||
{
|
||||
protected override long ToLong(TimeSpan value)
|
||||
{
|
||||
return (long)value.TotalSeconds;
|
||||
}
|
||||
|
||||
protected override TimeSpan ToTimespan(long value)
|
||||
{
|
||||
return TimeSpan.FromSeconds(value);
|
||||
}
|
||||
}
|
||||
public class Minutes : TimeSpanJsonConverter
|
||||
{
|
||||
protected override long ToLong(TimeSpan value)
|
||||
{
|
||||
return (long)value.TotalMinutes;
|
||||
}
|
||||
protected override TimeSpan ToTimespan(long value)
|
||||
{
|
||||
return TimeSpan.FromMinutes(value);
|
||||
}
|
||||
}
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?);
|
||||
}
|
||||
|
||||
protected abstract TimeSpan ToTimespan(long value);
|
||||
protected abstract long ToLong(TimeSpan value);
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
|
@ -24,11 +49,11 @@ namespace BTCPayServer.Client.JsonConverters
|
|||
}
|
||||
if (reader.TokenType != JsonToken.Integer)
|
||||
throw new JsonObjectException("Invalid timespan, expected integer", reader);
|
||||
return TimeSpan.FromSeconds((long)reader.Value);
|
||||
return ToTimespan((long)reader.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new JsonObjectException("Invalid locktime", reader);
|
||||
throw new JsonObjectException("Invalid timespan", reader);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,7 +61,7 @@ namespace BTCPayServer.Client.JsonConverters
|
|||
{
|
||||
if (value is TimeSpan s)
|
||||
{
|
||||
writer.WriteValue((long)s.TotalSeconds);
|
||||
writer.WriteValue(ToLong(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
BTCPayServer.Client/Models/AddCustomerEmailRequest.cs
Normal file
7
BTCPayServer.Client/Models/AddCustomerEmailRequest.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class AddCustomerEmailRequest
|
||||
{
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
|
@ -21,5 +21,10 @@ namespace BTCPayServer.Client.Models
|
|||
/// whether the user needed to verify their email on account creation
|
||||
/// </summary>
|
||||
public bool RequiresEmailConfirmation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// the roles of the user
|
||||
/// </summary>
|
||||
public string[] Roles { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
35
BTCPayServer.Client/Models/CreateInvoiceRequest.cs
Normal file
35
BTCPayServer.Client/Models/CreateInvoiceRequest.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public JObject Metadata { get; set; }
|
||||
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
|
||||
|
||||
public class CheckoutOptions
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public SpeedPolicy? SpeedPolicy { get; set; }
|
||||
|
||||
public string[] PaymentMethods { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
[JsonProperty("expirationMinutes")]
|
||||
public TimeSpan? Expiration { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
[JsonProperty("monitoringMinutes")]
|
||||
public TimeSpan? Monitoring { get; set; }
|
||||
|
||||
public double? PaymentTolerance { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ namespace BTCPayServer.Client.Models
|
|||
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||
public LightMoney Amount { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(JsonConverters.TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan Expiry { get; set; }
|
||||
public bool PrivateRouteHints { get; set; }
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Client.Models
|
|||
[JsonProperty(ItemConverterType = typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
public string Currency { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset? ExpiresAt { get; set; }
|
||||
|
|
23
BTCPayServer.Client/Models/InvoiceData.cs
Normal file
23
BTCPayServer.Client/Models/InvoiceData.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class InvoiceData : CreateInvoiceRequest
|
||||
{
|
||||
public string Id { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceStatus Status { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceExceptionStatus AdditionalStatus { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset MonitoringExpiration { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset ExpirationTime { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
}
|
||||
}
|
12
BTCPayServer.Client/Models/InvoiceExceptionStatus.cs
Normal file
12
BTCPayServer.Client/Models/InvoiceExceptionStatus.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum InvoiceExceptionStatus
|
||||
{
|
||||
None,
|
||||
PaidLate,
|
||||
PaidPartial,
|
||||
Marked,
|
||||
Invalid,
|
||||
PaidOver
|
||||
}
|
||||
}
|
12
BTCPayServer.Client/Models/InvoiceStatus.cs
Normal file
12
BTCPayServer.Client/Models/InvoiceStatus.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public enum InvoiceStatus
|
||||
{
|
||||
New,
|
||||
Paid,
|
||||
Expired,
|
||||
Invalid,
|
||||
Complete,
|
||||
Confirmed
|
||||
}
|
||||
}
|
11
BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs
Normal file
11
BTCPayServer.Client/Models/MarkInvoiceStatusRequest.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace BTCPayServer.Client.Models
|
||||
{
|
||||
public class MarkInvoiceStatusRequest
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceStatus Status { get; set; }
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Client.Models
|
|||
public string Currency { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Amount { get; set; }
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public string ViewLink { get; set; }
|
||||
|
|
|
@ -16,11 +16,11 @@ namespace BTCPayServer.Client.Models
|
|||
|
||||
public string Website { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace BTCPayServer.Client
|
|||
public const string CanModifyStoreSettings = "btcpay.store.canmodifystoresettings";
|
||||
public const string CanModifyStoreSettingsUnscoped = "btcpay.store.canmodifystoresettings:";
|
||||
public const string CanViewStoreSettings = "btcpay.store.canviewstoresettings";
|
||||
public const string CanViewInvoices = "btcpay.store.canviewinvoices";
|
||||
public const string CanCreateInvoice = "btcpay.store.cancreateinvoice";
|
||||
public const string CanViewPaymentRequests = "btcpay.store.canviewpaymentrequests";
|
||||
public const string CanModifyPaymentRequests = "btcpay.store.canmodifypaymentrequests";
|
||||
|
@ -26,6 +27,7 @@ namespace BTCPayServer.Client
|
|||
{
|
||||
get
|
||||
{
|
||||
yield return CanViewInvoices;
|
||||
yield return CanCreateInvoice;
|
||||
yield return CanModifyServerSettings;
|
||||
yield return CanModifyStoreSettings;
|
||||
|
@ -153,6 +155,8 @@ namespace BTCPayServer.Client
|
|||
return true;
|
||||
switch (subpolicy)
|
||||
{
|
||||
case Policies.CanViewInvoices when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewInvoices when this.Policy == Policies.CanViewStoreSettings:
|
||||
case Policies.CanViewStoreSettings when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanCreateInvoice when this.Policy == Policies.CanModifyStoreSettings:
|
||||
case Policies.CanViewProfile when this.Policy == Policies.CanModifyProfile:
|
||||
|
|
7
BTCPayServer.Client/PushNuget.ps1
Normal file
7
BTCPayServer.Client/PushNuget.ps1
Normal file
|
@ -0,0 +1,7 @@
|
|||
rm "bin\release\" -Recurse -Force
|
||||
dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg
|
||||
$package=(ls .\bin\Release\*.nupkg).FullName
|
||||
dotnet nuget push $package --source "https://api.nuget.org/v3/index.json"
|
||||
$ver = ((ls .\bin\release\*.nupkg)[0].Name -replace '.*(\d+\.\d+\.\d+)\.nupkg','$1')
|
||||
git tag -a "BTCPayServer.Client/v$ver" -m "BTCPayServer.Client/$ver"
|
||||
git push origin "BTCPayServer.Client/v$ver"
|
BIN
BTCPayServer.Client/icon.png
Normal file
BIN
BTCPayServer.Client/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,78 @@
|
|||
#if ALTCOINS
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitEthereum()
|
||||
{
|
||||
Add(new EthereumBTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "ETH",
|
||||
DisplayName = "Ethereum",
|
||||
DefaultRateRules = new[] {"ETH_X = ETH_BTC * BTC_X", "ETH_BTC = kraken(ETH_BTC)"},
|
||||
BlockExplorerLink =
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://etherscan.io/address/{0}"
|
||||
: "https://ropsten.etherscan.io/address/{0}",
|
||||
CryptoImagePath = "/imlegacy/eth.png",
|
||||
ShowSyncSummary = true,
|
||||
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
|
||||
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
|
||||
Divisibility = 18,
|
||||
});
|
||||
}
|
||||
|
||||
public void InitERC20()
|
||||
{
|
||||
if (NetworkType != NetworkType.Mainnet)
|
||||
{
|
||||
Add(new ERC20BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "FAU",
|
||||
DisplayName = "Faucet Token",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"FAU_X = FAU_BTC * BTC_X",
|
||||
"FAU_BTC = 0.01",
|
||||
},
|
||||
BlockExplorerLink = "https://ropsten.etherscan.io/address/{0}#tokentxns",
|
||||
ShowSyncSummary = false,
|
||||
CoinType = 1,
|
||||
ChainId = 3,
|
||||
//use https://erc20faucet.com for testnet
|
||||
SmartContractAddress = "0xFab46E002BbF0b4509813474841E0716E6730136",
|
||||
Divisibility = 18,
|
||||
CryptoImagePath = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Add(new ERC20BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "USDT20",
|
||||
DisplayName = "Tether USD (ERC20)",
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"USDT20_UST = 1",
|
||||
"USDT20_X = USDT20_BTC * BTC_X",
|
||||
"USDT20_BTC = bitfinex(UST_BTC)",
|
||||
},
|
||||
BlockExplorerLink =
|
||||
NetworkType == NetworkType.Mainnet
|
||||
? "https://etherscan.io/address/{0}#tokentxns"
|
||||
: "https://ropsten.etherscan.io/address/{0}#tokentxns",
|
||||
CryptoImagePath = "/imlegacy/liquid-tether.svg",
|
||||
ShowSyncSummary = false,
|
||||
CoinType = NetworkType == NetworkType.Mainnet? 60 : 1,
|
||||
ChainId = NetworkType == NetworkType.Mainnet ? 1 : 3,
|
||||
SmartContractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
Divisibility = 6
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,20 @@
|
|||
#if ALTCOINS
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class EthereumBTCPayNetwork : BTCPayNetworkBase
|
||||
{
|
||||
public int ChainId { get; set; }
|
||||
public int CoinType { get; set; }
|
||||
|
||||
public string GetDefaultKeyPath()
|
||||
{
|
||||
return $"m/44'/{CoinType}'/0'/0/x";
|
||||
}
|
||||
}
|
||||
|
||||
public class ERC20BTCPayNetwork : EthereumBTCPayNetwork
|
||||
{
|
||||
public string SmartContractAddress { get; set; }
|
||||
}
|
||||
}
|
||||
#endif
|
20
BTCPayServer.Common/Altcoins/Ethereum/EthereumExtensions.cs
Normal file
20
BTCPayServer.Common/Altcoins/Ethereum/EthereumExtensions.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
#if ALTCOINS
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class EthereumExtensions
|
||||
{
|
||||
|
||||
public static IEnumerable<string> GetAllEthereumSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfiltered)
|
||||
{
|
||||
var ethBased = networkProvider.GetAll().OfType<EthereumBTCPayNetwork>();
|
||||
var chainId = ethBased.Select(network => network.ChainId).Distinct();
|
||||
return unfiltered.GetAll().OfType<EthereumBTCPayNetwork>()
|
||||
.Where(network => chainId.Contains(network.ChainId))
|
||||
.Select(network => network.CryptoCode.ToUpperInvariant());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -6,11 +6,11 @@ namespace BTCPayServer
|
|||
{
|
||||
public static class LiquidExtensions
|
||||
{
|
||||
public static IEnumerable<string> GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfilteredNetworkProvider)
|
||||
public static IEnumerable<string> GetAllElementsSubChains(this BTCPayNetworkProvider networkProvider, BTCPayNetworkProvider unfiltered)
|
||||
{
|
||||
var elementsBased = networkProvider.GetAll().OfType<ElementsBTCPayNetwork>();
|
||||
var parentChains = elementsBased.Select(network => network.NetworkCryptoCode.ToUpperInvariant()).Distinct();
|
||||
return unfilteredNetworkProvider.GetAll().OfType<ElementsBTCPayNetwork>()
|
||||
return unfiltered.GetAll().OfType<ElementsBTCPayNetwork>()
|
||||
.Where(network => parentChains.Contains(network.NetworkCryptoCode)).Select(network => network.CryptoCode.ToUpperInvariant());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,8 +57,10 @@ namespace BTCPayServer
|
|||
InitMonero();
|
||||
InitPolis();
|
||||
InitChaincoin();
|
||||
InitArgoneum();
|
||||
// InitArgoneum();//their rate source is down 9/15/20.
|
||||
InitMonetaryUnit();
|
||||
InitEthereum();
|
||||
InitERC20();
|
||||
|
||||
// Assume that electrum mappings are same as BTC if not specified
|
||||
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
|
||||
|
|
|
@ -43,6 +43,8 @@ namespace BTCPayServer.Data
|
|||
public class APIKeyBlob
|
||||
{
|
||||
public string[] Permissions { get; set; }
|
||||
public string ApplicationIdentifier { get; set; }
|
||||
public string ApplicationAuthority { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace BTCPayServer.Data
|
|||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
public EventSeverity Severity { get; set; } = EventSeverity.Info;
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
@ -35,5 +36,25 @@ namespace BTCPayServer.Data
|
|||
#pragma warning restore CS0618
|
||||
});
|
||||
}
|
||||
|
||||
public enum EventSeverity
|
||||
{
|
||||
Info,
|
||||
Error,
|
||||
Success,
|
||||
Warning
|
||||
}
|
||||
|
||||
public string GetCssClass()
|
||||
{
|
||||
return Severity switch
|
||||
{
|
||||
EventSeverity.Info => "info",
|
||||
EventSeverity.Error => "danger",
|
||||
EventSeverity.Success => "success",
|
||||
EventSeverity.Warning => "warning",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Data
|
|||
[Required]
|
||||
public string InvoiceDataId { get; set; }
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string PullPaymentDataId { get; set; }
|
||||
public PullPaymentData PullPaymentData { get; set; }
|
||||
public InvoiceData InvoiceData { get; set; }
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace BTCPayServer.Migrations
|
|||
{
|
||||
Id = table.Column<string>(maxLength: 30, nullable: false),
|
||||
Date = table.Column<DateTimeOffset>(nullable: false),
|
||||
PullPaymentDataId = table.Column<string>(nullable: true),
|
||||
PullPaymentDataId = table.Column<string>(maxLength: 30, nullable: true),
|
||||
State = table.Column<string>(maxLength: 20, nullable: false),
|
||||
PaymentMethodId = table.Column<string>(maxLength: 20, nullable: false),
|
||||
Destination = table.Column<string>(nullable: true),
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20200901161733_AddInvoiceEventLogSeverity")]
|
||||
public partial class AddInvoiceEventLogSeverity : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Severity",
|
||||
table: "InvoiceEvents",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (this.SupportDropColumn(migrationBuilder.ActiveProvider))
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Severity",
|
||||
table: "InvoiceEvents");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -240,6 +240,9 @@ namespace BTCPayServer.Migrations
|
|||
b.Property<string>("Message")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
@ -1293,5 +1293,26 @@
|
|||
"divisibility":0,
|
||||
"symbol":"Sats",
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name": "Ethereum",
|
||||
"code": "ETH",
|
||||
"divisibility": 18,
|
||||
"symbol": null,
|
||||
"crypto": true
|
||||
},
|
||||
{
|
||||
"name":"USDt",
|
||||
"code":"USDT20",
|
||||
"divisibility":6,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
},
|
||||
{
|
||||
"name":"FaucetToken",
|
||||
"code":"FAU",
|
||||
"divisibility":18,
|
||||
"symbol":null,
|
||||
"crypto":true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -24,13 +24,21 @@ namespace BTCPayServer.Rating
|
|||
var jarray = await response.Content.ReadAsAsync<JArray>(cancellationToken);
|
||||
return jarray
|
||||
.Children<JObject>()
|
||||
.Where(p => CurrencyPair.TryParse(p["symbol"].Value<string>(), out _))
|
||||
.Select(p => new PairRate(CurrencyPair.Parse(p["symbol"].Value<string>()), CreateBidAsk(p)))
|
||||
.Select(p =>
|
||||
{
|
||||
CurrencyPair.TryParse(p["symbol"].Value<string>(), out var currency);
|
||||
var bidask = CreateBidAsk(p);
|
||||
return (currency, bidask);
|
||||
})
|
||||
.Where(p => p.currency != null && p.bidask != null)
|
||||
.Select(p => new PairRate(p.currency, p.bidask))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private BidAsk CreateBidAsk(JObject p)
|
||||
{
|
||||
if (p["bid"].Type != JTokenType.String || p["ask"].Type != JTokenType.String)
|
||||
return null;
|
||||
var bid = p["bid"].Value<decimal>();
|
||||
var ask = p["ask"].Value<decimal>();
|
||||
return new BidAsk(bid, ask);
|
||||
|
|
|
@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Rates
|
|||
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
|
||||
Providers.Add("bitflyer", new BitflyerRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITFLYER")));
|
||||
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
|
||||
Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
// Providers.Add("argoneum", new ArgoneumRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_ARGONEUM")));
|
||||
|
||||
|
||||
// Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking
|
||||
|
|
|
@ -877,7 +877,7 @@ normal:
|
|||
InvoiceEntity invoiceEntity = new InvoiceEntity();
|
||||
invoiceEntity.Networks = networkProvider;
|
||||
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
invoiceEntity.ProductInformation = new ProductInformation() { Price = 100 };
|
||||
invoiceEntity.Price = 100;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, }
|
||||
.SetPaymentMethodDetails(
|
||||
|
|
110
BTCPayServer.Tests/AltcoinTests/EthereumTests.cs
Normal file
110
BTCPayServer.Tests/AltcoinTests/EthereumTests.cs
Normal file
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Configuration.Memory;
|
||||
using NBitcoin;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class EthereumTests
|
||||
{
|
||||
public const int TestTimeout = 60_000;
|
||||
|
||||
public EthereumTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
public void LoadSubChainsAlways()
|
||||
{
|
||||
var options = new BTCPayServerOptions();
|
||||
options.LoadArgs(new ConfigurationRoot(new List<IConfigurationProvider>()
|
||||
{
|
||||
new MemoryConfigurationProvider(new MemoryConfigurationSource()
|
||||
{
|
||||
InitialData = new[] {new KeyValuePair<string, string>("chains", "usdt20"),}
|
||||
})
|
||||
}));
|
||||
|
||||
Assert.NotNull(options.NetworkProvider.GetNetwork("ETH"));
|
||||
Assert.NotNull(options.NetworkProvider.GetNetwork("USDT20"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Altcoins", "Altcoins")]
|
||||
public async Task CanUseEthereum()
|
||||
{
|
||||
using var s = SeleniumTester.Create("ETHEREUM", true);
|
||||
s.Server.ActivateETH();
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser(true);
|
||||
|
||||
IWebElement syncSummary = null;
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
syncSummary = s.Driver.FindElement(By.Id("modalDialog"));
|
||||
Assert.True(syncSummary.Displayed);
|
||||
});
|
||||
var web3Link = syncSummary.FindElement(By.LinkText("Configure Web3"));
|
||||
web3Link.Click();
|
||||
s.Driver.FindElement(By.Id("Web3ProviderUrl")).SendKeys("https://ropsten-rpc.linkpool.io");
|
||||
s.Driver.FindElement(By.Id("saveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.AssertElementNotFound(By.Id("modalDialog"));
|
||||
});
|
||||
|
||||
var store = s.CreateNewStore();
|
||||
s.Driver.FindElement(By.LinkText("Ethereum")).Click();
|
||||
|
||||
var seed = new Mnemonic(Wordlist.English);
|
||||
s.Driver.FindElement(By.Id("ModifyETH")).Click();
|
||||
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
s.Driver.FindElement(By.Id("ModifyUSDT20")).Click();
|
||||
s.Driver.FindElement(By.Id("Seed")).SendKeys(seed.ToString());
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("StoreSeed")), true);
|
||||
s.SetCheckbox(s.Driver.FindElement(By.Id("Enabled")), true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.AssertHappyMessage();
|
||||
|
||||
var invoiceId = s.CreateInvoice(store.storeName, 10);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
var currencyDropdownButton = s.Driver.WaitForElement(By.ClassName("payment__currencies"));
|
||||
Assert.Contains("ETH", currencyDropdownButton.Text);
|
||||
s.Driver.FindElement(By.Id("copy-tab")).Click();
|
||||
|
||||
var ethAddress = s.Driver.FindElements(By.ClassName("copySectionBox"))
|
||||
.Single(element => element.FindElement(By.TagName("label")).Text
|
||||
.Contains("Address", StringComparison.InvariantCultureIgnoreCase)).FindElement(By.TagName("input"))
|
||||
.GetAttribute("value");
|
||||
currencyDropdownButton.Click();
|
||||
var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem"));
|
||||
Assert.Equal(2, elements.Count);
|
||||
|
||||
elements.Single(element => element.Text.Contains("USDT20")).Click();
|
||||
s.Driver.FindElement(By.Id("copy-tab")).Click();
|
||||
var usdtAddress = s.Driver.FindElements(By.ClassName("copySectionBox"))
|
||||
.Single(element => element.FindElement(By.TagName("label")).Text
|
||||
.Contains("Address", StringComparison.InvariantCultureIgnoreCase)).FindElement(By.TagName("input"))
|
||||
.GetAttribute("value");
|
||||
Assert.Equal(usdtAddress, ethAddress);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
@ -11,6 +10,7 @@ using BTCPayServer.Security.GreenField;
|
|||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
@ -89,7 +89,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
|
||||
//there should be a store already by default in the dropdown
|
||||
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[2].SpecificStores[0]"));
|
||||
var dropdown = s.Driver.FindElement(By.Name("PermissionValues[3].SpecificStores[0]"));
|
||||
var option = dropdown.FindElement(By.TagName("option"));
|
||||
var storeId = option.GetAttribute("value");
|
||||
option.Click();
|
||||
|
@ -109,7 +109,6 @@ namespace BTCPayServer.Tests
|
|||
tester.PayTester.HttpClient);
|
||||
});
|
||||
|
||||
|
||||
//let's test the authorized screen now
|
||||
//options for authorize are:
|
||||
//applicationName
|
||||
|
@ -117,27 +116,30 @@ namespace BTCPayServer.Tests
|
|||
//permissions
|
||||
//strict
|
||||
//selectiveStores
|
||||
//redirect
|
||||
//appidentifier
|
||||
var appidentifier = "testapp";
|
||||
var callbackUrl = tester.PayTester.ServerUri + "postredirect-callback-test";
|
||||
var authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }).ToString();
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, applicationDetails: (appidentifier, new Uri(callbackUrl))).ToString();
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
s.Driver.PageSource.Contains("kukksappname");
|
||||
Assert.Contains(appidentifier, s.Driver.PageSource);
|
||||
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("type").ToLowerInvariant());
|
||||
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.store.canmodifystoresettings")).GetAttribute("value").ToLowerInvariant());
|
||||
Assert.Equal("hidden", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("type").ToLowerInvariant());
|
||||
Assert.Equal("true", s.Driver.FindElement(By.Id("btcpay.server.canmodifyserversettings")).GetAttribute("value").ToLowerInvariant());
|
||||
Assert.DoesNotContain("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
var url = s.Driver.Url;
|
||||
IEnumerable<KeyValuePair<string, string>> results = url.Split("?").Last().Split("&")
|
||||
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
|
||||
var apiKeyRepo = s.Server.PayTester.GetService<APIKeyRepository>();
|
||||
var accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
||||
|
||||
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetBlob().Permissions);
|
||||
await TestApiAgainstAccessToken(accessToken, tester, user,
|
||||
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
||||
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true).ToString();
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, applicationDetails: (null, new Uri(callbackUrl))).ToString();
|
||||
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
Assert.DoesNotContain("kukksappname", s.Driver.PageSource);
|
||||
|
@ -150,13 +152,27 @@ namespace BTCPayServer.Tests
|
|||
s.SetCheckbox(s, "btcpay.server.canmodifyserversettings", false);
|
||||
Assert.Contains("change-store-mode", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("consent-yes")).Click();
|
||||
url = s.Driver.Url;
|
||||
results = url.Split("?").Last().Split("&")
|
||||
.Select(s1 => new KeyValuePair<string, string>(s1.Split("=")[0], s1.Split("=")[1]));
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
|
||||
await TestApiAgainstAccessToken(results.Single(pair => pair.Key == "key").Value, tester, user,
|
||||
(await apiKeyRepo.GetKey(results.Single(pair => pair.Key == "key").Value)).GetBlob().Permissions);
|
||||
accessToken = GetAccessTokenFromCallbackResult(s.Driver);
|
||||
await TestApiAgainstAccessToken(accessToken, tester, user,
|
||||
(await apiKeyRepo.GetKey(accessToken)).GetBlob().Permissions);
|
||||
|
||||
//let's test the app identifier system
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri(callbackUrl))).ToString();
|
||||
|
||||
//if it's the same, go to the confirm page
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
s.Driver.FindElement(By.Id("continue")).Click();
|
||||
Assert.Equal(callbackUrl, s.Driver.Url);
|
||||
|
||||
//same app but different redirect = nono
|
||||
authUrl = BTCPayServerClient.GenerateAuthorizeUri(tester.PayTester.ServerUri,
|
||||
new[] { Policies.CanModifyStoreSettings, Policies.CanModifyServerSettings }, false, true, (appidentifier, new Uri("https://international.local/callback"))).ToString();
|
||||
|
||||
s.Driver.Navigate().GoToUrl(authUrl);
|
||||
Assert.False(s.Driver.Url.StartsWith("https://international.com/callback"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,5 +330,12 @@ namespace BTCPayServer.Tests
|
|||
|
||||
return JsonConvert.DeserializeObject<T>(rawJson);
|
||||
}
|
||||
|
||||
private string GetAccessTokenFromCallbackResult(IWebDriver driver)
|
||||
{
|
||||
var source = driver.FindElement(By.TagName("body")).Text;
|
||||
var json = JObject.Parse(source);
|
||||
return json.GetValue("apiKey")!.Value<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,12 @@
|
|||
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
|
||||
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);ALTCOINS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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="83.0.4103.3900" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="85.0.4183.8700" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
@ -222,6 +222,9 @@ namespace BTCPayServer.Tests
|
|||
var bitpay = new MockRateProvider();
|
||||
bitpay.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETB_BTC"), new BidAsk(0.1m)));
|
||||
rateProvider.Providers.Add("bitpay", bitpay);
|
||||
var kraken = new MockRateProvider();
|
||||
kraken.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("ETH_BTC"), new BidAsk(0.1m)));
|
||||
rateProvider.Providers.Add("kraken", kraken);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class ChangellyTests
|
||||
{
|
||||
public const int TestTimeout = 60_000;
|
||||
public ChangellyTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.CurrentStore.GetStoreBlob();
|
||||
Assert.Null(storeBlob.ChangellySettings);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.CurrentStore.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.NotNull(storeBlob.ChangellySettings);
|
||||
Assert.IsType<ChangellySettings>(storeBlob.ChangellySettings);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiKey, updateModel.ApiKey);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiSecret,
|
||||
updateModel.ApiSecret);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ApiUrl, updateModel.ApiUrl);
|
||||
Assert.Equal(storeBlob.ChangellySettings.ChangellyMerchantId,
|
||||
updateModel.ChangellyMerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanToggleChangellyPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiSecret = "secret",
|
||||
ApiKey = "key",
|
||||
ApiUrl = "http://gozo.com",
|
||||
ChangellyMerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().ChangellySettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CannotUseChangellyApiWithoutChangellyPaymentMethodSet()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var changellyController =
|
||||
tester.PayTester.GetController<ChangellyController>(user.UserId, user.StoreId);
|
||||
changellyController.IsTest = true;
|
||||
|
||||
//test non existing payment method
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
var updateModel = CreateDefaultChangellyParams(false);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
//set payment method but disabled
|
||||
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsType<BitpayErrorModel>(Assert
|
||||
.IsType<BadRequestObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
|
||||
updateModel.Enabled = true;
|
||||
//test with enabled method
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
Assert.IsNotType<BitpayErrorModel>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateChangellySettingsViewModel CreateDefaultChangellyParams(bool enabled)
|
||||
{
|
||||
return new UpdateChangellySettingsViewModel()
|
||||
{
|
||||
ApiKey = "6ed02cdf1b614d89a8c0ceb170eebb61",
|
||||
ApiSecret = "8fbd66a2af5fd15a6b5f8ed0159c5842e32a18538521ffa145bd6c9e124d3483",
|
||||
ChangellyMerchantId = "804298eb5753",
|
||||
Enabled = enabled
|
||||
};
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanGetCurrencyListFromChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
//save changelly settings
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
//confirm saved
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = TestUtils.CreateHttpFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
var result = Assert
|
||||
.IsType<OkObjectResult>(await changellyController.GetCurrencyList(user.StoreId))
|
||||
.Value as IEnumerable<CurrencyFull>;
|
||||
Assert.True(result.Any());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanCalculateToAmountForChangelly()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
var updateModel = CreateDefaultChangellyParams(true);
|
||||
var storesController = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await storesController.UpdateChangellySettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var factory = UnitTest1.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var httpClientFactory = TestUtils.CreateHttpFactory();
|
||||
var changellyController = new ChangellyController(
|
||||
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
|
||||
tester.NetworkProvider, fetcher);
|
||||
changellyController.IsTest = true;
|
||||
Assert.IsType<decimal>(Assert
|
||||
.IsType<OkObjectResult>(await changellyController.CalculateAmount(user.StoreId, "ltc", "btc", 1.0m, default))
|
||||
.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanComputeBaseAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeBaseAmount(1, 1));
|
||||
Assert.Equal(0.5m, ChangellyCalculationHelper.ComputeBaseAmount(1, 0.5m));
|
||||
Assert.Equal(2, ChangellyCalculationHelper.ComputeBaseAmount(0.5m, 1));
|
||||
Assert.Equal(4m, ChangellyCalculationHelper.ComputeBaseAmount(1, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanComputeCorrectAmount()
|
||||
{
|
||||
Assert.Equal(1, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 2));
|
||||
Assert.Equal(0.25m, ChangellyCalculationHelper.ComputeCorrectAmount(0.5m, 1, 0.5m));
|
||||
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ using BTCPayServer.Client.Models;
|
|||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -17,6 +19,7 @@ using NBitcoin;
|
|||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUglify.Helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using CreateApplicationUserRequest = BTCPayServer.Client.Models.CreateApplicationUserRequest;
|
||||
|
@ -145,10 +148,13 @@ namespace BTCPayServer.Tests
|
|||
// We have no admin, so it should work
|
||||
var user1 = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() { Email = "test@gmail.com", Password = "abceudhqw" });
|
||||
Assert.Empty(user1.Roles);
|
||||
|
||||
// We have no admin, so it should work
|
||||
var user2 = await unauthClient.CreateUser(
|
||||
new CreateApplicationUserRequest() { Email = "test2@gmail.com", Password = "abceudhqw" });
|
||||
|
||||
Assert.Empty(user2.Roles);
|
||||
|
||||
// Duplicate email
|
||||
await AssertValidationError(new[] { "Email" },
|
||||
async () => await unauthClient.CreateUser(
|
||||
|
@ -161,7 +167,8 @@ namespace BTCPayServer.Tests
|
|||
Password = "abceudhqw",
|
||||
IsAdministrator = true
|
||||
});
|
||||
|
||||
Assert.Contains("ServerAdmin", admin.Roles);
|
||||
|
||||
// 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,
|
||||
|
@ -557,6 +564,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.NotNull(apiKeyProfileUserData);
|
||||
Assert.Equal(apiKeyProfileUserData.Id, user.UserId);
|
||||
Assert.Equal(apiKeyProfileUserData.Email, user.RegisterDetails.Email);
|
||||
Assert.Contains("ServerAdmin", apiKeyProfileUserData.Roles);
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(async () => await clientInsufficient.GetCurrentUser());
|
||||
await clientServer.GetCurrentUser();
|
||||
|
@ -732,6 +740,237 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceLegacyTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var oldBitpay = user.BitPay;
|
||||
|
||||
Logs.Tester.LogInformation("Let's create an invoice with bitpay API");
|
||||
var oldInvoice = await oldBitpay.CreateInvoiceAsync(new Invoice()
|
||||
{
|
||||
Currency = "BTC",
|
||||
Price = 1000.19392922m,
|
||||
BuyerAddress1 = "blah",
|
||||
Buyer = new Buyer()
|
||||
{
|
||||
Address2 = "blah2"
|
||||
},
|
||||
ItemCode = "code",
|
||||
ItemDesc = "desc",
|
||||
OrderId = "orderId",
|
||||
PosData = "posData"
|
||||
});
|
||||
|
||||
async Task<Client.Models.InvoiceData> AssertInvoiceMetadata()
|
||||
{
|
||||
Logs.Tester.LogInformation("Let's check if we can get invoice in the new format with the metadata");
|
||||
var newInvoice = await client.GetInvoice(user.StoreId, oldInvoice.Id);
|
||||
Assert.Equal("posData", newInvoice.Metadata["posData"].Value<string>());
|
||||
Assert.Equal("code", newInvoice.Metadata["itemCode"].Value<string>());
|
||||
Assert.Equal("desc", newInvoice.Metadata["itemDesc"].Value<string>());
|
||||
Assert.Equal("orderId", newInvoice.Metadata["orderId"].Value<string>());
|
||||
Assert.False(newInvoice.Metadata["physical"].Value<bool>());
|
||||
Assert.Null(newInvoice.Metadata["buyerCountry"]);
|
||||
Assert.Equal(1000.19392922m, newInvoice.Amount);
|
||||
Assert.Equal("BTC", newInvoice.Currency);
|
||||
return newInvoice;
|
||||
}
|
||||
|
||||
await AssertInvoiceMetadata();
|
||||
|
||||
Logs.Tester.LogInformation("Let's hack the Bitpay created invoice to be just like before this update. (Invoice V1)");
|
||||
var invoiceV1 = "{\r\n \"version\": 1,\r\n \"id\": \"" + oldInvoice.Id + "\",\r\n \"storeId\": \"" + user.StoreId + "\",\r\n \"orderId\": \"orderId\",\r\n \"speedPolicy\": 1,\r\n \"rate\": 1.0,\r\n \"invoiceTime\": 1598329634,\r\n \"expirationTime\": 1598330534,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\",\r\n \"productInformation\": {\r\n \"itemDesc\": \"desc\",\r\n \"itemCode\": \"code\",\r\n \"physical\": false,\r\n \"price\": 1000.19392922,\r\n \"currency\": \"BTC\"\r\n },\r\n \"buyerInformation\": {\r\n \"buyerName\": null,\r\n \"buyerEmail\": null,\r\n \"buyerCountry\": null,\r\n \"buyerZip\": null,\r\n \"buyerState\": null,\r\n \"buyerCity\": null,\r\n \"buyerAddress2\": \"blah2\",\r\n \"buyerAddress1\": \"blah\",\r\n \"buyerPhone\": null\r\n },\r\n \"posData\": \"posData\",\r\n \"internalTags\": [],\r\n \"derivationStrategy\": null,\r\n \"derivationStrategies\": \"{\\\"BTC\\\":{\\\"signingKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\",\\\"source\\\":\\\"NBXplorer\\\",\\\"accountDerivation\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf-[legacy]\\\",\\\"accountOriginal\\\":null,\\\"accountKeySettings\\\":[{\\\"rootFingerprint\\\":\\\"54d5044d\\\",\\\"accountKeyPath\\\":\\\"44'/1'/0'\\\",\\\"accountKey\\\":\\\"tpubDD1AW2ruUxSsDa55NQYtNt7DQw9bqXx4K7r2aScySmjxHtsCZoxFTN3qCMcKLxgsRDMGSwk9qj1fBfi8jqSLenwyYkhDrmgaxQuvuKrTHEf\\\"}],\\\"label\\\":null}}\",\r\n \"status\": \"new\",\r\n \"exceptionStatus\": \"\",\r\n \"payments\": [],\r\n \"refundable\": false,\r\n \"refundMail\": null,\r\n \"redirectURL\": null,\r\n \"redirectAutomatically\": false,\r\n \"txFee\": 0,\r\n \"fullNotifications\": false,\r\n \"notificationEmail\": null,\r\n \"notificationURL\": null,\r\n \"serverUrl\": \"http://127.0.0.1:8001\",\r\n \"cryptoData\": {\r\n \"BTC\": {\r\n \"rate\": 1.0,\r\n \"paymentMethod\": {\r\n \"networkFeeMode\": 0,\r\n \"networkFeeRate\": 100.0,\r\n \"payjoinEnabled\": false\r\n },\r\n \"feeRate\": 100.0,\r\n \"txFee\": 0,\r\n \"depositAddress\": \"mm83rVs8ZnZok1SkRBmXiwQSiPFgTgCKpD\"\r\n }\r\n },\r\n \"monitoringExpiration\": 1598416934,\r\n \"historicalAddresses\": null,\r\n \"availableAddressHashes\": null,\r\n \"extendedNotifications\": false,\r\n \"events\": null,\r\n \"paymentTolerance\": 0.0,\r\n \"archived\": false\r\n}";
|
||||
var db = tester.PayTester.GetService<Data.ApplicationDbContextFactory>();
|
||||
using var ctx = db.CreateContext();
|
||||
var dbInvoice = await ctx.Invoices.FindAsync(oldInvoice.Id);
|
||||
dbInvoice.Blob = ZipUtils.Zip(invoiceV1);
|
||||
await ctx.SaveChangesAsync();
|
||||
var newInvoice = await AssertInvoiceMetadata();
|
||||
|
||||
Logs.Tester.LogInformation("Now, let's create an invoice with the new API but with the same metadata as Bitpay");
|
||||
newInvoice.Metadata.Add("lol", "lol");
|
||||
newInvoice = await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
|
||||
{
|
||||
Metadata = newInvoice.Metadata,
|
||||
Amount = 1000.19392922m,
|
||||
Currency = "BTC"
|
||||
});
|
||||
oldInvoice = await oldBitpay.GetInvoiceAsync(newInvoice.Id);
|
||||
await AssertInvoiceMetadata();
|
||||
Assert.Equal("lol", newInvoice.Metadata["lol"].Value<string>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task InvoiceTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var viewOnly = await user.CreateClient(Policies.CanViewInvoices);
|
||||
|
||||
//create
|
||||
|
||||
//validation errors
|
||||
await AssertValidationError(new[] { nameof(CreateInvoiceRequest.Currency), nameof(CreateInvoiceRequest.Amount), $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentTolerance)}", $"{nameof(CreateInvoiceRequest.Checkout)}.{nameof(CreateInvoiceRequest.Checkout.PaymentMethods)}[0]" }, async () =>
|
||||
{
|
||||
await client.CreateInvoice(user.StoreId, new CreateInvoiceRequest() { Amount = -1, Checkout = new CreateInvoiceRequest.CheckoutOptions() { PaymentTolerance = -2, PaymentMethods = new[] { "jasaas_sdsad" } } });
|
||||
});
|
||||
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "helloinvalid", Amount = 1 });
|
||||
});
|
||||
await user.RegisterDerivationSchemeAsync("BTC");
|
||||
var newInvoice = await client.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest() { Currency = "USD", Amount = 1, Metadata = JObject.Parse("{\"itemCode\": \"testitem\"}") });
|
||||
|
||||
//list
|
||||
var invoices = await viewOnly.GetInvoices(user.StoreId);
|
||||
|
||||
Assert.NotNull(invoices);
|
||||
Assert.Single(invoices);
|
||||
Assert.Equal(newInvoice.Id, invoices.First().Id);
|
||||
|
||||
//get payment request
|
||||
var invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
Assert.Equal(newInvoice.Metadata, invoice.Metadata);
|
||||
|
||||
//update
|
||||
invoice = await viewOnly.GetInvoice(user.StoreId, newInvoice.Id);
|
||||
|
||||
await AssertValidationError(new[] { nameof(MarkInvoiceStatusRequest.Status) }, async () =>
|
||||
{
|
||||
await client.MarkInvoiceStatus(user.StoreId, invoice.Id, new MarkInvoiceStatusRequest()
|
||||
{
|
||||
Status = InvoiceStatus.Complete
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//archive
|
||||
await AssertHttpError(403, async () =>
|
||||
{
|
||||
await viewOnly.ArchiveInvoice(user.StoreId, invoice.Id);
|
||||
});
|
||||
|
||||
await client.ArchiveInvoice(user.StoreId, invoice.Id);
|
||||
Assert.DoesNotContain(invoice.Id,
|
||||
(await client.GetInvoices(user.StoreId)).Select(data => data.Id));
|
||||
|
||||
//unarchive
|
||||
await client.UnarchiveInvoice(user.StoreId, invoice.Id);
|
||||
Assert.NotNull(await client.GetInvoice(user.StoreId, invoice.Id));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[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($"{Policies.CanUseLightningNodeInStore}:{merchant.StoreId}");
|
||||
var merchantInvoice = await merchantClient.CreateLightningInvoice(merchant.StoreId, "BTC", new CreateLightningInvoiceRequest(new LightMoney(1_000), "hey", TimeSpan.FromSeconds(60)));
|
||||
tester.PayTester.GetService<BTCPayServerEnvironment>().DevelopmentOverride = false;
|
||||
// The default client is using charge, so we should not be able to query channels
|
||||
var client = await user.CreateClient(Policies.CanUseInternalLightningNode);
|
||||
|
||||
var info = await client.GetLightningNodeInfo("BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
|
||||
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($"{Policies.CanUseLightningNodeInStore}:{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);
|
||||
|
||||
info = await client.GetLightningNodeInfo(user.StoreId, "BTC");
|
||||
Assert.Single(info.NodeURIs);
|
||||
Assert.NotEqual(0, info.BlockHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void NumericJsonConverterTests()
|
||||
|
@ -762,10 +1001,11 @@ namespace BTCPayServer.Tests
|
|||
Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(decimal), null, null);
|
||||
});Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(double), null, null);
|
||||
});
|
||||
Assert.Throws<JsonSerializationException>(() =>
|
||||
{
|
||||
jsonConverter.ReadJson(Get("null"), typeof(double), 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));
|
||||
Assert.Equal(1.2, jsonConverter.ReadJson(Get(stringJson), typeof(double), null, null));
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.PaymentRequest;
|
||||
|
|
|
@ -308,8 +308,9 @@ namespace BTCPayServer.Tests
|
|||
currencyEl.Clear();
|
||||
currencyEl.SendKeys(currency);
|
||||
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
|
||||
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName + Keys.Enter);
|
||||
Driver.FindElement(By.Id("Create")).ForceClick();
|
||||
Driver.FindElement(By.Name("StoreId")).SendKeys(storeName);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
|
||||
Assert.True(Driver.PageSource.Contains("just created!"), "Unable to create Invoice");
|
||||
var statusElement = Driver.FindElement(By.ClassName("alert-success"));
|
||||
var id = statusElement.Text.Split(" ")[1];
|
||||
|
|
|
@ -78,6 +78,11 @@ namespace BTCPayServer.Tests
|
|||
PayTester.Chains.Add("LBTC");
|
||||
PayTester.LBTCNBXplorerUri = LBTCExplorerClient.Address;
|
||||
}
|
||||
public void ActivateETH()
|
||||
{
|
||||
PayTester.Chains.Add("ETH");
|
||||
}
|
||||
|
||||
#endif
|
||||
public void ActivateLightning()
|
||||
{
|
||||
|
|
|
@ -151,6 +151,36 @@ namespace BTCPayServer.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanParsePaymentMethodId()
|
||||
{
|
||||
var id = PaymentMethodId.Parse("BTC");
|
||||
var id1 = PaymentMethodId.Parse("BTC-OnChain");
|
||||
var id2 = PaymentMethodId.Parse("BTC-BTCLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal(id, id2);
|
||||
Assert.Equal("BTC", id.ToString());
|
||||
Assert.Equal("BTC", id.ToString());
|
||||
id = PaymentMethodId.Parse("LTC");
|
||||
Assert.Equal("LTC", id.ToString());
|
||||
Assert.Equal("LTC", id.ToStringNormalized());
|
||||
id = PaymentMethodId.Parse("LTC-offchain");
|
||||
id1 = PaymentMethodId.Parse("LTC-OffChain");
|
||||
id2 = PaymentMethodId.Parse("LTC-LightningLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal(id, id2);
|
||||
Assert.Equal("LTC_LightningLike", id.ToString());
|
||||
Assert.Equal("LTC-LightningNetwork", id.ToStringNormalized());
|
||||
#if ALTCOINS
|
||||
id = PaymentMethodId.Parse("XMR");
|
||||
id1 = PaymentMethodId.Parse("XMR-MoneroLike");
|
||||
Assert.Equal(id, id1);
|
||||
Assert.Equal("XMR_MoneroLike", id.ToString());
|
||||
Assert.Equal("XMR", id.ToStringNormalized());
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public async Task CheckNoDeadLink()
|
||||
|
@ -325,7 +355,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
#if ALTCOINS
|
||||
[Fact]
|
||||
[Trait("Fast", "Fast")]
|
||||
public void CanCalculateCryptoDue()
|
||||
|
@ -346,7 +376,7 @@ namespace BTCPayServer.Tests
|
|||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods().TryGet("BTC", PaymentTypes.BTCLike);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
@ -396,7 +426,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
entity = new InvoiceEntity();
|
||||
entity.Networks = networkProvider;
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(
|
||||
new PaymentMethod() { CryptoCode = "BTC", Rate = 1000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
|
@ -490,7 +520,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
#endif
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUseTestWebsiteUI()
|
||||
|
@ -545,7 +575,7 @@ namespace BTCPayServer.Tests
|
|||
Rate = 5000,
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.Price = 5000;
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
|
||||
|
@ -806,92 +836,6 @@ namespace BTCPayServer.Tests
|
|||
.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
[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()
|
||||
|
|
|
@ -81,7 +81,7 @@ services:
|
|||
- customer_lnd
|
||||
- merchant_lnd
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.1.40
|
||||
image: nicolasdorier/nbxplorer:2.1.42
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -265,7 +265,7 @@ services:
|
|||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.10.2-beta
|
||||
image: btcpayserver/lnd:v0.11.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -295,7 +295,7 @@ services:
|
|||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.10.2-beta
|
||||
image: btcpayserver/lnd:v0.11.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
|
|
@ -78,7 +78,7 @@ services:
|
|||
- customer_lnd
|
||||
- merchant_lnd
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.1.40
|
||||
image: nicolasdorier/nbxplorer:2.1.42
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
|
@ -207,7 +207,7 @@ services:
|
|||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.10.2-beta
|
||||
image: btcpayserver/lnd:v0.11.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -237,7 +237,7 @@ services:
|
|||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.10.2-beta
|
||||
image: btcpayserver/lnd:v0.11.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<Content Remove="Views\Shared\Ethereum\**\*" />
|
||||
<Content Remove="Views\Shared\Monero\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="1.1.3" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.4" />
|
||||
|
@ -246,5 +246,5 @@
|
|||
<_ContentIncludedByDefault Remove="Views\Components\NotificationsDropdown\Default.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" wwwroot_4swagger_4v1_4swagger_1template_1pull-payments_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>
|
||||
<ProjectExtensions><VisualStudio><UserProperties wwwroot_4swagger_4v1_4swagger_1template_1invoices_1json__JsonSchema="https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json" 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_1pull-payments_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>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@model BasePagingViewModel
|
||||
|
||||
<nav aria-label="..." class="w-100">
|
||||
@if (Model.Total != 0)
|
||||
{
|
||||
@if (Model.Total > 0)
|
||||
{
|
||||
<nav aria-label="..." class="w-100">
|
||||
<ul class="pagination float-left">
|
||||
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
|
||||
<a class="page-link" tabindex="-1" href="@NavigatePages(-1, Model.Count)">«</a>
|
||||
|
@ -25,25 +25,38 @@
|
|||
<a class="page-link" href="@NavigatePages(1, Model.Count)">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
<ul class="pagination float-right">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size:</span>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 50 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 50)">50</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 100 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 100)">100</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 250 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 250)">250</a>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 500 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 500)">500</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@if (Model.Total >= 50)
|
||||
{
|
||||
<ul class="pagination float-right">
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Page Size:</span>
|
||||
</li>
|
||||
<li class="page-item @(Model.Count == 50 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 50)">50</a>
|
||||
</li>
|
||||
@if (Model.Total >= 100)
|
||||
{
|
||||
<li class="page-item @(Model.Count == 100 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 100)">100</a>
|
||||
</li>
|
||||
}
|
||||
@if (Model.Total >= 250)
|
||||
{
|
||||
<li class="page-item @(Model.Count == 250 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 250)">250</a>
|
||||
</li>
|
||||
}
|
||||
@if (Model.Total >= 500)
|
||||
{
|
||||
<li class="page-item @(Model.Count == 500 ? "active" : null)">
|
||||
<a class="page-link" href="@NavigatePages(0, 500)">500</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</nav>
|
||||
}
|
||||
@{
|
||||
string NavigatePages(int prevNext, int count)
|
||||
{
|
||||
|
|
|
@ -93,6 +93,7 @@ namespace BTCPayServer.Configuration
|
|||
var filtered = networkProvider.Filter(supportedChains.ToArray());
|
||||
#if ALTCOINS
|
||||
supportedChains.AddRange(filtered.GetAllElementsSubChains(networkProvider));
|
||||
supportedChains.AddRange(filtered.GetAllEthereumSubChains(networkProvider));
|
||||
#endif
|
||||
#if !ALTCOINS
|
||||
var onlyBTC = supportedChains.Count == 1 && supportedChains.First() == "BTC";
|
||||
|
|
|
@ -175,7 +175,7 @@ namespace BTCPayServer.Controllers
|
|||
var store = await _AppService.GetStore(app);
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
|
@ -317,7 +317,7 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
OrderId = AppService.GetCrowdfundOrderId(appId),
|
||||
Currency = settings.TargetCurrency,
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("[controller]/{storeId}")]
|
||||
public class ChangellyController : Controller
|
||||
{
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly RateFetcher _RateProviderFactory;
|
||||
|
||||
public ChangellyController(ChangellyClientProvider changellyClientProvider,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
RateFetcher rateProviderFactory)
|
||||
{
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("currencies")]
|
||||
public async Task<IActionResult> GetCurrencyList(string storeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
return Ok(await client.GetCurrenciesFull());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("calculate")]
|
||||
public async Task<IActionResult> CalculateAmount(string storeId, string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = await TryGetChangellyClient(storeId);
|
||||
|
||||
if (fromCurrency.Equals("usd", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| fromCurrency.Equals("eur", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return await HandleCalculateFiatAmount(fromCurrency, toCurrency, toCurrencyAmount, cancellationToken);
|
||||
}
|
||||
|
||||
var callCounter = 0;
|
||||
var baseRate = await client.GetExchangeAmount(fromCurrency, toCurrency, 1);
|
||||
var currentAmount = ChangellyCalculationHelper.ComputeBaseAmount(baseRate, toCurrencyAmount);
|
||||
while (true)
|
||||
{
|
||||
if (callCounter > 10)
|
||||
{
|
||||
BadRequest();
|
||||
}
|
||||
|
||||
var computedAmount = await client.GetExchangeAmount(fromCurrency, toCurrency, currentAmount);
|
||||
callCounter++;
|
||||
if (computedAmount < toCurrencyAmount)
|
||||
{
|
||||
currentAmount =
|
||||
ChangellyCalculationHelper.ComputeCorrectAmount(currentAmount, computedAmount,
|
||||
toCurrencyAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Ok(currentAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest(new BitpayErrorModel()
|
||||
{
|
||||
Error = e.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Changelly> TryGetChangellyClient(string storeId)
|
||||
{
|
||||
var store = IsTest ? null : HttpContext.GetStoreData();
|
||||
storeId = storeId ?? store?.Id;
|
||||
|
||||
return await _changellyClientProvider.TryGetChangellyClient(storeId, store);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> HandleCalculateFiatAmount(string fromCurrency, string toCurrency,
|
||||
decimal toCurrencyAmount, CancellationToken cancellationToken)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
var rules = store.GetStoreBlob().GetRateRules(_btcPayNetworkProvider);
|
||||
var rate = await _RateProviderFactory.FetchRate(new CurrencyPair(toCurrency, fromCurrency), rules, cancellationToken);
|
||||
if (rate.BidAsk == null)
|
||||
return BadRequest();
|
||||
var flatRate = rate.BidAsk.Center;
|
||||
return Ok(flatRate * toCurrencyAmount);
|
||||
}
|
||||
|
||||
public bool IsTest { get; set; } = false;
|
||||
}
|
||||
|
||||
|
||||
}
|
242
BTCPayServer/Controllers/GreenField/InvoiceController.cs
Normal file
242
BTCPayServer/Controllers/GreenField/InvoiceController.cs
Normal file
|
@ -0,0 +1,242 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CreateInvoiceRequest = BTCPayServer.Client.Models.CreateInvoiceRequest;
|
||||
using InvoiceData = BTCPayServer.Client.Models.InvoiceData;
|
||||
|
||||
namespace BTCPayServer.Controllers.GreenField
|
||||
{
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
public class GreenFieldInvoiceController : Controller
|
||||
{
|
||||
private readonly InvoiceController _invoiceController;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
|
||||
public GreenFieldInvoiceController(InvoiceController invoiceController, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_invoiceController = invoiceController;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> GetInvoices(string storeId, bool includeArchived = false)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoices =
|
||||
await _invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = new[] { store.Id },
|
||||
IncludeArchived = includeArchived
|
||||
});
|
||||
|
||||
return Ok(invoices.Select(ToModel));
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = Policies.CanViewInvoices,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
|
||||
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanCreateInvoice,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices")]
|
||||
public async Task<IActionResult> CreateInvoice(string storeId, CreateInvoiceRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.Amount < 0.0m)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.Currency))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Currency), "Currency is required");
|
||||
}
|
||||
|
||||
if (request.Checkout.PaymentMethods?.Any() is true)
|
||||
{
|
||||
for (int i = 0; i < request.Checkout.PaymentMethods.Length; i++)
|
||||
{
|
||||
if (!PaymentMethodId.TryParse(request.Checkout.PaymentMethods[i], out _))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentMethods[i],
|
||||
"Invalid payment method", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (request.Checkout.Expiration != null && request.Checkout.Expiration < TimeSpan.FromSeconds(30.0))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.Expiration,
|
||||
"Expiration time must be at least 30 seconds", this);
|
||||
}
|
||||
|
||||
if (request.Checkout.PaymentTolerance != null &&
|
||||
(request.Checkout.PaymentTolerance < 0 || request.Checkout.PaymentTolerance > 100))
|
||||
{
|
||||
request.AddModelError(invoiceRequest => invoiceRequest.Checkout.PaymentTolerance,
|
||||
"PaymentTolerance can only be between 0 and 100 percent", this);
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _invoiceController.CreateInvoiceCoreRaw(request, store,
|
||||
Request.GetAbsoluteUri(""));
|
||||
return Ok(ToModel(invoice));
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
{
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/status")]
|
||||
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
|
||||
MarkInvoiceStatusRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Status),
|
||||
"Status can only be marked to invalid or complete within certain conditions.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
return await GetInvoice(storeId, invoiceId);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings,
|
||||
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
|
||||
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
|
||||
if (invoice.StoreId != store.Id)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!invoice.Archived)
|
||||
{
|
||||
return this.CreateAPIError("already-unarchived", "Invoice is already unarchived");
|
||||
}
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
|
||||
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, false, storeId);
|
||||
return await GetInvoice(storeId, invoiceId);
|
||||
}
|
||||
|
||||
|
||||
private InvoiceData ToModel(InvoiceEntity entity)
|
||||
{
|
||||
return new InvoiceData()
|
||||
{
|
||||
ExpirationTime = entity.ExpirationTime,
|
||||
MonitoringExpiration = entity.MonitoringExpiration,
|
||||
CreatedTime = entity.InvoiceTime,
|
||||
Amount = entity.Price,
|
||||
Id = entity.Id,
|
||||
Status = entity.Status,
|
||||
AdditionalStatus = entity.ExceptionStatus,
|
||||
Currency = entity.Currency,
|
||||
Metadata = entity.Metadata.ToJObject(),
|
||||
Checkout = new CreateInvoiceRequest.CheckoutOptions()
|
||||
{
|
||||
Expiration = entity.ExpirationTime - entity.InvoiceTime,
|
||||
Monitoring = entity.MonitoringExpiration - entity.ExpirationTime,
|
||||
PaymentTolerance = entity.PaymentTolerance,
|
||||
PaymentMethods =
|
||||
entity.GetPaymentMethods().Select(method => method.GetId().ToStringNormalized()).ToArray(),
|
||||
SpeedPolicy = entity.SpeedPolicy
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,10 +32,10 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
|
||||
[Authorize(Policy = Policies.CanViewPaymentRequests, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores/{storeId}/payment-requests")]
|
||||
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId)
|
||||
public async Task<ActionResult<IEnumerable<PaymentRequestData>>> GetPaymentRequests(string storeId, bool includeArchived = false)
|
||||
{
|
||||
var prs = await _paymentRequestRepository.FindPaymentRequests(
|
||||
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = false });
|
||||
new PaymentRequestQuery() { StoreId = storeId, IncludeArchived = includeArchived });
|
||||
return Ok(prs.Items.Select(FromModel));
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,6 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
//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
|
||||
|
@ -124,8 +123,8 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
RecommendedFeeBlockTarget = storeBlob.RecommendedFeeBlockTarget,
|
||||
DefaultLang = storeBlob.DefaultLang,
|
||||
MonitoringExpiration = TimeSpan.FromMinutes(storeBlob.MonitoringExpiration),
|
||||
InvoiceExpiration = TimeSpan.FromMinutes(storeBlob.InvoiceExpiration),
|
||||
MonitoringExpiration = storeBlob.MonitoringExpiration,
|
||||
InvoiceExpiration = storeBlob.InvoiceExpiration,
|
||||
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
|
||||
CustomLogo = storeBlob.CustomLogo,
|
||||
CustomCSS = storeBlob.CustomCSS,
|
||||
|
@ -150,7 +149,6 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
//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
|
||||
|
@ -160,8 +158,8 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
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.MonitoringExpiration = restModel.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = restModel.InvoiceExpiration;
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.CustomLogo = restModel.CustomLogo;
|
||||
blob.CustomCSS = restModel.CustomCSS;
|
||||
|
|
|
@ -58,7 +58,7 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
public async Task<ActionResult<ApplicationUserData>> GetCurrentUser()
|
||||
{
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
return FromModel(user);
|
||||
return await FromModel(user);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -152,17 +152,20 @@ namespace BTCPayServer.Controllers.GreenField
|
|||
}
|
||||
}
|
||||
_eventAggregator.Publish(new UserRegisteredEvent() { RequestUri = Request.GetAbsoluteRootUri(), User = user, Admin = request.IsAdministrator is true });
|
||||
return CreatedAtAction(string.Empty, user);
|
||||
var model = await FromModel(user);
|
||||
return CreatedAtAction(string.Empty, model);
|
||||
}
|
||||
|
||||
private static ApplicationUserData FromModel(ApplicationUser data)
|
||||
private async Task<ApplicationUserData> FromModel(ApplicationUser data)
|
||||
{
|
||||
var roles = (await _userManager.GetRolesAsync(data)).ToArray();
|
||||
return new ApplicationUserData()
|
||||
{
|
||||
Id = data.Id,
|
||||
Email = data.Email,
|
||||
EmailConfirmed = data.EmailConfirmed,
|
||||
RequiresEmailConfirmation = data.RequiresEmailConfirmation
|
||||
RequiresEmailConfirmation = data.RequiresEmailConfirmation,
|
||||
Roles = roles
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -12,6 +13,7 @@ using BTCPayServer.Security;
|
|||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
@ -139,7 +141,6 @@ namespace BTCPayServer.Controllers
|
|||
return View();
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Route("translate")]
|
||||
public async Task<IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm)
|
||||
|
@ -199,6 +200,18 @@ namespace BTCPayServer.Controllers
|
|||
return View("RecoverySeedBackup", vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("postredirect-callback-test")]
|
||||
public ActionResult PostRedirectCallbackTestpage(IFormCollection data)
|
||||
{
|
||||
var list = data.Keys.Aggregate(new Dictionary<string, string>(), (res, key) =>
|
||||
{
|
||||
res.Add(key, data[key]);
|
||||
return res;
|
||||
});
|
||||
return Json(list);
|
||||
}
|
||||
|
||||
public IActionResult Error()
|
||||
{
|
||||
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace BTCPayServer.Controllers
|
|||
[HttpPost]
|
||||
[Route("invoices")]
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] CreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] BitpayCreateInvoiceRequest invoice, CancellationToken cancellationToken)
|
||||
{
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(400, "Invalid invoice");
|
||||
|
|
|
@ -11,15 +11,17 @@ using BTCPayServer.Client.Models;
|
|||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using DBriize.Utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -30,6 +32,7 @@ using NBitcoin;
|
|||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
|
@ -52,7 +55,6 @@ namespace BTCPayServer.Controllers
|
|||
if (invoice == null)
|
||||
return NotFound();
|
||||
|
||||
var prodInfo = invoice.ProductInformation;
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
var model = new InvoiceDetailsModel()
|
||||
{
|
||||
|
@ -68,16 +70,14 @@ namespace BTCPayServer.Controllers
|
|||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
MonitoringDate = invoice.MonitoringExpiration,
|
||||
OrderId = invoice.OrderId,
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
|
||||
Fiat = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.Metadata.TaxIncluded ?? 0.0m, invoice.Currency),
|
||||
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
TypedMetadata = invoice.Metadata,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
PosData = PosDataParser.ParsePosData(invoice.PosData),
|
||||
PosData = PosDataParser.ParsePosData(invoice.Metadata.PosData),
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = CanRefund(invoice.GetInvoiceState()),
|
||||
};
|
||||
|
@ -166,7 +166,6 @@ namespace BTCPayServer.Controllers
|
|||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> Refund(string invoiceId, RefundModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (invoice is null)
|
||||
|
@ -177,32 +176,45 @@ namespace BTCPayServer.Controllers
|
|||
if (!CanRefund(invoice.GetInvoiceState()))
|
||||
return NotFound();
|
||||
var paymentMethodId = new PaymentMethodId(model.SelectedPaymentMethod, PaymentTypes.BTCLike);
|
||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.ProductInformation.Currency, true);
|
||||
var cdCurrency = _CurrencyNameTable.GetCurrencyData(invoice.Currency, true);
|
||||
var paymentMethodDivisibility = _CurrencyNameTable.GetCurrencyData(paymentMethodId.CryptoCode, false)?.Divisibility ?? 8;
|
||||
if (model.SelectedRefundOption is null)
|
||||
RateRules rules;
|
||||
RateResult rateResult;
|
||||
CreatePullPayment createPullPayment;
|
||||
switch (model.RefundStep)
|
||||
{
|
||||
model.Title = "What to refund?";
|
||||
var paymentMethod = invoice.GetPaymentMethods()[paymentMethodId];
|
||||
var paidCurrency = Math.Round(paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate, cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility);
|
||||
model.RateThenText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode, true);
|
||||
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
var rateResult = await _RateProvider.FetchRate(new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.ProductInformation.Currency), rules, cancellationToken);
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), $"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
model.Title = "What to refund?";
|
||||
var paymentMethod = invoice.GetPaymentMethods()[paymentMethodId];
|
||||
var paidCurrency =
|
||||
Math.Round(paymentMethod.Calculate().Paid.ToDecimal(MoneyUnit.BTC) * paymentMethod.Rate,
|
||||
cdCurrency.Divisibility);
|
||||
model.CryptoAmountThen = Math.Round(paidCurrency / paymentMethod.Rate, paymentMethodDivisibility);
|
||||
model.RateThenText =
|
||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountThen, paymentMethodId.CryptoCode,
|
||||
true);
|
||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateResult = await _RateProvider.FetchRate(
|
||||
new Rating.CurrencyPair(paymentMethodId.CryptoCode, invoice.Currency), rules,
|
||||
cancellationToken);
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
model.CurrentRateText =
|
||||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode,
|
||||
true);
|
||||
model.FiatAmount = paidCurrency;
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency, true);
|
||||
return View(model);
|
||||
}
|
||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
model.CurrentRateText = _CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode, true);
|
||||
model.FiatAmount = paidCurrency;
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.ProductInformation.Currency, true);
|
||||
return View(model);
|
||||
}
|
||||
else
|
||||
{
|
||||
var createPullPayment = new HostedServices.CreatePullPayment();
|
||||
case RefundSteps.SelectRate:
|
||||
createPullPayment = new HostedServices.CreatePullPayment();
|
||||
createPullPayment.Name = $"Refund {invoice.Id}";
|
||||
createPullPayment.PaymentMethodIds = new[] { paymentMethodId };
|
||||
createPullPayment.StoreId = invoice.StoreId;
|
||||
|
@ -217,31 +229,79 @@ namespace BTCPayServer.Controllers
|
|||
createPullPayment.Amount = model.CryptoAmountNow;
|
||||
break;
|
||||
case "Fiat":
|
||||
createPullPayment.Currency = invoice.ProductInformation.Currency;
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = model.FiatAmount;
|
||||
break;
|
||||
case "Custom":
|
||||
model.Title = "How much to refund?";
|
||||
model.CustomCurrency = invoice.Currency;
|
||||
model.CustomAmount = model.FiatAmount;
|
||||
model.RefundStep = RefundSteps.SelectCustomAmount;
|
||||
return View(model);
|
||||
default:
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invalid choice");
|
||||
return View(model);
|
||||
}
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Html = "Share this page with a customer so they can claim a refund <br />Once claimed you need to initiate a refund from Wallet > Payouts",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
(await ctx.Invoices.FindAsync(invoice.Id)).CurrentRefundId = ppId;
|
||||
ctx.Refunds.Add(new RefundData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
PullPaymentDataId = ppId
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
// TODO: Having dedicated UI later on
|
||||
return RedirectToAction(nameof(PullPaymentController.ViewPullPayment),
|
||||
"PullPayment",
|
||||
new { pullPaymentId = ppId });
|
||||
|
||||
break;
|
||||
case RefundSteps.SelectCustomAmount:
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateResult = await _RateProvider.FetchRate(
|
||||
new Rating.CurrencyPair(paymentMethodId.CryptoCode, model.CustomCurrency), rules,
|
||||
cancellationToken);
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
createPullPayment = new HostedServices.CreatePullPayment();
|
||||
createPullPayment.Name = $"Refund {invoice.Id}";
|
||||
createPullPayment.PaymentMethodIds = new[] { paymentMethodId };
|
||||
createPullPayment.StoreId = invoice.StoreId;
|
||||
createPullPayment.Currency = model.CustomCurrency;
|
||||
createPullPayment.Amount = model.CustomAmount;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
|
||||
this.TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Html = "Share this page with a customer so they can claim a refund <br />Once claimed you need to initiate a refund from Wallet > Payouts",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
(await ctx.Invoices.FindAsync(invoice.Id)).CurrentRefundId = ppId;
|
||||
ctx.Refunds.Add(new RefundData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
PullPaymentDataId = ppId
|
||||
});
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
// TODO: Having dedicated UI later on
|
||||
return RedirectToAction(nameof(PullPaymentController.ViewPullPayment),
|
||||
"PullPayment",
|
||||
new { pullPaymentId = ppId });
|
||||
|
||||
|
||||
}
|
||||
|
||||
private InvoiceDetailsModel InvoicePopulatePayments(InvoiceEntity invoice)
|
||||
|
@ -403,13 +463,9 @@ namespace BTCPayServer.Controllers
|
|||
var dto = invoice.EntityToDTO();
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var currency = invoice.Currency;
|
||||
var accounting = paymentMethod.Calculate();
|
||||
|
||||
ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled &&
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
|
@ -417,20 +473,14 @@ namespace BTCPayServer.Controllers
|
|||
: null;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
? (accounting.Due.ToDecimal(MoneyUnit.BTC) *
|
||||
(1m + (changelly.AmountMarkupPercentage / 100m)))
|
||||
: (decimal?)null;
|
||||
|
||||
var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId];
|
||||
|
||||
var divisibility = _CurrencyNameTable.GetNumberFormatInfo(paymentMethod.GetId().CryptoCode, false)?.CurrencyDecimalDigits;
|
||||
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
RootPath = this.Request.PathBase.Value.WithTrailingSlash(),
|
||||
OrderId = invoice.OrderId,
|
||||
OrderId = invoice.Metadata.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
CustomCSSLink = storeBlob.CustomCSS,
|
||||
|
@ -440,7 +490,7 @@ namespace BTCPayServer.Controllers
|
|||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
BtcDue = accounting.Due.ShowMoney(divisibility),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ShowMoney(divisibility),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation),
|
||||
OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ShowRecommendedFee = storeBlob.ShowRecommendedFee,
|
||||
|
@ -448,7 +498,7 @@ namespace BTCPayServer.Controllers
|
|||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
ItemDesc = invoice.Metadata.ItemDesc,
|
||||
Rate = ExchangeRate(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
|
||||
RedirectAutomatically = invoice.RedirectAutomatically,
|
||||
|
@ -461,9 +511,6 @@ namespace BTCPayServer.Controllers
|
|||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
ChangellyEnabled = changelly != null,
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
|
@ -498,7 +545,7 @@ namespace BTCPayServer.Controllers
|
|||
paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob);
|
||||
if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats")
|
||||
{
|
||||
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency);
|
||||
model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.Currency);
|
||||
}
|
||||
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
|
||||
model.PaymentMethodId = paymentMethodId.ToString();
|
||||
|
@ -507,17 +554,17 @@ namespace BTCPayServer.Controllers
|
|||
return model;
|
||||
}
|
||||
|
||||
private string OrderAmountFromInvoice(string cryptoCode, ProductInformation productInformation)
|
||||
private string OrderAmountFromInvoice(string cryptoCode, InvoiceEntity invoiceEntity)
|
||||
{
|
||||
// if invoice source currency is the same as currently display currency, no need for "order amount from invoice"
|
||||
if (cryptoCode == productInformation.Currency)
|
||||
if (cryptoCode == invoiceEntity.Currency)
|
||||
return null;
|
||||
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(invoiceEntity.Price, invoiceEntity.Currency);
|
||||
}
|
||||
private string ExchangeRate(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
string currency = paymentMethod.ParentEntity.Currency;
|
||||
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
|
||||
|
@ -623,13 +670,12 @@ namespace BTCPayServer.Controllers
|
|||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
StatusString = state.ToString(),
|
||||
ShowCheckout = invoice.Status == InvoiceStatus.New,
|
||||
Date = invoice.InvoiceTime,
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
|
||||
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
|
||||
CanMarkInvalid = state.CanMarkInvalid(),
|
||||
CanMarkComplete = state.CanMarkComplete(),
|
||||
Details = InvoicePopulatePayments(invoice),
|
||||
|
@ -731,7 +777,7 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
try
|
||||
{
|
||||
var result = await CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var result = await CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Amount.Value,
|
||||
Currency = model.Currency,
|
||||
|
@ -777,14 +823,12 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 1008, InvoiceEvent.MarkedInvalid));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Invalid);
|
||||
model.StatusString = new InvoiceState("invalid", "marked").ToString();
|
||||
}
|
||||
else if (newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice, 2008, InvoiceEvent.MarkedCompleted));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Complete);
|
||||
model.StatusString = new InvoiceState("complete", "marked").ToString();
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,10 @@ using BTCPayServer.Services.Stores;
|
|||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using CreateInvoiceRequest = BTCPayServer.Models.CreateInvoiceRequest;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
|
@ -72,40 +73,37 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(BitpayCreateInvoiceRequest invoice,
|
||||
StoreData store, string serverUrl, List<string> additionalTags = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting");
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
var entity = await CreateInvoiceCoreRaw(invoice, store, serverUrl, additionalTags, cancellationToken);
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration;
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration;
|
||||
if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime)
|
||||
{
|
||||
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
||||
}
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD";
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Metadata.OrderId = invoice.OrderId;
|
||||
entity.Metadata.PosData = invoice.PosData;
|
||||
entity.ServerUrl = serverUrl;
|
||||
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
|
||||
entity.ExtendedNotifications = invoice.ExtendedNotifications;
|
||||
entity.NotificationURLTemplate = invoice.NotificationURL;
|
||||
entity.NotificationEmail = invoice.NotificationEmail;
|
||||
entity.BuyerInformation = Map<CreateInvoiceRequest, BuyerInformation>(invoice);
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
if (additionalTags != null)
|
||||
entity.InternalTags.AddRange(additionalTags);
|
||||
//Another way of passing buyer info to support
|
||||
FillBuyerInfo(invoice.Buyer, entity.BuyerInformation);
|
||||
if (entity?.BuyerInformation?.BuyerEmail != null)
|
||||
{
|
||||
if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
|
||||
}
|
||||
FillBuyerInfo(invoice, entity);
|
||||
|
||||
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
|
||||
|
||||
|
@ -120,22 +118,19 @@ namespace BTCPayServer.Controllers
|
|||
invoice.Price = Math.Max(0.0m, invoice.Price);
|
||||
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
|
||||
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
|
||||
|
||||
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);
|
||||
|
||||
entity.Metadata.ItemCode = invoice.ItemCode;
|
||||
entity.Metadata.ItemDesc = invoice.ItemDesc;
|
||||
entity.Metadata.Physical = invoice.Physical;
|
||||
entity.Metadata.TaxIncluded = invoice.TaxIncluded;
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Price = invoice.Price;
|
||||
|
||||
entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite;
|
||||
|
||||
entity.RedirectAutomatically =
|
||||
invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically);
|
||||
|
||||
entity.Status = InvoiceStatus.New;
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
|
||||
IPaymentFilter excludeFilter = null;
|
||||
if (invoice.PaymentCurrencies?.Any() is true)
|
||||
{
|
||||
invoice.SupportedTransactionCurrencies ??=
|
||||
|
@ -151,17 +146,67 @@ namespace BTCPayServer.Controllers
|
|||
var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies
|
||||
.Where(c => c.Value.Enabled)
|
||||
.Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null)
|
||||
.Where(c => c != null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)));
|
||||
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
|
||||
}
|
||||
entity.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List<string> additionalTags = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var entity = _InvoiceRepository.CreateNewInvoice();
|
||||
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
|
||||
if (invoice.Metadata != null)
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
|
||||
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
entity.Currency = invoice.Currency;
|
||||
entity.Price = invoice.Amount;
|
||||
entity.SpeedPolicy = invoice.Checkout.SpeedPolicy ?? store.SpeedPolicy;
|
||||
IPaymentFilter excludeFilter = null;
|
||||
if (invoice.Checkout.PaymentMethods != null)
|
||||
{
|
||||
var supportedTransactionCurrencies = invoice.Checkout.PaymentMethods
|
||||
.Select(c => PaymentMethodId.TryParse(c, out var p) ? p : null)
|
||||
.ToHashSet();
|
||||
excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p));
|
||||
}
|
||||
entity.PaymentTolerance = invoice.Checkout.PaymentTolerance ?? storeBlob.PaymentTolerance;
|
||||
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, cancellationToken);
|
||||
}
|
||||
|
||||
internal async Task<InvoiceEntity> CreateInvoiceCoreRaw(InvoiceEntity entity, StoreData store, IPaymentFilter invoicePaymentMethodFilter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
InvoiceLogs logs = new InvoiceLogs();
|
||||
logs.Write("Creation of invoice starting", InvoiceEventData.EventSeverity.Info);
|
||||
|
||||
var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
|
||||
if (entity.Metadata.BuyerEmail != null)
|
||||
{
|
||||
if (!EmailValidator.IsEmail(entity.Metadata.BuyerEmail))
|
||||
throw new BitpayHttpException(400, "Invalid email");
|
||||
entity.RefundMail = entity.Metadata.BuyerEmail;
|
||||
}
|
||||
entity.Status = InvoiceStatus.New;
|
||||
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
|
||||
var rules = storeBlob.GetRateRules(_NetworkProvider);
|
||||
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
|
||||
if (invoicePaymentMethodFilter != null)
|
||||
{
|
||||
excludeFilter = PaymentFilter.Or(excludeFilter,
|
||||
invoicePaymentMethodFilter);
|
||||
}
|
||||
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId))
|
||||
.Select(c => _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode))
|
||||
.Where(c => c != null))
|
||||
{
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, entity.Currency));
|
||||
//TODO: abstract
|
||||
if (storeBlob.LightningMaxValue != null)
|
||||
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, storeBlob.LightningMaxValue.Currency));
|
||||
|
@ -173,7 +218,12 @@ namespace BTCPayServer.Controllers
|
|||
var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken);
|
||||
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
|
||||
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
|
||||
// This loop ends with .ToList so we are querying all payment methods at once
|
||||
// instead of sequentially to improve response time
|
||||
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
|
||||
.Select(c =>
|
||||
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
|
||||
|
@ -183,10 +233,7 @@ namespace BTCPayServer.Controllers
|
|||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
|
||||
.ToList();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
.ToList())
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
|
@ -209,8 +256,6 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
|
||||
foreach (var app in await getAppsTaggingStore)
|
||||
{
|
||||
entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id));
|
||||
|
@ -228,13 +273,12 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return true; });
|
||||
ex.Handle(e => { logs.Write($"Error while fetching rates {ex}", InvoiceEventData.EventSeverity.Error); return true; });
|
||||
}
|
||||
await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs);
|
||||
});
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO();
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, InvoiceEvent.Created));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Task WhenAllFetched(InvoiceLogs logs, Dictionary<CurrencyPair, Task<RateResult>> fetchingByCurrencyPair)
|
||||
|
@ -242,16 +286,16 @@ namespace BTCPayServer.Controllers
|
|||
return Task.WhenAll(fetchingByCurrencyPair.Select(async pair =>
|
||||
{
|
||||
var rateResult = await pair.Value;
|
||||
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}");
|
||||
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}");
|
||||
logs.Write($"{pair.Key}: The rating rule is {rateResult.Rule}", InvoiceEventData.EventSeverity.Info);
|
||||
logs.Write($"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}", InvoiceEventData.EventSeverity.Info);
|
||||
if (rateResult.Errors.Count != 0)
|
||||
{
|
||||
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
|
||||
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})", InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
foreach (var ex in rateResult.ExchangeExceptions)
|
||||
{
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
|
||||
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})", InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
}).ToArray());
|
||||
}
|
||||
|
@ -263,7 +307,7 @@ namespace BTCPayServer.Controllers
|
|||
var logPrefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
|
||||
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.Currency)];
|
||||
if (rate.BidAsk == null)
|
||||
{
|
||||
return null;
|
||||
|
@ -287,7 +331,7 @@ namespace BTCPayServer.Controllers
|
|||
paymentMethod.Calculate().Due, supportedPaymentMethod.PaymentId);
|
||||
if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
logs.Write($"{logPrefix} {errorMessage}");
|
||||
logs.Write($"{logPrefix} {errorMessage}", InvoiceEventData.EventSeverity.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -304,11 +348,11 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})");
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})", InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})");
|
||||
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})", InvoiceEventData.EventSeverity.Error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -327,24 +371,30 @@ namespace BTCPayServer.Controllers
|
|||
return policy;
|
||||
}
|
||||
|
||||
private void FillBuyerInfo(Buyer buyer, BuyerInformation buyerInformation)
|
||||
private void FillBuyerInfo(BitpayCreateInvoiceRequest req, InvoiceEntity invoiceEntity)
|
||||
{
|
||||
var buyerInformation = invoiceEntity.Metadata;
|
||||
buyerInformation.BuyerAddress1 = req.BuyerAddress1;
|
||||
buyerInformation.BuyerAddress2 = req.BuyerAddress2;
|
||||
buyerInformation.BuyerCity = req.BuyerCity;
|
||||
buyerInformation.BuyerCountry = req.BuyerCountry;
|
||||
buyerInformation.BuyerEmail = req.BuyerEmail;
|
||||
buyerInformation.BuyerName = req.BuyerName;
|
||||
buyerInformation.BuyerPhone = req.BuyerPhone;
|
||||
buyerInformation.BuyerState = req.BuyerState;
|
||||
buyerInformation.BuyerZip = req.BuyerZip;
|
||||
var buyer = req.Buyer;
|
||||
if (buyer == null)
|
||||
return;
|
||||
buyerInformation.BuyerAddress1 = buyerInformation.BuyerAddress1 ?? buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 = buyerInformation.BuyerAddress2 ?? buyer.Address2;
|
||||
buyerInformation.BuyerCity = buyerInformation.BuyerCity ?? buyer.City;
|
||||
buyerInformation.BuyerCountry = buyerInformation.BuyerCountry ?? buyer.country;
|
||||
buyerInformation.BuyerEmail = buyerInformation.BuyerEmail ?? buyer.email;
|
||||
buyerInformation.BuyerName = buyerInformation.BuyerName ?? buyer.Name;
|
||||
buyerInformation.BuyerPhone = buyerInformation.BuyerPhone ?? buyer.phone;
|
||||
buyerInformation.BuyerState = buyerInformation.BuyerState ?? buyer.State;
|
||||
buyerInformation.BuyerZip = buyerInformation.BuyerZip ?? buyer.zip;
|
||||
}
|
||||
|
||||
private TDest Map<TFrom, TDest>(TFrom data)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<TDest>(JsonConvert.SerializeObject(data));
|
||||
buyerInformation.BuyerAddress1 ??= buyer.Address1;
|
||||
buyerInformation.BuyerAddress2 ??= buyer.Address2;
|
||||
buyerInformation.BuyerCity ??= buyer.City;
|
||||
buyerInformation.BuyerCountry ??= buyer.country;
|
||||
buyerInformation.BuyerEmail ??= buyer.email;
|
||||
buyerInformation.BuyerName ??= buyer.Name;
|
||||
buyerInformation.BuyerPhone ??= buyer.phone;
|
||||
buyerInformation.BuyerState ??= buyer.State;
|
||||
buyerInformation.BuyerZip ??= buyer.zip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,10 +38,11 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
return View("Confirm", new ConfirmModel()
|
||||
{
|
||||
Title = "Delete API Key " + (string.IsNullOrEmpty(key.Label) ? string.Empty : key.Label) + "(" + key.Id + ")",
|
||||
Description = "Any application using this api key will immediately lose access",
|
||||
Title = $"Delete API Key {(string.IsNullOrEmpty(key.Label) ? string.Empty : key.Label)}",
|
||||
DescriptionHtml = true,
|
||||
Description = $"Any application using this API key will immediately lose access: <code>{key.Id}</code>",
|
||||
Action = "Delete",
|
||||
ActionUrl = this.Url.ActionLink(nameof(RemoveAPIKeyPost), values: new { id = id })
|
||||
ActionUrl = Url.ActionLink(nameof(RemoveAPIKeyPost), values: new { id })
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -77,10 +78,10 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
return View("AddApiKey", await SetViewModelValues(new AddApiKeyViewModel()));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("~/api-keys/authorize")]
|
||||
public async Task<IActionResult> AuthorizeAPIKey(string[] permissions, string applicationName = null,
|
||||
bool strict = true, bool selectiveStores = false)
|
||||
public async Task<IActionResult> AuthorizeAPIKey(string[] permissions, string applicationName = null, Uri redirect = null,
|
||||
bool strict = true, bool selectiveStores = false, string applicationIdentifier = null)
|
||||
{
|
||||
if (!_btcPayServerEnvironment.IsSecure)
|
||||
{
|
||||
|
@ -94,14 +95,91 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
permissions ??= Array.Empty<string>();
|
||||
|
||||
var parsedPermissions = Permission.ToPermissions(permissions).GroupBy(permission => permission.Policy);
|
||||
var requestPermissions = Permission.ToPermissions(permissions);
|
||||
if (!string.IsNullOrEmpty(applicationIdentifier) && redirect != null)
|
||||
{
|
||||
//check if there is an app identifier that matches and belongs to the current user
|
||||
var keys = await _apiKeyRepository.GetKeys(new APIKeyRepository.APIKeyQuery()
|
||||
{
|
||||
UserId = new[] {_userManager.GetUserId(User)}
|
||||
});
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var blob = key.GetBlob();
|
||||
|
||||
if (blob.ApplicationIdentifier != applicationIdentifier ||
|
||||
blob.ApplicationAuthority != redirect.Authority)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//matched the identifier and authority, but we need to check if what the app is requesting in terms of permissions is enough
|
||||
var alreadyPresentPermissions = Permission.ToPermissions(blob.Permissions)
|
||||
.GroupBy(permission => permission.Policy);
|
||||
var fail = false;
|
||||
foreach (var permission in requestPermissions.GroupBy(permission => permission.Policy))
|
||||
{
|
||||
var presentPermission =
|
||||
alreadyPresentPermissions.SingleOrDefault(grouping => permission.Key == grouping.Key);
|
||||
if (strict && presentPermission == null)
|
||||
{
|
||||
fail = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Policies.IsStorePolicy(permission.Key))
|
||||
{
|
||||
if (!selectiveStores &&
|
||||
permission.Any(permission1 => !string.IsNullOrEmpty(permission1.Scope)))
|
||||
{
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message =
|
||||
"Cannot request specific store permission when selectiveStores is not enable"
|
||||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
else if (!selectiveStores && presentPermission.Any(permission1 =>
|
||||
!string.IsNullOrEmpty(permission1.Scope)))
|
||||
{
|
||||
fail = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//we have a key that is sufficient, redirect to a page to confirm that it's ok to provide this key to the app.
|
||||
return View("ConfirmAPIKey",
|
||||
new AuthorizeApiKeysViewModel()
|
||||
{
|
||||
ApiKey = key.Id,
|
||||
RedirectUrl = redirect,
|
||||
Label = applicationName,
|
||||
ApplicationName = applicationName,
|
||||
SelectiveStores = selectiveStores,
|
||||
Strict = strict,
|
||||
Permissions = string.Join(';', permissions),
|
||||
ApplicationIdentifier = applicationIdentifier
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var vm = await SetViewModelValues(new AuthorizeApiKeysViewModel()
|
||||
{
|
||||
RedirectUrl = redirect,
|
||||
Label = applicationName,
|
||||
ApplicationName = applicationName,
|
||||
SelectiveStores = selectiveStores,
|
||||
Strict = strict,
|
||||
Permissions = string.Join(';', parsedPermissions.SelectMany(grouping => grouping.Select(permission => permission.ToString())))
|
||||
Permissions = string.Join(';', requestPermissions),
|
||||
ApplicationIdentifier = applicationIdentifier
|
||||
});
|
||||
AdjustVMForAuthorization(vm);
|
||||
|
||||
|
@ -172,24 +250,52 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
switch (viewModel.Command.ToLowerInvariant())
|
||||
var command = viewModel.Command.ToLowerInvariant();
|
||||
switch (command)
|
||||
{
|
||||
case "no":
|
||||
case "cancel":
|
||||
return RedirectToAction("APIKeys");
|
||||
case "yes":
|
||||
var key = await CreateKey(viewModel);
|
||||
|
||||
case "authorize":
|
||||
case "confirm":
|
||||
var key = command == "authorize"
|
||||
? await CreateKey(viewModel, (viewModel.ApplicationIdentifier, viewModel.RedirectUrl?.Authority))
|
||||
: await _apiKeyRepository.GetKey(viewModel.ApiKey);
|
||||
|
||||
if (viewModel.RedirectUrl != null)
|
||||
{
|
||||
var permissions = key.GetBlob().Permissions;
|
||||
var redirectVm = new PostRedirectViewModel()
|
||||
{
|
||||
FormUrl = viewModel.RedirectUrl.ToString(),
|
||||
Parameters =
|
||||
{
|
||||
new KeyValuePair<string, string>("apiKey", key.Id),
|
||||
new KeyValuePair<string, string>("userId", key.UserId)
|
||||
}
|
||||
};
|
||||
foreach (var permission in permissions)
|
||||
{
|
||||
redirectVm.Parameters.Add(
|
||||
new KeyValuePair<string, string>("permissions[]", permission));
|
||||
}
|
||||
|
||||
return View("PostRedirect", redirectVm);
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Html = $"API key generated! <code class='alert-link'>{key.Id}</code>"
|
||||
});
|
||||
|
||||
return RedirectToAction("APIKeys", new { key = key.Id });
|
||||
|
||||
default:
|
||||
return View(viewModel);
|
||||
}
|
||||
|
@ -221,6 +327,7 @@ namespace BTCPayServer.Controllers
|
|||
});
|
||||
return RedirectToAction("APIKeys");
|
||||
}
|
||||
|
||||
private IActionResult HandleCommands(AddApiKeyViewModel viewModel)
|
||||
{
|
||||
if (string.IsNullOrEmpty(viewModel.Command))
|
||||
|
@ -241,7 +348,6 @@ namespace BTCPayServer.Controllers
|
|||
switch (command)
|
||||
{
|
||||
case "change-store-mode":
|
||||
|
||||
permissionValueItem.StoreMode = permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
|
||||
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
|
||||
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
|
||||
|
@ -252,6 +358,7 @@ namespace BTCPayServer.Controllers
|
|||
permissionValueItem.SpecificStores.Add(null);
|
||||
}
|
||||
return View(viewModel);
|
||||
|
||||
case "add-store":
|
||||
permissionValueItem.SpecificStores.Add(null);
|
||||
return View(viewModel);
|
||||
|
@ -268,18 +375,20 @@ namespace BTCPayServer.Controllers
|
|||
return null;
|
||||
}
|
||||
|
||||
private async Task<APIKeyData> CreateKey(AddApiKeyViewModel viewModel)
|
||||
private async Task<APIKeyData> CreateKey(AddApiKeyViewModel viewModel, (string appIdentifier, string appAuthority) app = default)
|
||||
{
|
||||
var key = new APIKeyData()
|
||||
{
|
||||
Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)),
|
||||
Type = APIKeyType.Permanent,
|
||||
UserId = _userManager.GetUserId(User),
|
||||
Label = viewModel.Label
|
||||
Label = viewModel.Label,
|
||||
};
|
||||
key.SetBlob(new APIKeyBlob()
|
||||
{
|
||||
Permissions = GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray()
|
||||
Permissions = GetPermissionsFromViewModel(viewModel).Select(p => p.ToString()).Distinct().ToArray(),
|
||||
ApplicationAuthority = app.appAuthority,
|
||||
ApplicationIdentifier = app.appIdentifier
|
||||
});
|
||||
await _apiKeyRepository.CreateKey(key);
|
||||
return key;
|
||||
|
@ -363,6 +472,8 @@ namespace BTCPayServer.Controllers
|
|||
{BTCPayServer.Client.Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
|
||||
{BTCPayServer.Client.Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view invoices on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")},
|
||||
{$"{BTCPayServer.Client.Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")},
|
||||
{BTCPayServer.Client.Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")},
|
||||
|
@ -400,12 +511,14 @@ namespace BTCPayServer.Controllers
|
|||
public class AuthorizeApiKeysViewModel : AddApiKeyViewModel
|
||||
{
|
||||
public string ApplicationName { get; set; }
|
||||
public string ApplicationIdentifier { get; set; }
|
||||
public Uri RedirectUrl { get; set; }
|
||||
public bool Strict { get; set; }
|
||||
public bool SelectiveStores { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class ApiKeysViewModel
|
||||
{
|
||||
public List<APIKeyData> ApiKeyDatas { get; set; }
|
||||
|
|
|
@ -87,7 +87,18 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
return new EmptyResult();
|
||||
}
|
||||
#if DEBUG
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateJunk(int x = 100)
|
||||
{
|
||||
for (int i = 0; i < x; i++)
|
||||
{
|
||||
await _notificationSender.SendNotification(new AdminScope(), new JunkNotification());
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
#endif
|
||||
[HttpGet]
|
||||
public IActionResult Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
|
@ -99,9 +110,9 @@ namespace BTCPayServer.Controllers
|
|||
Skip = skip,
|
||||
Count = count,
|
||||
Items = _db.Notifications
|
||||
.Where(a => a.ApplicationUserId == userId)
|
||||
.OrderByDescending(a => a.Created)
|
||||
.Skip(skip).Take(count)
|
||||
.Where(a => a.ApplicationUserId == userId)
|
||||
.Select(a => _notificationManager.ToViewModel(a))
|
||||
.ToList(),
|
||||
Total = _db.Notifications.Count(a => a.ApplicationUserId == userId)
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Filters;
|
||||
|
@ -19,6 +20,9 @@ using Microsoft.AspNetCore.Identity;
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
@ -256,7 +260,7 @@ namespace BTCPayServer.Controllers
|
|||
try
|
||||
{
|
||||
var redirectUrl = _linkGenerator.PaymentRequestLink(id, Request.Scheme, Request.Host, Request.PathBase);
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
|
||||
Currency = blob.Currency,
|
||||
|
@ -303,9 +307,7 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
foreach (var invoice in invoices)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoice.Id);
|
||||
_EventAggregator.Publish(new InvoiceEvent(await _InvoiceRepository.GetInvoice(invoice.Id), 1008,
|
||||
InvoiceEvent.MarkedInvalid));
|
||||
await _InvoiceRepository.MarkInvoiceStatus(invoice.Id, InvoiceStatus.Invalid);
|
||||
}
|
||||
|
||||
if (redirect)
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace BTCPayServer.Controllers
|
|||
DataWrapper<InvoiceResponse> invoice = null;
|
||||
try
|
||||
{
|
||||
invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
|
||||
invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
|
||||
{
|
||||
Price = model.Price,
|
||||
Currency = model.Currency,
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/changelly")]
|
||||
public IActionResult UpdateChangellySettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateChangellySettingsViewModel vm = new UpdateChangellySettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateChangellySettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().ChangellySettings;
|
||||
if (existing == null)
|
||||
return;
|
||||
vm.ApiKey = existing.ApiKey;
|
||||
vm.ApiSecret = existing.ApiSecret;
|
||||
vm.ApiUrl = existing.ApiUrl;
|
||||
vm.ChangellyMerchantId = existing.ChangellyMerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.AmountMarkupPercentage = existing.AmountMarkupPercentage;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/changelly")]
|
||||
public async Task<IActionResult> UpdateChangellySettings(string storeId, UpdateChangellySettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var changellySettings = new ChangellySettings()
|
||||
{
|
||||
ApiKey = vm.ApiKey,
|
||||
ApiSecret = vm.ApiSecret,
|
||||
ApiUrl = vm.ApiUrl,
|
||||
ChangellyMerchantId = vm.ChangellyMerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
AmountMarkupPercentage = vm.AmountMarkupPercentage
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.ChangellySettings = changellySettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Changelly settings modified";
|
||||
_changellyClientProvider.InvalidateClient(storeId);
|
||||
return RedirectToAction(nameof(UpdateStore), new
|
||||
{
|
||||
storeId
|
||||
});
|
||||
case "test":
|
||||
try
|
||||
{
|
||||
var client = new Changelly(_httpClientFactory.CreateClient(), changellySettings.ApiKey, changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
var result = await client.GetCurrenciesFull();
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Test Successful";
|
||||
return View(vm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = ex.Message;
|
||||
return View(vm);
|
||||
}
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -93,7 +93,7 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
if (connectionString.BaseUri.Scheme == "http")
|
||||
{
|
||||
if (!isInternalNode)
|
||||
if (!isInternalNode && !connectionString.AllowInsecure)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ConnectionString), "The url must be HTTPS");
|
||||
return View(vm);
|
||||
|
|
|
@ -12,7 +12,6 @@ using BTCPayServer.HostedServices;
|
|||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
|
@ -57,7 +56,6 @@ namespace BTCPayServer.Controllers
|
|||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
ChangellyClientProvider changellyClientProvider,
|
||||
IWebHostEnvironment env, IHttpClientFactory httpClientFactory,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
SettingsRepository settingsRepository,
|
||||
|
@ -71,7 +69,6 @@ namespace BTCPayServer.Controllers
|
|||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_LangService = langService;
|
||||
_changellyClientProvider = changellyClientProvider;
|
||||
_TokenController = tokenController;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
|
@ -102,7 +99,6 @@ namespace BTCPayServer.Controllers
|
|||
readonly TokenRepository _TokenRepository;
|
||||
readonly UserManager<ApplicationUser> _UserManager;
|
||||
private readonly LanguageService _LangService;
|
||||
private readonly ChangellyClientProvider _changellyClientProvider;
|
||||
readonly IWebHostEnvironment _Env;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
|
@ -481,8 +477,8 @@ namespace BTCPayServer.Controllers
|
|||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
AddPaymentMethods(store, storeBlob, vm);
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.MonitoringExpiration = (int)storeBlob.MonitoringExpiration.TotalMinutes;
|
||||
vm.InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes;
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PaymentTolerance = storeBlob.PaymentTolerance;
|
||||
vm.PayJoinEnabled = storeBlob.PayJoinEnabled;
|
||||
|
@ -537,13 +533,6 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
var changellyEnabled = storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.AdditionalPaymentMethod()
|
||||
{
|
||||
Enabled = changellyEnabled,
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.AdditionalPaymentMethod()
|
||||
|
@ -579,8 +568,8 @@ namespace BTCPayServer.Controllers
|
|||
var blob = CurrentStore.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.MonitoringExpiration = TimeSpan.FromMinutes(model.MonitoringExpiration);
|
||||
blob.InvoiceExpiration = TimeSpan.FromMinutes(model.InvoiceExpiration);
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
blob.PaymentTolerance = model.PaymentTolerance;
|
||||
var payjoinChanged = blob.PayJoinEnabled != model.PayJoinEnabled;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
|
@ -70,10 +71,40 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
public async Task<IActionResult> ListStores(
|
||||
string sortOrder = null,
|
||||
string sortOrderColumn = null
|
||||
)
|
||||
{
|
||||
StoresViewModel result = new StoresViewModel();
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
if (sortOrder != null && sortOrderColumn != null)
|
||||
{
|
||||
stores = stores.OrderByDescending(store =>
|
||||
{
|
||||
switch (sortOrderColumn)
|
||||
{
|
||||
case nameof(store.StoreName):
|
||||
return store.StoreName;
|
||||
case nameof(store.StoreWebsite):
|
||||
return store.StoreWebsite;
|
||||
default:
|
||||
return store.Id;
|
||||
}
|
||||
}).ToArray();
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
case "desc":
|
||||
ViewData[$"{sortOrderColumn}SortOrder"] = "asc";
|
||||
break;
|
||||
case "asc":
|
||||
stores = stores.Reverse().ToArray();
|
||||
ViewData[$"{sortOrderColumn}SortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < stores.Length; i++)
|
||||
{
|
||||
var store = stores[i];
|
||||
|
|
|
@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
|
|||
if (sendModel.FeeSatoshiPerByte is decimal v &&
|
||||
v > decimal.Zero)
|
||||
{
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(v), 1);
|
||||
psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(v);
|
||||
}
|
||||
if (sendModel.NoChange)
|
||||
{
|
||||
|
|
|
@ -471,6 +471,8 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte.LastOrDefault()?.FeeRate;
|
||||
model.SupportRBF = network.SupportRBF;
|
||||
|
||||
model.CryptoDivisibility = network.Divisibility;
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource())
|
||||
{
|
||||
try
|
||||
|
@ -480,7 +482,7 @@ namespace BTCPayServer.Controllers
|
|||
if (result.BidAsk != null)
|
||||
{
|
||||
model.Rate = result.BidAsk.Center;
|
||||
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
|
||||
model.FiatDivisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
|
||||
model.Fiat = currencyPair.Right;
|
||||
}
|
||||
else
|
||||
|
@ -891,7 +893,7 @@ namespace BTCPayServer.Controllers
|
|||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint does not match the one set in your wallet settings. Probable cause are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings.");
|
||||
return View(viewModel);
|
||||
return View(nameof(SignWithSeed), viewModel);
|
||||
}
|
||||
|
||||
var changed = PSBTChanged(psbt, () => psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath, new SigningOptions()
|
||||
|
@ -901,7 +903,7 @@ namespace BTCPayServer.Controllers
|
|||
if (!changed)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, PSBT already signed.");
|
||||
return View(viewModel);
|
||||
return View(nameof(SignWithSeed), viewModel);
|
||||
}
|
||||
ModelState.Remove(nameof(viewModel.SigningContext.PSBT));
|
||||
viewModel.SigningContext.PSBT = psbt.ToBase64();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
|
@ -8,6 +9,17 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
var entity = NBitcoin.JsonConverters.Serializer.ToObject<InvoiceEntity>(ZipUtils.Unzip(invoiceData.Blob), null);
|
||||
entity.Networks = networks;
|
||||
if (entity.Metadata is null)
|
||||
{
|
||||
if (entity.Version < InvoiceEntity.GreenfieldInvoices_Version)
|
||||
{
|
||||
entity.MigrateLegacyInvoice();
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Metadata = new InvoiceMetadata();
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
public static InvoiceState GetInvoiceState(this InvoiceData invoiceData)
|
||||
|
|
|
@ -195,7 +195,7 @@ namespace BTCPayServer.Data
|
|||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal MinimumClaim { get; set; }
|
||||
public PullPaymentView View { get; set; } = new PullPaymentView();
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter))]
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
public TimeSpan? Period { get; set; }
|
||||
|
||||
[JsonProperty(ItemConverterType = typeof(PaymentMethodIdJsonConverter))]
|
||||
|
|
|
@ -3,10 +3,10 @@ using System.Collections.Generic;
|
|||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using BTCPayServer.Client.JsonConverters;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Mails;
|
||||
|
@ -19,8 +19,8 @@ namespace BTCPayServer.Data
|
|||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 1440;
|
||||
InvoiceExpiration = TimeSpan.FromMinutes(15);
|
||||
MonitoringExpiration = TimeSpan.FromDays(1);
|
||||
PaymentTolerance = 0;
|
||||
ShowRecommendedFee = true;
|
||||
RecommendedFeeBlockTarget = 1;
|
||||
|
@ -66,17 +66,19 @@ namespace BTCPayServer.Data
|
|||
}
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[DefaultValue(typeof(TimeSpan), "1.00:00:00")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
public TimeSpan MonitoringExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DefaultValue(15)]
|
||||
[DefaultValue(typeof(TimeSpan), "00:15:00")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int InvoiceExpiration
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Minutes))]
|
||||
public TimeSpan InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
@ -105,10 +107,8 @@ namespace BTCPayServer.Data
|
|||
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
public CoinSwitchSettings CoinSwitchSettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
public string LightningDescriptionTemplate
|
||||
{
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
|
@ -15,11 +16,26 @@ namespace BTCPayServer.Events
|
|||
public const string FailedToConfirm = "invoice_failedToConfirm";
|
||||
public const string Confirmed = "invoice_confirmed";
|
||||
public const string Completed = "invoice_completed";
|
||||
|
||||
public static Dictionary<string, int> EventCodes = new Dictionary<string, int>()
|
||||
{
|
||||
{Created, 1001},
|
||||
{ReceivedPayment, 1002},
|
||||
{PaidInFull, 1003},
|
||||
{Expired, 1004},
|
||||
{Confirmed, 1005},
|
||||
{Completed, 1006},
|
||||
{MarkedInvalid, 1008},
|
||||
{FailedToConfirm, 1013},
|
||||
{PaidAfterExpiration, 1009},
|
||||
{ExpiredPaidPartial, 2000},
|
||||
{MarkedCompleted, 2008},
|
||||
};
|
||||
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name)
|
||||
public InvoiceEvent(InvoiceEntity invoice, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
EventCode = code;
|
||||
EventCode = EventCodes[name];
|
||||
Name = name;
|
||||
}
|
||||
|
||||
|
|
|
@ -104,9 +104,8 @@ namespace BTCPayServer.HostedServices
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.PosData, out cartItems)))
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||
{
|
||||
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||
|
||||
|
@ -116,9 +115,9 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
|
||||
var items = cartItems ?? new Dictionary<string, int>();
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.ProductInformation.ItemCode))
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
|
||||
{
|
||||
items.TryAdd(invoiceEvent.Invoice.ProductInformation.ItemCode, 1);
|
||||
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UpdateAppInventory()
|
||||
|
|
|
@ -5,7 +5,10 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -305,7 +308,7 @@ namespace BTCPayServer.HostedServices
|
|||
List<Task> tasks = new List<Task>();
|
||||
|
||||
// Awaiting this later help make sure invoices should arrive in order
|
||||
tasks.Add(SaveEvent(invoice.Id, e));
|
||||
tasks.Add(SaveEvent(invoice.Id, e, InvoiceEventData.EventSeverity.Info));
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
|
@ -336,26 +339,26 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
await SaveEvent(e.InvoiceId, e, InvoiceEventData.EventSeverity.Info);
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
await SaveEvent(e.InvoiceId, e, InvoiceEventData.EventSeverity.Info);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
await SaveEvent(e.InvoiceId, e, string.IsNullOrEmpty(e.Error)? InvoiceEventData.EventSeverity.Success: InvoiceEventData.EventSeverity.Error);
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task SaveEvent(string invoiceId, object evt)
|
||||
private Task SaveEvent(string invoiceId, object evt, InvoiceEventData.EventSeverity severity)
|
||||
{
|
||||
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
|
||||
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt, severity);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -65,9 +66,9 @@ namespace BTCPayServer.HostedServices
|
|||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, InvoiceEvent.Expired));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Expired));
|
||||
if (invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice, 2000, InvoiceEvent.ExpiredPaidPartial));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
|
@ -81,7 +82,7 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, InvoiceEvent.PaidInFull));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
@ -90,7 +91,7 @@ namespace BTCPayServer.HostedServices
|
|||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, InvoiceEvent.PaidAfterExpiration));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.PaidAfterExpiration));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +137,7 @@ namespace BTCPayServer.HostedServices
|
|||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, InvoiceEvent.FailedToConfirm));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
@ -144,7 +145,7 @@ namespace BTCPayServer.HostedServices
|
|||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, InvoiceEvent.Confirmed));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Confirmed));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +155,7 @@ namespace BTCPayServer.HostedServices
|
|||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, InvoiceEvent.Completed));
|
||||
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ using BTCPayServer.Logging;
|
|||
using BTCPayServer.PaymentRequest;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Payments.PayJoin;
|
||||
using BTCPayServer.Security;
|
||||
|
@ -49,6 +48,7 @@ using NicolasDorier.RateLimits;
|
|||
using Serilog;
|
||||
#if ALTCOINS
|
||||
using BTCPayServer.Services.Altcoins.Monero;
|
||||
using BTCPayServer.Services.Altcoins.Ethereum;
|
||||
#endif
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
|
@ -79,6 +79,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddPayJoinServices();
|
||||
#if ALTCOINS
|
||||
services.AddMoneroLike();
|
||||
services.AddEthereumLike();
|
||||
#endif
|
||||
services.TryAddSingleton<SettingsRepository>();
|
||||
services.TryAddSingleton<LabelFactory>();
|
||||
|
@ -96,7 +97,7 @@ namespace BTCPayServer.Hosting
|
|||
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService<BTCPayNetworkProvider>());
|
||||
return new InvoiceRepository(dbContext, dbpath, o.GetRequiredService<BTCPayNetworkProvider>(), o.GetService<EventAggregator>());
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
|
@ -218,8 +219,6 @@ namespace BTCPayServer.Hosting
|
|||
|
||||
services.AddSingleton<PaymentMethodHandlerDictionary>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
|
||||
services.AddSingleton<NotificationManager>();
|
||||
services.AddScoped<NotificationSender>();
|
||||
|
||||
|
@ -246,7 +245,10 @@ namespace BTCPayServer.Hosting
|
|||
|
||||
services.AddSingleton<INotificationHandler, InvoiceEventNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
|
||||
|
||||
#if DEBUG
|
||||
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
||||
#endif
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
|
@ -8,20 +9,21 @@ namespace BTCPayServer.Logging
|
|||
{
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public string Log { get; set; }
|
||||
|
||||
public InvoiceEventData.EventSeverity Severity { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Timestamp.UtcDateTime}: {Log}";
|
||||
return $"{Timestamp.UtcDateTime}:{Severity} {Log}";
|
||||
}
|
||||
|
||||
}
|
||||
public class InvoiceLogs
|
||||
{
|
||||
readonly List<InvoiceLog> _InvoiceLogs = new List<InvoiceLog>();
|
||||
public void Write(string data)
|
||||
public void Write(string data, InvoiceEventData.EventSeverity eventSeverity)
|
||||
{
|
||||
lock (_InvoiceLogs)
|
||||
{
|
||||
_InvoiceLogs.Add(new InvoiceLog() { Timestamp = DateTimeOffset.UtcNow, Log = data });
|
||||
_InvoiceLogs.Add(new InvoiceLog() { Timestamp = DateTimeOffset.UtcNow, Log = data, Severity = eventSeverity});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,11 +56,11 @@ namespace BTCPayServer.Logging
|
|||
var timespan = DateTimeOffset.UtcNow - _Before;
|
||||
if (timespan.TotalSeconds >= 1.0)
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds");
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalSeconds} seconds", InvoiceEventData.EventSeverity.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds");
|
||||
_logs.Write($"{_msg} took {(int)timespan.TotalMilliseconds} milliseconds", InvoiceEventData.EventSeverity.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Models
|
||||
{
|
||||
public class CreateInvoiceRequest
|
||||
public class BitpayCreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty(PropertyName = "buyer")]
|
||||
public Buyer Buyer { get; set; }
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
@ -73,22 +74,12 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string RefundEmail
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string TaxIncluded { get; set; }
|
||||
public BuyerInformation BuyerInformation
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string TransactionSpeed { get; set; }
|
||||
public object StoreName
|
||||
|
@ -113,11 +104,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
get;
|
||||
set;
|
||||
}
|
||||
public ProductInformation ProductInformation
|
||||
{
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
public InvoiceMetadata TypedMetadata { get; set; }
|
||||
public AddressModel[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Models.InvoicingModels
|
||||
|
@ -19,7 +20,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public string InvoiceId { get; set; }
|
||||
|
||||
public InvoiceStatus Status { get; set; }
|
||||
public string StatusString { get; set; }
|
||||
public bool CanMarkComplete { get; set; }
|
||||
public bool CanMarkInvalid { get; set; }
|
||||
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
|
||||
|
|
|
@ -62,12 +62,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public string PaymentMethodId { get; set; }
|
||||
public string PaymentMethodName { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
|
||||
public bool ChangellyEnabled { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string PeerInfo { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal? ChangellyAmountDue { get; set; }
|
||||
|
||||
public bool CoinSwitchEnabled { get; set; }
|
||||
public string CoinSwitchMode { get; set; }
|
||||
|
|
|
@ -6,7 +6,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public enum RefundSteps
|
||||
{
|
||||
SelectPaymentMethod,
|
||||
SelectRate
|
||||
SelectRate,
|
||||
SelectCustomAmount
|
||||
}
|
||||
public class RefundModel
|
||||
{
|
||||
|
@ -22,5 +23,9 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public string RateThenText { get; set; }
|
||||
public string FiatText { get; set; }
|
||||
public decimal FiatAmount { get; set; }
|
||||
|
||||
[Display(Name = "Specify the amount and currency for the refund")]
|
||||
public decimal CustomAmount { get; set; }
|
||||
public string CustomCurrency { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ namespace BTCPayServer.Models
|
|||
{
|
||||
public string AspAction { get; set; }
|
||||
public string AspController { get; set; }
|
||||
public string FormUrl { get; set; }
|
||||
|
||||
public List<KeyValuePair<string, string>> Parameters { get; set; } = new List<KeyValuePair<string, string>>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
|
||||
public string[] Words
|
||||
{
|
||||
get => Mnemonic.Split(" ");
|
||||
get => Mnemonic.Split((char[])null, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class UpdateChangellySettingsViewModel
|
||||
{
|
||||
[Required] public string ApiKey { get; set; }
|
||||
|
||||
[Required] public string ApiSecret { get; set; }
|
||||
|
||||
[Required] public string ApiUrl { get; set; } = "https://api.changelly.com";
|
||||
|
||||
[Display(Name = "Optional, Changelly Merchant Id")]
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Range(0, 100)]
|
||||
[Display(Name =
|
||||
"Percentage to multiply amount requested at Changelly to avoid underpaid situations due to Changelly not guaranteeing rates. ")]
|
||||
public decimal AmountMarkupPercentage { get; set; } = new decimal(2);
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
|
@ -47,7 +47,8 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
[Display(Name = "Don't create UTXO change")]
|
||||
public bool NoChange { get; set; }
|
||||
public decimal? Rate { get; set; }
|
||||
public int Divisibility { get; set; }
|
||||
public int FiatDivisibility { get; set; }
|
||||
public int CryptoDivisibility { get; set; }
|
||||
public string Fiat { get; set; }
|
||||
public string RateError { get; set; }
|
||||
public bool SupportRBF { get; set; }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.PaymentRequestViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
|
@ -9,6 +10,7 @@ using BTCPayServer.Services.Invoices;
|
|||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
|
@ -99,9 +101,9 @@ namespace BTCPayServer.PaymentRequest
|
|||
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.ProductInformation.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.ProductInformation.Price, blob.Currency),
|
||||
Currency = entity.ProductInformation.Currency,
|
||||
Amount = entity.Price,
|
||||
AmountFormatted = _currencies.FormatCurrency(entity.Price, blob.Currency),
|
||||
Currency = entity.Currency,
|
||||
ExpiryDate = entity.ExpirationTime.DateTime,
|
||||
Status = entity.GetInvoiceState().ToString(),
|
||||
Payments = entity
|
||||
|
|
|
@ -171,11 +171,11 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
?.CanSupportTransactionCheck is true;
|
||||
onchainMethod.PayjoinEnabled &= supportedPaymentMethod.IsHotWallet && nodeSupport;
|
||||
if (!supportedPaymentMethod.IsHotWallet)
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet");
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet", InvoiceEventData.EventSeverity.Warning);
|
||||
if (!nodeSupport)
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.");
|
||||
logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.", InvoiceEventData.EventSeverity.Warning);
|
||||
if (onchainMethod.PayjoinEnabled)
|
||||
logs.Write($"{prefix} Payjoin is enabled for this invoice.");
|
||||
logs.Write($"{prefix} Payjoin is enabled for this invoice.", InvoiceEventData.EventSeverity.Info);
|
||||
}
|
||||
|
||||
return onchainMethod;
|
||||
|
|
|
@ -406,7 +406,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
|||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
return invoice;
|
||||
}
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SshNet.Security.Cryptography;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class Changelly
|
||||
{
|
||||
private readonly string _apisecret;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public Changelly(HttpClient httpClient, string apiKey, string apiSecret, string apiUrl)
|
||||
{
|
||||
_apisecret = apiSecret;
|
||||
_httpClient = httpClient;
|
||||
_httpClient.BaseAddress = new Uri(apiUrl);
|
||||
_httpClient.DefaultRequestHeaders.Add("api-key", apiKey);
|
||||
}
|
||||
|
||||
|
||||
private static string ToHexString(byte[] array)
|
||||
{
|
||||
var hex = new StringBuilder(array.Length * 2);
|
||||
foreach (var b in array)
|
||||
{
|
||||
hex.AppendFormat(CultureInfo.InvariantCulture, "{0:x2}", b);
|
||||
}
|
||||
|
||||
return hex.ToString();
|
||||
}
|
||||
|
||||
private async Task<ChangellyResponse<T>> PostToApi<T>(string message)
|
||||
{
|
||||
using var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(_apisecret));
|
||||
var hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
|
||||
var sign = ToHexString(hashMessage);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "");
|
||||
request.Headers.Add("sign", sign);
|
||||
request.Content = new StringContent(message, Encoding.UTF8, "application/json");
|
||||
|
||||
var result = await _httpClient.SendAsync(request);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
throw new ChangellyException(result.ReasonPhrase);
|
||||
var content =
|
||||
await result.Content.ReadAsStringAsync();
|
||||
return JObject.Parse(content).ToObject<ChangellyResponse<T>>();
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<CurrencyFull>> GetCurrenciesFull()
|
||||
{
|
||||
const string message = @"{
|
||||
""jsonrpc"": ""2.0"",
|
||||
""id"": 1,
|
||||
""method"": ""getCurrenciesFull"",
|
||||
""params"": []
|
||||
}";
|
||||
|
||||
var result = await PostToApi<IEnumerable<CurrencyFull>>(message);
|
||||
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
public virtual async Task<decimal> GetExchangeAmount(string fromCurrency,
|
||||
string toCurrency,
|
||||
decimal amount)
|
||||
{
|
||||
var message =
|
||||
$"{{\"id\": \"test\",\"jsonrpc\": \"2.0\",\"method\": \"getExchangeAmount\",\"params\":{{\"from\": \"{fromCurrency}\",\"to\": \"{toCurrency}\",\"amount\": \"{amount}\"}}}}";
|
||||
|
||||
var result = await PostToApi<string>(message);
|
||||
|
||||
return Convert.ToDecimal(result.Result, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public static class ChangellyCalculationHelper
|
||||
{
|
||||
public static decimal ComputeBaseAmount(decimal baseRate, decimal toAmount)
|
||||
{
|
||||
return (1m / baseRate) * toAmount;
|
||||
}
|
||||
|
||||
public static decimal ComputeCorrectAmount(decimal currentFromAmount, decimal currentAmount,
|
||||
decimal expectedAmount)
|
||||
{
|
||||
return (currentFromAmount / currentAmount) * expectedAmount;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyClientProvider
|
||||
{
|
||||
private readonly StoreRepository _storeRepository;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Changelly> _clientCache =
|
||||
new ConcurrentDictionary<string, Changelly>();
|
||||
|
||||
public ChangellyClientProvider(StoreRepository storeRepository, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public void InvalidateClient(string storeId)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
_clientCache.Remove(storeId, out var value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<Changelly> TryGetChangellyClient(string storeId, StoreData storeData = null)
|
||||
{
|
||||
if (_clientCache.ContainsKey(storeId))
|
||||
{
|
||||
return _clientCache[storeId];
|
||||
}
|
||||
|
||||
if (storeData == null)
|
||||
{
|
||||
storeData = await _storeRepository.FindStore(storeId);
|
||||
if (storeData == null)
|
||||
{
|
||||
throw new ChangellyException("Store not found");
|
||||
}
|
||||
}
|
||||
|
||||
var blob = storeData.GetStoreBlob();
|
||||
var changellySettings = blob.ChangellySettings;
|
||||
|
||||
|
||||
if (changellySettings == null || !changellySettings.IsConfigured())
|
||||
{
|
||||
throw new ChangellyException("Changelly not configured for this store");
|
||||
}
|
||||
|
||||
if (!changellySettings.Enabled)
|
||||
{
|
||||
throw new ChangellyException("Changelly not enabled for this store");
|
||||
}
|
||||
|
||||
var changelly = new Changelly(_httpClientFactory.CreateClient("Changelly"), changellySettings.ApiKey,
|
||||
changellySettings.ApiSecret,
|
||||
changellySettings.ApiUrl);
|
||||
_clientCache.AddOrReplace(storeId, changelly);
|
||||
return changelly;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellyException : Exception
|
||||
{
|
||||
public ChangellyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
namespace BTCPayServer.Payments.Changelly
|
||||
{
|
||||
public class ChangellySettings
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
public string ApiSecret { get; set; }
|
||||
public string ApiUrl { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal AmountMarkupPercentage { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return
|
||||
!string.IsNullOrEmpty(ApiKey) ||
|
||||
!string.IsNullOrEmpty(ApiSecret) ||
|
||||
!string.IsNullOrEmpty(ApiUrl);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
|
||||
public class ChangellyResponse<T>
|
||||
{
|
||||
[JsonProperty("jsonrpc")]
|
||||
public string JsonRPC { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public object Id { get; set; }
|
||||
[JsonProperty("result")]
|
||||
public T Result { get; set; }
|
||||
[JsonProperty("error")]
|
||||
public Error Error { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
public class CurrencyFull
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonProperty("fullName")]
|
||||
public string FullName { get; set; }
|
||||
[JsonProperty("enabled")]
|
||||
public bool Enable { get; set; }
|
||||
[JsonProperty("payinConfirmations")]
|
||||
public int PayInConfirmations { get; set; }
|
||||
[JsonProperty("image")]
|
||||
public string ImageLink { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Changelly.Models
|
||||
{
|
||||
public class Error
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue