Merge branch 'master' into MonetaryUnit

This commit is contained in:
Andrew Camilleri 2020-09-17 11:43:31 +02:00 committed by GitHub
commit f14ad8be58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
179 changed files with 5922 additions and 2209 deletions

View file

@ -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>

View file

@ -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;
}
}

View 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);
}
}
}

View file

@ -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);
}

View file

@ -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('&');
}
}
}

View file

@ -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));
}
}
}

View file

@ -0,0 +1,7 @@
namespace BTCPayServer.Client.Models
{
public class AddCustomerEmailRequest
{
public string Email { get; set; }
}
}

View file

@ -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; }
}
}

View 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; }
}
}
}

View file

@ -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; }

View file

@ -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; }

View 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; }
}
}

View file

@ -0,0 +1,12 @@
namespace BTCPayServer.Client.Models
{
public enum InvoiceExceptionStatus
{
None,
PaidLate,
PaidPartial,
Marked,
Invalid,
PaidOver
}
}

View file

@ -0,0 +1,12 @@
namespace BTCPayServer.Client.Models
{
public enum InvoiceStatus
{
New,
Paid,
Expired,
Invalid,
Complete,
Confirmed
}
}

View 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; }
}
}

View file

@ -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; }

View file

@ -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);

View file

@ -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:

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -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

View file

@ -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

View 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

View file

@ -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());
}
}

View file

@ -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>())

View file

@ -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; }
}

View file

@ -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
};
}
}
}

View file

@ -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; }

View file

@ -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),

View file

@ -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");
}
}
}
}

View file

@ -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");

View file

@ -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
}
]

View file

@ -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);

View file

@ -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

View file

@ -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(

View 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);
}
}
}

View file

@ -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>();
}
}
}

View file

@ -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>

View file

@ -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);
}

View file

@ -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));
}
}
}

View file

@ -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));

View file

@ -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;

View file

@ -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;

View file

@ -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];

View file

@ -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()
{

View file

@ -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()

View file

@ -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"

View file

@ -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"

View file

@ -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>

View file

@ -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)">&laquo;</a>
@ -25,25 +25,38 @@
<a class="page-link" href="@NavigatePages(1, Model.Count)">&raquo;</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)
{

View file

@ -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";

View file

@ -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,

View file

@ -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;
}
}

View 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
}
};
}
}
}

View file

@ -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));
}

View file

@ -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;

View file

@ -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
};
}
}

View file

@ -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 });

View file

@ -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");

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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; }

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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);
}
}
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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];

View file

@ -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)
{

View file

@ -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();

View file

@ -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)

View file

@ -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))]

View file

@ -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
{

View file

@ -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;
}

View file

@ -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()

View file

@ -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)

View file

@ -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();
}

View file

@ -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 =>
{

View file

@ -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);
}
}
}

View file

@ -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; }

View file

@ -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; }

View file

@ -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;

View file

@ -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; }

View file

@ -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; }
}
}

View file

@ -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>>();
}
}

View file

@ -16,7 +16,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string[] Words
{
get => Mnemonic.Split(" ");
get => Mnemonic.Split((char[])null, System.StringSplitOptions.RemoveEmptyEntries);
}
}
}

View file

@ -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; }
}
}

View file

@ -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; }

View file

@ -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

View file

@ -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;

View file

@ -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)

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -1,11 +0,0 @@
using System;
namespace BTCPayServer.Payments.Changelly
{
public class ChangellyException : Exception
{
public ChangellyException(string message) : base(message)
{
}
}
}

View file

@ -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);
}
}
}

View file

@ -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; }
}
}

View file

@ -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; }
}
}

View file

@ -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